Bootstrap

shiro学习——整合jwt时全局异常处理器无法处理JWTFilter中抛出的异常

一、shiro中会出现的异常

1、 AuthencationException

AuthenticationException 异常是Shiro在登录认证过程中,认证失败需要抛出的异常。

AuthenticationException包含以下子类:

  • CredentitalsException 凭证异常
    IncorrectCredentialsException 不正确的凭证
    ExpiredCredentialsException 凭证过期
  • AccountException 账号异常
    ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
    UnknownAccountException 未知的账号
    ExcessiveAttemptsException 认证次数超过限制
    DisabledAccountException 禁用的账号
    LockedAccountException 账号被锁定
  • UnsupportedTokenException 使用了不支持的Token

2、 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 中继续判断,死循环。

;