在上文中描述了项目中MybatisPlus的代码生成功能。这里讲述Jwt+Shiro做身份和权限认证。
笔者用自然语言描述一个工作流程:用户访问项目资源时,默认是不允许游客访问的,需要用户身份才可以访问,所以需要用户登录,放回一个token给用户,当用户需要访问资源时在request中携带token。后端会对token进行身份认证,通过后才允许访问,否则返回错误。而且对特殊的资源还添加了权限,并不是所有的用户都可以访问我这些资源,只有被赋予了权限才可以访问,因此添加了权限认证。在项目中使用的机制:
- token签发和校验
- 游客资源放行
- 权限认证
- token刷新
首先贴出关键的代码:
- ShiroConfig
package com.huafeng.cams.common.shiro;
import com.huafeng.cams.common.shiro.jwt.JwtFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Author JinXing _让世界看到我
* On 2020/4/22
* Note TODO
*/
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置我们自定义的过滤器
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置无权限时的跳转,好像没有效果
//shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
//设置放行的登录访问 *好像错误的访问也会跳转到这个访问位置
//shiroFilterFactoryBean.setLoginUrl("/sysUser/login");
/**
* 设置过滤器
* anon:表示可以匿名访问
* authc:表示需要认证通过才可以访问
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//测试不需要认证的的接口
filterChainDefinitionMap.put("/common/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/captcha.jpg", "anon");
filterChainDefinitionMap.put("/sysUser/login", "anon");
filterChainDefinitionMap.put("/*.html", "anon");
//需要shiro认证的接口
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截,剩余的都需要认证.表示其它的所有接口都要经过我们的过滤器
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(myRealm());
return defaultSecurityManager;
}
@Bean
public ShiroRealm myRealm() {
ShiroRealm realm = new ShiroRealm();
return realm;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
//creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
Shiro做权限控制的配置文件,这个中创建了一个filter对访问进行过滤,在过滤器中对用户的身份和权限进行认证。如果是非法的访问在这里就会被打回,不会到controller。同时创建这个filter时需要一些捆绑配置,按照配置文件写就可以了,基本上都是固定写法,不同版本可能有所区别,但是大同小异。其中myRealm()函数是指定一个自定义的realm,本文中也将realm代码块贴出。在下文会讲到。
重点是创建的filter,这个filter是我们自定义的,可以在这里对这个filter进行管理,比如什么访问会放行,什么访问要进filter。一般都是常用的写法,静态资源要放行,登录接口要放行,swagger要放行,这些如果没有添加进去使用过程中就会出现错误,所以注意一下就行。设置过滤器时注意这里:
* anon:表示可以匿名访问
* authc:表示需要认证通过才可以访问
- JwtFilter
package com.huafeng.cams.common.shiro.jwt;
import com.google.gson.Gson;
import com.huafeng.cams.common.R;
import com.huafeng.cams.common.exception.MyException;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends BasicHttpAuthenticationFilter {
private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
logger.info("JwtFilter.createToken");
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new JwtToken(token);
}
/**
* 对访问进行拦截
* @param request
* @param response
* @param mappedValue
* @return true:放行,false拦截
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
logger.info("JwtFilter.isAccessAllowed");
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
/**
* isAccessAllowed拦截后会执行此函数
* 在request中获取到token
* 如果没有token直接响应客户端,并且返回false访问结束
* 如果有token就执行executeLogin函数
* executeLogin函数不需要重写,后面会执行到自定义的realm中的doGetAuthenticationInfo身份认证函数
* 如果身份认证通过就会继续后续的访问
* 如果controller中有权限标识就会执行权限认证
* 如果controller中没有权限标识就会直接进入controller
* 如果身份认证没有通过executeLogin函数就会返回false,并且会执行onLoginFailure函数
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
logger.info("JwtFilter.onAccessDenied");
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
String result = new Gson().toJson(R.RESULT_ERROR("请先登录"));
response(response,result);
return false;
}
boolean login = executeLogin(request, response);
if (login) {
refreshToken(request, response);
}
return login;
}
/**
* 身份信息认证成功后刷新token给客户端
* 自定义的token刷新策略
*
* @param request
* @param response
*/
private void refreshToken(ServletRequest request, ServletResponse response) throws MyException {
AuthenticationToken authenticationToken = createToken(request, response);
String token = (String) authenticationToken.getCredentials();
String newToken = JwtUtil.refreshToken(token);
if (!StringUtils.isEmpty(newToken)){
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader(JwtUtil.TOKEN_TAG,newToken);
}
}
/**
* 这个函数标识token身份信息认证失败
* 可以在AuthenticationException异常中拿到我们定义身份认证中抛出的异常,获取到异常信息返回给客户端
* @param token
* @param e 自定义realm中身份认证抛出的异常,可以从中获取到抛出的异常信息
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
logger.info("JwtFilter.onLoginFailure");
//获取登录失败的异常的信息
//e.printStackTrace();
Throwable t = e.getCause() == null ? e : e.getCause();
String msg = "用户信息验证失败!";
if (t != null) {
msg = t.getMessage();
}
String result = new Gson().toJson(R.RESULT_ERROR(msg));
response(response,result);
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
return httpRequest.getHeader(JwtUtil.TOKEN_TAG);
}
/**
* filter中返回客户端
* @param response
* @param json
*/
private void response(ServletResponse response, String json) {
try {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setCharacterEncoding("utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.getWriter().print(json);
} catch (IOException e) {
logger.warn("filter响应错误:" + e.getMessage());
e.printStackTrace();
}
}
}
我们的过滤器要集成BasicHttpAuthenticationFilter,并覆写以下几个函数:
- AuthenticationToken createToken()
- boolean isAccessAllowed()
没有被放行的访问首先会进入这个函数,询问是否拦截这个访问,true:放行,false:拦截。 - boolean onAccessDenied()
被isAccessAllowed拦截的访问会执行到这里,在这里我们首先在request中获取用户携带的token,如果没有token就直接在这里返回提示登录,并结束这次访问。如果有token那么就对token进行验证,执行executeLogin()函数,这个函数不用覆写,系统会通过createToken()函数获取到token,将访问执行到realm中的AuthenticationInfo doGetAuthenticationInfo()身份认证函数中,如果身份认证没有异常就说明认证通过,反之就是验证失败。如果验证通过executeLogin()函数会返回true,同理onAccessDenied()也会返回true访问将继续进行,否则就是返回false。并且会执行onLoginFailure()。 - boolean onLoginFailure()
在onAccessDenied()函数中返回false表示用户身份认证失败,即realm中身份认证失败,那么理论上会有失败的异常,如果没有异常就是系统的异常。在这里可以获取异常信息直接返回给客户端。并返回false结束这次访问。
- JwtToken
package com.huafeng.cams.common.shiro.jwt;
import org.apache.shiro.authc.AuthenticationToken;
/**
* Author JinXing _让世界看到我
* On 2020/4/22
* Note JwtToken实现shiro的AuthenticationToken接口
*/
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
这个token继承AuthenticationToken主要就是在身份认证中方便获取到token信息,一般就是固定写法。
- JwtUtil
package com.huafeng.cams.common.shiro.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.huafeng.cams.common.exception.MyException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Author JinXing _让世界看到我
* On 2020/1/3
* Note token 工具
*/
public class JwtUtil {
public static final String TOKEN_TAG = "token";
private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);
//token 过期时间 单位ms
public static final long EXPIRE_TIME = 1000 * 60 * 60;
//token 私钥
public static final String TOKEN_SECRET = "xxxxxx";
public static final String TOKEN_KEY_USERNAME = "username";
public static final String TOKEN_KEY_USER_ID = "userId";
/**
* 生成签名
*
* @param username
* @param userId
* @return 加密的token
*/
public static String sign(String username, long userId) throws MyException {
try {
//过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//私钥加密
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//设置头信息
Map<String, Object> map = new HashMap<>(2);
map.put("typ", "jwt");
map.put("alg", "HS256");
String sign = JWT.create()
.withHeader(map)
.withClaim(TOKEN_KEY_USERNAME, username)
.withClaim(TOKEN_KEY_USER_ID, userId)
.withExpiresAt(date)
.sign(algorithm);
return sign;
} catch (Exception e) {
throw new MyException("生成Token失败:" + e.getMessage());
}
}
/**
* 验证token
*
* @param token
* @return
*/
public static boolean verify(String token, String username, long id) {
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT
.require(algorithm)
.withClaim(TOKEN_KEY_USERNAME, username)
.withClaim(TOKEN_KEY_USER_ID, id)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
logger.warn("token验证异常:" + e.getMessage());
}
return false;
}
/**
* 解析token
*
* @param token
* @param key
* @return
*/
public static Claim decodeToken(String token, String key) {
if (token == null || key == null) {
return null;
}
DecodedJWT jwt = JWT.decode(token);
Claim claim = jwt.getClaim(key);
return claim;
}
/**
* 解析token
*
* @param token
* @param key
* @return
*/
public static long decodeTokenAsLong(String token, String key) {
if (token == null || key == null) {
return -1;
}
try {
DecodedJWT jwt = JWT.decode(token);
Claim claim = jwt.getClaim(key);
return claim.asLong();
} catch (Exception e) {
logger.warn("token decode failed!" + e.getMessage());
return -1;
}
}
/**
* 解析token中的用户id
*
* @param token
* @return
*/
public static long getUserId(String token) {
return decodeTokenAsLong(token, TOKEN_KEY_USER_ID);
}
/**
* 解析token
*
* @param token
* @param key
* @return
*/
public static String decodeTokenAsString(String token, String key) {
if (token == null || key == null) {
return null;
}
try {
DecodedJWT jwt = JWT.decode(token);
Claim claim = jwt.getClaim(key);
return claim.asString();
} catch (Exception e) {
logger.warn("token decode failed!" + e.getMessage());
return null;
}
}
/**
* 解析token中的用户名称
*
* @param token
* @return
*/
public static String getUsername(String token) {
return decodeTokenAsString(token, TOKEN_KEY_USERNAME);
}
/**
* 解析token
*
* @param token
* @return
*/
public static Map<String, Object> decodeTokenAsMap(String token) {
Map<String, Object> map = new HashMap<>();
if (token == null) {
return null;
}
DecodedJWT jwt = JWT.decode(token);
map.put(TOKEN_KEY_USERNAME, jwt.getClaim(TOKEN_KEY_USERNAME).asString());
map.put(TOKEN_KEY_USER_ID, jwt.getClaim(TOKEN_KEY_USER_ID).asLong());
return map;
}
/**
* 刷新token
* 获取一个token中的信息,创建一个新的token
* @param token
* @return
*/
public static String refreshToken(String token) throws MyException {
if (StringUtils.isEmpty(token)) {
return null;
}
long userId = getUserId(token);
String username = getUsername(token);
if (userId!=-1&&username!=null){
return sign(username, userId);
}
return null;
}
}
token工具类,主要完成token的签发、校验和解码等。
- ShiroRealm
package com.huafeng.cams.common.shiro;
import com.google.gson.Gson;
import com.huafeng.cams.common.shiro.jwt.JwtToken;
import com.huafeng.cams.common.shiro.jwt.JwtUtil;
import com.huafeng.cams.module.sys_user.entity.SysUser;
import com.huafeng.cams.module.sys_user.service.ISysUserService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
/**
* Author JinXing _让世界看到我
* On 2020/4/22
* Note TODO
*/
public class ShiroRealm extends AuthorizingRealm {
private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
@Autowired
ISysUserService mUserService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 只有当需要认证用户权限时才会调用此函数
* 在controller的函数中使用@RequiresPermissions("艇员","系统管理员",logical = Logical.OR)注解,表示这个访问是要添加权限的,可以是单个,也可以是多个用逗号隔开,如果不加logical属性则是必须同时满足两个权限才可以访问,加了这个属性就是满足其一就行
* 上述是采用权限的方式来验证,也可以使用@RequiresRoles(value = {"艇员","系统管理员"},logical = Logical.OR)注解采用角色的方式来认证,表示拥有这个角色的用户才可以访问,注解含义与上述相同,只不过一个是验证权限,一个是验证角色,采用的策略不同,可以根据场景来选择使用方式
* 根据权限验证注解的添加,如果用户访问添加了权限验证的函数,就会执行此函数,在这里如果验证通过会访问controller,如果验证不能通过就会出现AuthorizationException异常,Subject does not have role提示用户没有这角色,或者没有这个权限.理论上是应该捕获这个异常对用户进行统一返回.
* 上述是对controller中的函数添加权限控制,此函数中就是对应的权限验证策略
* 函数中可以给simpleAuthorizationInfo对象添加角色和权限,返回后验证与控制器中添加的权限进行校验.可以根据使用场景做不同的权限控制.
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String token = principalCollection.toString();
logger.info("权限认证:"+token);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
long userId = JwtUtil.getUserId(token);
Set<String> roles = mUserService.getRoles(userId);
Set<String> permissions = mUserService.getPermissions(userId);
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
/**
* 默认使用此函数进行用户校验,如果有错误在这里抛出来即可
* 自定义filter的executeLogin函数放行过来带token的访问
* 在这里对token中的身份进行验证,如果此处验证token信息不正确可以直接抛出异常,然后在别的地方捕获掉异常对客户端进行返回(目前还没有做到捕获异常环节)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
String token = (String) authenticationToken.getCredentials();
logger.info("身份认证:"+token);
if (StringUtils.isEmpty(token)){
throw new AuthorizationException("token null");
}
long userId = JwtUtil.getUserId(token);
if (userId<0){
throw new AuthorizationException("token invalid");
}
SysUser user = mUserService.getById(userId);
if (user==null){
throw new AuthorizationException("user invalid");
}
if (user.getcStatus()!=SysUser.STATUS_NORMAL){
throw new AuthorizationException("用户状态异常");
}
if (!JwtUtil.verify(token,user.getUsername(),user.getId())) {
throw new AuthorizationException("token lose");
}
return new SimpleAuthenticationInfo(token,token,getName());
}
}
realm中自定义的身份认证和权限认证。继承AuthorizingRealm,覆写以下函数:
- boolean supports()
有人说不写这个函数就会出错,所以就写上了,一般固定写法。 - AuthenticationInfo doGetAuthenticationInfo()
身份认证函数,在filter中的onAccessDenied()函数中如果request中有token信息,就会执行executeLogin()函数,最终会执行到doGetAuthenticationInfo()函数,在这里获取到token信息,并进行一系列自定义的验证:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
String token = (String) authenticationToken.getCredentials();
logger.info("身份认证:"+token);
if (StringUtils.isEmpty(token)){
throw new AuthorizationException("token null");
}
long userId = JwtUtil.getUserId(token);
if (userId<0){
throw new AuthorizationException("token invalid");
}
SysUser user = mUserService.getById(userId);
if (user==null){
throw new AuthorizationException("user invalid");
}
if (user.getcStatus()!=SysUser.STATUS_NORMAL){
throw new AuthorizationException("用户状态异常");
}
if (!JwtUtil.verify(token,user.getUsername(),user.getId())) {
throw new AuthorizationException("token lose");
}
return new SimpleAuthenticationInfo(token,token,getName());
}
如果没有通过验证,直接在这里抛出异常即可,并且会执行到filter中的onLoginFailure()函数,在那里可以获取到这里抛出的异常信息并返回客户端等处理。
- AuthorizationInfo doGetAuthorizationInfo()
权限认证的函数,在这里对用户的权限进行验证。这个函数不一定执行,当controller中的接口添加了@RequiresPermissions("menu:update")、@RequiresRoles("admin")等权限控制注解时,shiro就会在这里对用户进行权限认证,这里的认证规则是自定义的,业务不同也无法炮制。流程就是在这里获取到token信息,通过token获取用户的信息,根据用户信息获取用户角色信息,根据角色获取到该角色的权限,然后将权限和角色添加到鉴权对象中,由shiro去完成权限认证。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String token = principalCollection.toString();
logger.info("权限认证:"+token);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
long userId = JwtUtil.getUserId(token);
Set<String> roles = mUserService.getRoles(userId);
Set<String> permissions = mUserService.getPermissions(userId);
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
如果权限认证失败,shiro会抛出AuthorizationException异常,这个异常需要在全局异常捕获器中进行捕获。如果权限认证成功,就会进入到controller。
- 全局异常捕获
package com.huafeng.cams.common.exception;
import com.huafeng.cams.common.R;
import io.netty.handler.codec.PrematureChannelClosureException;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* Author JinXing _让世界看到我
* On 2020/4/23
* Note 要采用统一返回格式,所以要处理全局的异常,统一返回.利用RestControllerAdvice实现
*/
@RestControllerAdvice
public class ExceptionController {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
*
* token认证失败的异常
* @return
*/
@ExceptionHandler(AuthenticationException.class)
public R handleAuthenticationException(Exception e){
logger.warn("身份认证失败,请重新登录!"+e.getMessage());
return R.RESULT_ERROR("身份认证失败,请重新登录!"+e.getMessage());
}
/**
*
* 权限认证失败的异常
* @return
*/
@ExceptionHandler(AuthorizationException.class)
public R handleAuthorizationException(Exception e){
logger.warn("权限认证失败!"+e.getMessage());
return R.RESULT_ERROR("你没有权限进行此操作!"+e.getMessage());
}
/**
* 捕获其它异常
* @param request
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public R globalException(HttpServletRequest request,Exception e){
e.printStackTrace();
logger.warn("操作失败!"+getStatus(request)+":"+e.getMessage());
return R.RESULT_ERROR("操作失败!"+getStatus(request)+"-->"+e.getMessage());
}
/**
* 获取request的状态码
* @param request
* @return
*/
private HttpStatus getStatus(HttpServletRequest request){
Integer code = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (code==null){
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(code);
}
}
当权限认证失败时shiro会抛出异常, 在全局异常捕获中使用@RestControllerAdvice+@ExceptionHandler(AuthorizationException.class)可以捕获这个异常,在这里对客户端进行统一返回,当然这个全局异常捕获不仅仅捕获shiro的异常,其它的异常可以在这里捕获,采用统一的数据格式返回客户端。
- token刷新
在使用token进行用户身份认证时,用户登录成功后会给用户签发一个token,并且每次访问资源都要携带这个token。而在签发token时,会给token标记一个过期时间,表明这个token可以用,但是不能一直用,当token过期后就没有用了,所以在用户使用时要对这个token进行刷新,及时的给用户一个可以用的新token。
笔者使用的机制是当用户在访问资源时会对其身份进行认证,如果认证成功就给用户重新签发一个token,放在response中返回给客户端。客户端在每次访问完成后在response中获取到token替换掉之前的token,然后下次访问时就用这次新签发的token。这样算token过期时间就不是从用户登录成功第一次签发的时候算,而是从此次完成访问的时候开始算。这个其实在上文中贴出filter代码块中就有所体现:
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
logger.info("JwtFilter.onAccessDenied");
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
String result = new Gson().toJson(R.RESULT_ERROR("请先登录"));
response(response,result);
return false;
}
boolean login = executeLogin(request, response);
if (login) {
refreshToken(request, response);
}
return login;
}
/**
* 身份信息认证成功后刷新token给客户端
* 自定义的token刷新策略
*
* @param request
* @param response
*/
private void refreshToken(ServletRequest request, ServletResponse response) throws MyException {
AuthenticationToken authenticationToken = createToken(request, response);
String token = (String) authenticationToken.getCredentials();
String newToken = JwtUtil.refreshToken(token);
if (!StringUtils.isEmpty(newToken)){
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader(JwtUtil.TOKEN_TAG,newToken);
}
}
在身份认证成功后重新签发一个token放在response中返回给客户端。