问题引导
目前正在使用SpringBoot+Shiro+JWT进行系统的认证与鉴权开发,今天在开发的过程遇到了一个问题,特此记录。
众所周知,SpringBoot提供了一套强大的异常处理工具,我们习惯使用的组合是@RestControllerAdvise
+@ExceptionHandler
的方式对业务层抛出的异常进行统一处理。但是这个的适用前提是用户的请求能到达控制层,也就是需要保证这个请求不会在到达控制层之前被Filter
以及Interceptor
过滤或者拦截。
问题就在这里,Shiro或者说不只是Shiro,很多权限框架都是通过Filter
以及Interceptor
进行先进行权限认证后再允许用户访问对应的控制层接口,也就是说,对于Shiro来说实际上登录这个操作是在Filter进行的,那么在Filter中抛出的异常,是肯定到不了控制蹭的,这就是问题所在。
异常抛出
没什么好说的,JsonResultException作为总的异常,其余内部业务继承这个总的,之后只需要在控制层捕获这个异常就ok了。
这一部分主要是用于后期代码理解
下面是Realm中的doGetAuthenticationInfo
方法中的自定义异常抛出
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// .....省略1000行
try{
JwtUtil.getInstance().verifyToken(credentials, password);
}catch(TokenExpiredException e){
log.error("token已经过期[" + credentials + "]");
throw new TokenException(JwtResultCode.TOKEN_EXPIRED);
}catch (SignatureVerificationException e){
log.error("token签证失效[" + credentials + "]");
throw new AuthException(AuthResultCode.AUTH_ERROR);
}catch(JWTVerificationException e){
log.error("token无效[" + credentials + "]");
throw new TokenException(JwtResultCode.TOKEN_INVALID);
}
// ....再省略1000行
}
解决方案
先给出解决方案,具体原因在后面
Filter中的改动
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 前置省略1000行
try {
// 尝试登录
// login方法最终交由Realm中doGetAuthenticationInfo进行认证
subject.login(token);
}catch(AuthenticationException e){
// 这个cause有可能就是你的异常
// 所以如果是你的异常,那么就需要进行自行的处理
Throwable cause = e.getCause();
if(Objects.nonNull(cause) && cause instanceof JsonResultException) {
JsonResultException ex = (JsonResultException) cause;
this.handleFailResponse(response, ex);
}
return false;
}
// 未抛出异常就返回true
return true;
}
/**
* 处理此Filter中的禁止通行的情况
*
* @param response ServletResponse
* @param result 报错
* @throws IOException 由于使用了writer可能会出现IO流报错
*/
private void handleFailResponse(ServletResponse response, RuntimeException result){
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setStatus(200);
httpServletResponse.setContentType("application/json");
httpServletResponse.setCharacterEncoding("UTF-8");
try {
httpServletResponse.getWriter().write(JSON.toJSONString(JsonResultUtil.getInstance().fail(result)));
httpServletResponse.getWriter().flush();
}catch(IOException e){
log.error(e.getMessage());
}
}
解决思路
笔者完成Shiro配置的时候自定义的Filter是继承的BasicHttpAuthenticationFilter
(懂的都懂,这个继承哪个只要满足自己需求就行 ),实现了executeLogin
方法,里面执行了subject.login()
方法,这个方法内部调用了。那么既然我调用了subject.login()
,在外层包try-catch
捕获抛出的自定义异常在进行异常处理设置response返回给前端不就好了?于是就有了第一版解决方式
try {
// 尝试登录
// login方法最终交由Realm中doGetAuthenticationInfo进行认证
subject.login(token);
}catch(JsonResultException e){
this.handleFailResponse(response, e);
return false;
}
结果如下
笑死,这返回了个啥,这边解析的Json数据,如果返回了字符串是不会展示,不管怎么样肯定不是我期待的Json数据格式。于是开始debug,debug路径DelegatingSubject->DefaultSecurityManager->AuthenticatingSecurityManager->AbstractAuthenticator
,最终AbstractAuthenticator
是处理异常的类,并且发现了一个好玩的东西
也就是最终在Filter中只能捕获到AuthenticationException,想要获取到自定义的异常,需要结构这个AuthenticationException,判断是否为自定义异常如果是那么进行你想要的处理,最终的版本就是上面的解决方案了。
修改之后,重新调用了一下接口
完美收官
结语
之后这个项目会开源,但是具体开源日期受笔者的开发进度影响比较大,随意点哈,暂时不给源码,因为写的太烂了。但是如果各位有更加占优的方式,欢迎在评论区评论