一、问题场景
- 在SpringBoot程序中,使用Shiro框架进行权限校验,使用Filter进行判断,当不符合条件时进行抛出异常,拦截用户的请求
- Filter中的抛出的Shiro异常无法被异常拦截器所拦截
二、Shiro中会出现的异常
(一)AuthencationException
AuthenticationException 异常是Shiro在登录认证过程中,认证失败需要抛出的异常。
AuthenticationException包含以下子类:
- CredentitalsException 凭证异常
IncorrectCredentialsException 不正确的凭证
ExpiredCredentialsException 凭证过期 - AccountException 账号异常
ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
UnknownAccountException 未知的账号
ExcessiveAttemptsException 认证次数超过限制
DisabledAccountException 禁用的账号
LockedAccountException 账号被锁定 - UnsupportedTokenException 使用了不支持的Token
(二)AuthorizationException
AuthorizationException异常是Shiro在登录授权过程中或授权后可能出现的异常。
AuthorizationException包含以下子类
- UnauthorizedException 请求的操作或对请求的资源的访问是不允许的。
- UnanthenticatedException 当尚未完成成功认证时,尝试执行授权操作时引发异常。
二、为什么无法被全局异常处理器处理
以上是我们捕获异常的地方,是shiro整合jwt后的过滤器,继承了BasicHttpAuthenticationFilter
下面是它完成的继承链路
最终继承到了 servlet 的 Filter 类, Filter 处理是在控制器之前的, 所以由 @ControllerAdvice注解的全局异常处理器无法处理这里的异常(@ControllerAdvice是由spring 提供的增强控制器) 。
三、解决方案
1、把统一返回的信息写入response中
@Log4j2
public class JWTFilter extends BasicHttpAuthenticationFilter {
// 登录标识
private static String LOGIN_SIGN = "Authorization";
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
//进行Shiro的登录UserRealm
try {
return executeLogin(request, response);
} catch (IOException e) {
log.error("执行response.getWriter()方法异常");
}
}
this.sendChallenge(request, response);
return false;
}
/**
* 检测用户是否登录
* 检测header里面是否包含Authorization字段即可
*
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(LOGIN_SIGN);
return authorization != null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
try {
getSubject(request, response).login(jwtToken);
} catch (AuthenticationException e) {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(BaseResponseUtil.error(CodeEnum.NOT_SUPPORT,e.getMessage())));
return false;
}
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* isAccessAllowed()返回false便会执行这个方法,
* @param request
* @param response
* @return 返回false,则过滤器的流程结束且不会执行访问controller的方法
* @throws Exception
*/
@Override
public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
return false;
}
}
此处需要注意的是过滤器中方法执行的流程:isAccessAllowed -> isLoginAttempt -> executeLogin
如果isAccessAllowed 的返回值是false的话便会执行onAccessDenied方法,这里如果不重写的话就会执行父类的方法
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
boolean loggedIn = false;
if (this.isLoginAttempt(request, response)) {
loggedIn = this.executeLogin(request, response);
}
if (!loggedIn) {
this.sendChallenge(request, response);
}
return loggedIn;
}
父类的方法会重新执行executeLogin方法,然后导致再次抛出异常被捕获到,返回头里面的信息就会重复,且返回的状态码为401,不会是200,这是因为如果不是登录方法的话会执行this.sendChallenge(request, response);返回状态码为401。
onAccessDenied方法返回值为false表示过滤器的工作结束。
2、使用重定向到处理该错误的controller中
在JWTFilter 新增responseError方法,修改executeLogin方法,在过滤器中捕获到异常时,httpServletResponse.sendRedirect 使用重定向到一个我们自定义的处理过滤器错误的一个controller中,返回自定义格式的对象到前端。
@Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Authorization"); JwtToken jwtToken = new JwtToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 try { getSubject(request, response).login(jwtToken); } catch (AuthenticationException e) { responseError(response,401,e.getMessage()) return false; } // 如果没有抛出异常则代表登入成功,返回true return true; } /** * 将非法请求跳转到 /filterError/**中 */ private void responseError(ServletResponse response, int code,String message) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) response; //设置编码,否则中文字符在重定向时会变为空字符串 message = URLEncoder.encode(message, "UTF-8"); //如果有项目名称路径记得加上 httpServletResponse.sendRedirect("/filterError/" + code + "/" + message); } catch (IOException e1) { log.error(e1.getMessage()); } } }
@RestController public class FilterErrorController { @ResponseBody @RequestMapping("/filterError/{code}/{message}") public Map<String,Object> error(@PathVariable("code")Integer code, @PathVariable("message")String message){ Map<String,Object> map = new HashMap<>(); map.put("code",code); map.put("message",message); return map; } }
需要注意的是,在shiro的配置类中需要配置对重定向的路径访问无需授权,否侧重定向后会重新进入JWTFilter 中继续判断,死循环。