Spring和SpringSecurity 的全局异常处理

spring异常中,controller层传出的异常可以由 @ExceptionHandler 处理,比如处理sql异常?

@Slf4j
@RestControllerAdvice
public class SqlExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public ResponseEntity<Object> onConstraintValidationException(SQLIntegrityConstraintViolationException e) {
        log.error(e.getMessage(),e.getSQLState());
        return ResponseEntity.ok().body("重复主键");
    }
}

还有一种异常是spring security 过滤链的异常,过滤连的异常由上级过滤链中包裹 doFilter 的try catch 块捕捉,比如 ExceptionTranslationFilter 就捕捉更下位的 FilterSecurityInterceptor 抛出的AuthenticationException 来进行我们常见的方法权限管理.

假如我们有一个自定义过滤器,其中抛出了异常没有被默认过滤链捕捉,这时,@ExceptionHandler 是不能捕捉到过滤链中的异常的,如果我们想要捕捉

堆栈溢出-如何管理 Spring 过滤器中抛出的异常? 

方法① 自定义一个最高优先级的过滤器,用于捕捉你想捕捉的异常,并处理它

public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (RuntimeException e) {

            // custom error response class used across my project
            ErrorResponse errorResponse = new ErrorResponse(e);

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.getWriter().write(convertObjectToJson(errorResponse));
    }
}

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}

然后注册它

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .anyRequest().authenticated()
                )
                .rememberMe()
                .cors().and()
                .formLogin(login->
                        login
                                .loginPage("/login")
                        .permitAll()
                )
                .addFilterBefore(new ExceptionHandlerFilter(), CorsFilter.class)
                .build();
    }

方法② 像 CsrfFilter 一样, 在构造时创建自己异常的处理程序 ,如在CsrfFilter源码中就是这么处理的,然后交由 AccessDeniedHandlerImpl 返回错误消息和重定向

AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken): new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);

或者你想重用 @ExceptionHandler的全局异常处理,把方法①注册的过滤器改造一下

@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error("Spring Security Filter Chain Exception:", e);
            resolver.resolveException(request, response, null, e);
        }
    }
}

又或者,你还想把spring security 中默认抛出的那些已经被捕获了的异常也囊括到自己的全局异常处理器中

@Controller
public class ErrorControllerImpl implements ErrorController {
  @RequestMapping("/error")
  public void handleError(HttpServletRequest request) throws Throwable {
    if (request.getAttribute("javax.servlet.error.exception") != null) {
      throw (Throwable) request.getAttribute("javax.servlet.error.exception");
    }
  }
}

还比如常见的登录中用于验证用户名密码的 DaoAuthenticationProvider 使用父类AbstractUserDetailsAuthenticationProvider 的 authenticate 方法检查用户名密码,抛出 BadCredentialsException 会由 ProviderManager 捕捉,交由 AuthenticationFailureHandler (默认实现是 SimpleUrlAuthenticationFailureHandler )处理 ,比如在 SimpleUrlAuthenticationFailureHandler 源码中?加工了错误信息到 request 的附加属性和session 里面

/**
*缓存AuthenticationException以在视图呈现中使用。
*如果forwardToDestination设置为 true,则将使用请求范围,否则将尝试在会话中存储异常。 如果没有会话*并且allowSessionCreation为true则将创建一个会话。 否则将不会存储异常
*/
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
		if (this.forwardToDestination) {
			request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
			return;
		}
		HttpSession session = request.getSession(false);
		if (session != null || this.allowSessionCreation) {
			request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
		}
	}

进而我们可以在页面中使用模板引擎 freemarker 或者 thymeleaf 读取并显示这段错误信息(freemarker中要先设置允许freemarker读取session)

spring.freemarker.expose-session-attributes=true
<!DOCTYPE html>
<html>
<head>
<#import "/spring.ftl" as spring />
</head>
<body>
            <#if (Session.SPRING_SECURITY_LAST_EXCEPTION.message)?? && Session.SPRING_SECURITY_LAST_EXCEPTION.message?has_content>
                <div style="text-align: center;margin:0 auto;">
                    <span style="color: red;position:relative;top:-18px">${Session.SPRING_SECURITY_LAST_EXCEPTION.message}</span>
                </div>
            </#if>
</body>
</html>

页面的显示效果如下?

 如果我们有一个自定义的过滤器,用于实现判断是否需要验证码以及验证码是否正确,我们就可以将 AuthenticationFailureHandler 注册为bean

    @Bean
    public AuthenticationFailureHandler simpleUrlAuthenticationFailureHandler(){
        return new SimpleUrlAuthenticationFailureHandler("/login"+"?error");
    }

 然后在我们的验证码过滤器中使用该 AuthenticationFailureHandler 

@Override
protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
	String value = request.getParameter(验证码参数的key);
	if (value == null) {
		this.authenticationFailureHandler.onAuthenticationFailure(request, response, new 验证码不存在Exception("验证码不存在"));
		return;
	}
	if (校验验证码成功(value)) {
		filterChain.doFilter(request, response);
	} else {
		this.authenticationFailureHandler.onAuthenticationFailure(request, response, new 验证码错误Exception("验证码错误"));
	}
}

验证码抛出的异常信息就能和登录的用户名密码校验失败抛出的错误信息保持一致

 


版权声明:本文为muyuanjin67原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。