你知道Spring Security只处理哪些异常么?

234 阅读3分钟

前言

Spring Security只拦截两种异常, 一种是认证异常AuthenticationException, 另一种是权限异常AccessDeniedException, 其他异常交给 Spring 解决

预告下, 下一个章节是重点, 等我更新

Spring Security异常体系

  • AuthenticationException

  • AccessDeniedException

  • 认证异常

异常异常介绍
AuthenticationException认证异常的父类,抽象类
BadCredentialsException登录凭证(密码)异常
InsufficientAuthenticationException登录凭证不够充分而抛出的异常
SessionAuthenticationException会话并发管理时抛出的异常,例如会话总数超出最大限制数
UsernameNotFoundException用户名不存在异常
PreAuthenticatedCredentialsNotFoundException身份预认证失败异常
ProviderNotFoundException未配置AuthenticationProvider 异常
AuthenticationServiceException由于系统问题而无法处理认证请求异常。
IntemnalAuthenticationServiceException由于系统问题而无法处理认证请求异常。和AuthenticationServiceException不同之处在于,如果外部系统出错,则不会抛出该异常
AuthenticationCredentialsNotFoundExceptionSecurityContext中不存在认证主体时抛出的异常
NonceExpiredExceptionHTTP摘要认证时随机数过期异常
RememberMeAuthenticationExceptionRememberMe 认证异常
CookieThefExceptionRememberMe 认证时Cookie被盗窃异常
InvalidCookieExceptionRememberMe 认证时无效的Cookie异常
AccountStatusException账户状态异常
LockedException账户被锁定异常
DisabledException账户被禁用异常
CredentialsExpiredException登录凭证(密码)过期异常
AccountExpiredException账户过期异常
  • 权限异常
异常异常介绍
AccessDeniedException权限异常的父类
AuthorizationServiceException由于系统问题而无法处理权限时抛出异常
CsrfExceptionCsrf令牌异常
MissingCsrfTokenExceptionCsrf令牌缺失异常
InvalidCsrfTokenExceptionCsrf令牌无效异常

源码分析

ExceptionTranslationFilter源码分析

image-20221130162330488

image-20221130162531811

首先, 如果你看过Spring Security过滤器的执行顺序, 你就会发现ExceptionTranslationWebFilter对象是倒二执行的, 所以会捕获异常

我们去找ExceptionTranslationWebFilter过滤器的doFilter方法:

这就就不分析ExceptionHandlingConfigurer类的

@Override
public void configure(H http) {
    AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
    ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
          getRequestCache(http));
    AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
    exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
    exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
    http.addFilter(exceptionTranslationFilter);
}

方法了, 主要就是往ExceptionTranslationFilter配置了一些对象

核心代码:

Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
      .getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
   securityException = (AccessDeniedException) this.throwableAnalyzer
         .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
   rethrow(ex);
}
if (response.isCommitted()) {
   throw new ServletException("Unable to handle the Spring Security Exception "
         + "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);

这段代码分为两个部分

  1. 判断是认证异常还是权限异常?
  2. 最后执行处理异常的过程handleSpringSecurityException
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
      FilterChain chain, RuntimeException exception) throws IOException, ServletException {
   if (exception instanceof AuthenticationException) {
      handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
   }
   else if (exception instanceof AccessDeniedException) {
      handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
   }
}

在上面代码判断是认证异常还是权限异常?

根据不同的异常处理

如果是认证异常则执行这段代码:

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
      AuthenticationException reason) throws ServletException, IOException {
   SecurityContext context = SecurityContextHolder.createEmptyContext();
   SecurityContextHolder.setContext(context);
   this.requestCache.saveRequest(request, response);
   this.authenticationEntryPoint.commence(request, response, reason);
}
  1. 清空SecurityContextHolder的内容
  2. 保存当前请求
  3. 调用authenticationEntryPoint.commence(request, response, reason)方法完成认证失败处理

而上面的authenticationEntryPoint对象, 只有根据下面的方法才能判断它是什么类型?

private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
   if (this.defaultEntryPointMappings.isEmpty()) {
      return new Http403ForbiddenEntryPoint();
   }
   if (this.defaultEntryPointMappings.size() == 1) {
      return this.defaultEntryPointMappings.values().iterator().next();
   }
   DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
         this.defaultEntryPointMappings);
   entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
   return entryPoint;
}

image-20221130171017655

后续就不再分析了, 每个都分析源码太累了...

自定义异常配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
   AntPathRequestMatcher preferredMatcher = new AntPathRequestMatcher("/xxxx", "POST");
   return http
         .authorizeRequests()
         .antMatchers("/admin").hasRole("admin")
         .anyRequest().authenticated()
         .and()
         .formLogin()
         .and()
         .exceptionHandling()
         .authenticationEntryPoint((request, response, authException) -> {
            authException.printStackTrace();
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("请登录");
         })
         .accessDeniedHandler((request, response, accessDeniedException) -> {
            accessDeniedException.printStackTrace();
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.getWriter().write("forbidden");
         })
         .defaultAuthenticationEntryPointFor((request, response, authException) -> {
            authException.printStackTrace();
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("preferredMatcher: 请登录");
         }, preferredMatcher)
         .and().csrf().disable()
         .build();
}

这里注意了,上面的认证是使用的json。而不是url。如果你使用的是URL的话,那么就要从定向到 login地址了。

上面的代码, 可能会出现中文乱码, 你知道出现了什么问题了么? 嘿嘿, 帮我改, 我懒

这样我们在遇到权限异常时前端打印: forbidden, 遇到认证异常时前端打印: 请登录

还可以借助defaultAuthenticationEntryPointFordefaultAccessDeniedHandlerFor方法实现以 url, 请求 MediaType等 为单位的异常配置

请求头为单位的请求:

image-20221130173644246

image-20221130174011564