前言
上一篇文章讲解了Spring Boot集成shiro采用用户名和密码实现登录和权限验证,在前后端分离的环境下都采用的时Token方式,本文将讲解Spring Boot集成Shiro+JWT实现登录认证。
集成Shiro
导入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
自定义认证token
public class JwtToken implements AuthenticationToken
{
private static final long serialVersionUID = -773194305699843478L;
private String token;
public JwtToken(String token)
{
this.token = token;
}
@Override
public Object getPrincipal()
{
return token;
}
@Override
public Object getCredentials()
{
return token;
}
}
自定义Realm实现认证
public class JWTTokenRealm extends AuthorizingRealm
{
private Logger logger = LoggerFactory.getLogger(JWTTokenRealm.class);
@Autowired
private UserService userService;
//需重写此方法,不然Shiro会报错
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)
{
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User user = (User) principalCollection.getPrimaryPrincipal();
Integer userOid=user.getOid();
List<Role> roleList= userService.getRolesByUserOid(userOid);
//用户角色
Set<String> roleSet=new HashSet<>();
//权限信息
Set<String> funcSet=new HashSet<>();
Set<Integer> roleOids=new HashSet<>();
//查询角色
if(roleList!=null && !roleList.isEmpty())
{
roleList.stream().forEach(t->{
roleSet.add(String.valueOf(t.getRoleId()));
roleOids.add(t.getOid());
});
}
//查询权限
List<Func> funcList= userService.getResByRoleOid(roleOids);
if(funcList!=null && !funcList.isEmpty()){
for(Func func:funcList)
{
funcSet.add(func.getUrl());
}
}
//用户角色
info.addRoles(roleSet);
//用户权限
info.addStringPermissions(funcSet);
return info;
}
//用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authToken) throws AuthenticationException
{
//获取token值
String accessToken = (String) authToken.getPrincipal();
logger.info("AuthenticationInfo accessToken:{}",accessToken);
//验证token是否有效
Claims claims = JwtUtil.getClaims(accessToken);
String userId = "";
if (claims == null)
{
throw new RuntimeException("token失效,请重新登录");
}
else
{
userId = claims.getSubject();
// 验证主题
if (StringUtils.isEmpty(userId))
{
throw new RuntimeException("token失效,请重新登录");
}
}
//查询用户信息
User user = userService.getUserByUserId(userId);
if(user==null)
{
throw new RuntimeException("用户名或密码不正确");
}
//账号状态验证
if("0".equals(user.getActive()))
{
throw new LockedAccountException("用户账号状态不正确");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
return info;
}
}
说明:从shiro中获取登录的token,验证token是否有效。
JWT工具类
public final class JwtUtil
{
private static final String JWT_SCRET_KEY = "123#@456*";
private String secret;
private long expire;
private String header;
public static String createJWT(String subject, String issue, Object claim,
long ttlMillis)
{
long nowMillis = System.currentTimeMillis();
long expireMillis = nowMillis + ttlMillis;
String result = Jwts.builder().setSubject(subject).setIssuer(issue)
.setExpiration(new Date(expireMillis)).claim("user", claim).setId(issue)
.signWith(getSignatureAlgorithm(), getSignedKey())
.compressWith(CompressionCodecs.DEFLATE).compact();
return result;
}
public static Jws<Claims> pareseJWT(String jwt)
{
Jws<Claims> claims;
try
{
claims = Jwts.parser().setSigningKey(getSignedKey())
.parseClaimsJws(jwt);
}
catch (Exception ex)
{
claims = null;
}
return claims;
}
public static Claims getClaims(String jwt)
{
Claims claims;
try
{
claims = Jwts.parser().setSigningKey(getSignedKey())
.parseClaimsJws(jwt).getBody();
}
catch (Exception ex)
{
claims = null;
}
return claims;
}
/**
* token是否过期
*
* @return true:过期
*/
public boolean isTokenExpired(Date expiration)
{
return expiration.before(new Date());
}
/**
* 获取密钥
*
* @return Key
*/
private static Key getSignedKey()
{
byte[] apiKeySecretBytes = DatatypeConverter
.parseBase64Binary(getAuthKey());
Key signingKey = new SecretKeySpec(apiKeySecretBytes,
getSignatureAlgorithm().getJcaName());
return signingKey;
}
private static SignatureAlgorithm getSignatureAlgorithm()
{
return SignatureAlgorithm.HS256;
}
public static String getAuthKey()
{
String auth = JWT_SCRET_KEY;
return auth;
}
public String getSecret()
{
return secret;
}
public void setSecret(String secret)
{
this.secret = secret;
}
public long getExpire()
{
return expire;
}
public void setExpire(long expire)
{
this.expire = expire;
}
public String getHeader()
{
return header;
}
public void setHeader(String header)
{
this.header = header;
}
}
自定义认证过滤器
public class JWTFilter extends AuthenticatingFilter
{
private Logger logger = LoggerFactory.getLogger(JWTFilter.class);
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest)
{
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isEmpty(token)){
token = httpRequest.getParameter("token");
}
return token;
}
@Override
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response) throws Exception
{
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isEmpty(token)){
return null;
}
return new JwtToken(token);
}
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse response, Object mappedValue)
{
HttpServletRequest request = (HttpServletRequest) servletRequest;
String uri = request.getRequestURI();
logger.info("start TokenInterceptor preHandle.." + uri);
//过滤无需认证的请求
if (SystemUtil.isFree(uri) || SystemUtil.isProtected(uri))
{
return true;
}
//过滤options方法
if(request.getMethod().equals(RequestMethod.OPTIONS.name()))
{
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest,
ServletResponse response) throws Exception
{
//获取请求token,如果token不存在,直接返回401
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = getRequestToken(request);
if(StringUtils.isEmpty(token))
{
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
Map<String, Object> map =new HashMap<String, Object>();
map.put("code", 401);
map.put("msg", "invalid token");
String json=JSON.toJSONString(map);
httpResponse.getWriter().print(json);
return false;
}
//认证成功调用登录接口
return executeLogin(request, response);
}
/**
* 登录失败
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest servletRequest, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest request = (HttpServletRequest) servletRequest;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Map<String, Object> map =new HashMap<String, Object>();
map.put("code", 401);
map.put("msg",throwable);
String json=JSON.toJSONString(map);
httpResponse.getWriter().print(json);
}
catch (IOException e1)
{
logger.error("onLoginFailure error",e);
}
return false;
}
}
1.过滤器方法执行顺序为:preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin 。 2.isAccessAllowed 认证时需要对特殊的请求进行过滤 3.onAccessDenied 认证失败,可以自定义提示信息
shiro的核心配置
@Configuration
public class ShiroConfig
{
@Bean
public JWTTokenRealm tokenRealm()
{
JWTTokenRealm tokenRealm=new JWTTokenRealm();
return tokenRealm;
}
@Bean
public DefaultWebSecurityManager securityManager()
{
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(tokenRealm());
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager)
{
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加jwt过滤
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters);
//配置系统的受限资源
Map<String,String> map = new HashMap<>();
/**
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
//登录请求无需认证
map.put("/login", "anon");
//其他所有请求需要经过jwt认证
map.put("/**", "jwt");
//访问需要认证的页面,如果未登录会跳转到/unLogin
shiroFilterFactoryBean.setLoginUrl("/unLogin");
//访问未授权页面会自动跳转到/unAuth
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 开启注解方式,页面可以使用注解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
}
说明:注意需要添加认证的过滤器,且所有的请求都需要过滤器验证
//添加过滤器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters);
登录验证
@RequestMapping("/login")
@ResponseBody
public String login(@RequestParam String userId,@RequestParam String password)
{
User user = new User();
user.setUserId(userId);
user.setPwd(password);
loginService.authUser(user);
//设置有效时间
Long expreTime =86400000l;
String tokenKey = UUID.randomUUID().toString();
String tokenId = JwtUtil.createJWT(user.getUserId(), tokenKey,
user.getUserId(), expreTime);
//返回给前端
Map<String, String> map = new HashMap<>();
map.put("token", tokenId);
logger.info("login token----->:{}",tokenId);
return JSON.toJSONString(map);
}
测试
授权测试
用户未登录,直击访问/app/sys/user/list,则页面会提示401错误
用户登录测试
用户访问/login请求输入正确的用户名和密码,登录成功后将返回token
token错误验证
用户访问/app/sys/user/resourceTest,header中传入的token值不正确
角色、权限测试
角色和权限测试与第一章的方法相同。
总结
本文讲解了Spring Boot shiro与JWT实现登录验证,如有问题可以随时解答。