ToB企服应用市场:ToB评测及商务社交产业平台

标题: Spring Security之安全异常处理 [打印本页]

作者: 南七星之家    时间: 2024-8-9 15:46
标题: Spring Security之安全异常处理
前言

在我们的安全框架中,不管是什么框架(包括通过过滤器自定义)都须要处理涉及安全相关的异常,比方:登录失败要跳转到登录页,访问权限不足要返回页面亦或是json。接下来,我们就看看Spring Security是怎么处理异常的!
什么是异常处理

在Spring Security中,特指对于安全异常的处理。
我们知道Spring Security主要是基于过滤器来实现的,因此每个安全过滤器都大概发生安全异常,以是处理逻辑会被散落在各个过滤器中。
Spring自然是不能忍受这种设计,于是就有了专门的安全异常处理。
注:下文我们都用异常处理来代指安全异常处理。
异常处理设计

Spring Security将安全异常分为两类。

认证异常和访问拒绝异常的区别

与访问拒绝异常相比,认证异常要复杂不少。这是由认证过程和认证方式的多样性导致的。

异常处理器

异常类定义异常处理器认证异常AuthenticationExceptionAuthenticationFailureHandler访问异常AccessDeniedExceptionAccessDeniedHandler 为什么要搞两个异常,还要搞两个组件来处理呢?
实际上,这两个组件定义的可以说一模一样:
  1. public interface AuthenticationFailureHandler {
  2.         void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
  3.                         throws IOException, ServletException;
  4. }
  5. public interface AccessDeniedHandler {
  6.         void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
  7.                         throws IOException, ServletException;
  8. }
复制代码
除了方法名,和入参的异常不同,其他的都是一样的。乃至,如果我们进一步看看异常的定义的话,连异常定义也是类似的,都是继承于RuntimeException,没有任何其他多余的字段和逻辑。
AuthenticationFailureHandler的实现

AuthenticationFailureHandler描述AuthenticationEntryPointFailureHandler通过AuthenticationEntryPoint组件处理SimpleUrlAuthenticationFailureHandler重定向到指定URL,如果没有指定,则退化返回401ForwardAuthenticationFailureHandler重定向到指定的URL,必须指定URLExceptionMappingAuthenticationFailureHandler通过匹配异常寻找对应的处理器,一般由用户自行配置。DelegatingAuthenticationFailureHandler委托其他的处理器处理 这里有一个特殊的,他利用另一个组件AuthenticationEntryPoint进行处理。
AuthenticationEntryPoint


AccessDeniedHandler的实现


到这里问一句,这里我们看到了几种设计模式?策略模式、委托模式、组合模式。可以看到Spring对于代码的追求,这也是我们阅读源码的目标之一,学习好的设计。而这背后都是设计原则。
异常处理原理

前面我们大概相识了异常处理的来龙去脉,知道了其焦点组件。现在我们来深入相识其原理。
认证异常处理原理

要明白这个,就必须回顾一下认证流程(这里以默认的用户暗码登录为例):
  1. AbstractAuthenticationProcessingFilter#doFilter
  2. > UsernamePasswordAuthenticationFilter#attemptAuthentication
  3. |-> ProviderManager#authenticate
  4. |-|-> AbstractUserDetailsAuthenticationProvider#authenticate
  5. |-|-|->DaoAuthenticationProvider#retrieveUser
  6. |-|-|-|->JdbcDaoImpl#loadUserByUsername
  7. |-|-|->DaoAuthenticationProvider#additionalAuthenticationChecks
  8. |-|-|->DaoAuthenticationProvider#createSuccessAuthentication
  9. |-|-|->AbstractUserDetailsAuthenticationProvider#createSuccessAuthentication
  10. // 同层级的表示顺序调用,不同层级的:上层方法调用下层方法,是递进关系。层级减少表示方法返回
复制代码
负责处理认证哀求的AbstractAuthenticationProcessingFilter#doFilter方法中会捕捉异常,并交给unsuccessfulAuthentication方法处理。
  1. public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
  2.                 implements ApplicationEventPublisherAware, MessageSourceAware {
  3.        
  4.         protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
  5.                         AuthenticationException failed) throws IOException, ServletException {
  6.                 // 1. 清空安全上下文
  7.                 this.securityContextHolderStrategy.clearContext();
  8.                 // 2. 记住我功能,清空cookie,认证失败的处理
  9.                 this.rememberMeServices.loginFail(request, response);
  10.                 // 3. 通过认证失败处理处理
  11.                 this.failureHandler.onAuthenticationFailure(request, response, failed);
  12.         }
  13. }
复制代码
默认情况下,会利用SimpleUrlAuthenticationFailureHandler重定向到登录页面。
什么?怎么知道是这个处理器?行吧,我们来看看FormLoginConfigurer的源码吧。
  1. public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
  2.                 AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
  3.        
  4.         public FormLoginConfigurer() {
  5.                 // 调用父类构造器
  6.                 super(new UsernamePasswordAuthenticationFilter(), null);
  7.                 usernameParameter("username");
  8.                 passwordParameter("password");
  9.         }
  10.        
  11.         /**
  12.          * 在配置过目标过滤器之前,会先调用这个方法进行Configurer的初始化
  13.          */
  14.         @Override
  15.         public void init(H http) throws Exception {
  16.                 // 初始化父类
  17.                 super.init(http);
  18.                 // 初始化默认的登录页面
  19.                 initDefaultLoginFilter(http);
  20.         }
  21. }
  22. public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>
  23.                 extends AbstractHttpConfigurer<T, B> {
  24.        
  25.         protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
  26.                 // 调用另一个构造器
  27.                 this();
  28.                 this.authFilter = authenticationFilter;
  29.                 if (defaultLoginProcessingUrl != null) {
  30.                         // 由于FormLoginConfigurer的构造器中传的是null,因此不会走到这
  31.                         // 当然,由于这个方法是public,因此也可以在配置时被我们调用
  32.                         // 他无非就是指定什么地址是认证请求罢了
  33.                         loginProcessingUrl(defaultLoginProcessingUrl);
  34.                 }
  35.         }
  36.         protected AbstractAuthenticationFilterConfigurer() {
  37.                 // 构造器中设置登录页面uri
  38.                 setLoginPage("/login");
  39.         }
  40.         private void setLoginPage(String loginPage) {
  41.                 this.loginPage = loginPage;
  42.                 // 指定AuthenticationEntryPoint,后面异常处理过滤器用的是这个来处理没有登录的异常。
  43.                 this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
  44.         }
  45.        
  46.         @Override
  47.         public void init(B http) throws Exception {
  48.                 // 更新/初始化认证相关的默认组件
  49.                 updateAuthenticationDefaults();
  50.                 // 更新访问权限-认证页面、认证请求、认证失败
  51.                 updateAccessDefaults(http);
  52.                 // 注册默认的AuthenticationEntryPoint
  53.                 registerDefaultAuthenticationEntryPoint(http);
  54.         }
  55.         protected final void updateAuthenticationDefaults() {
  56.                 if (this.loginProcessingUrl == null) {
  57.                         loginProcessingUrl(this.loginPage);
  58.                 }
  59.                 if (this.failureHandler == null) {
  60.                         // 指定默认的异常跳转页面
  61.                         failureUrl(this.loginPage + "?error");
  62.                 }
  63.                 LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);
  64.                 if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
  65.                         logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
  66.                 }
  67.         }
  68.         public final T failureUrl(String authenticationFailureUrl) {
  69.                 // 就是这个啦
  70.                 T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
  71.                 this.failureUrl = authenticationFailureUrl;
  72.                 return result;
  73.         }
  74.         protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {
  75.                 // 所谓注册就是注册到异常处理过滤器上
  76.                 // 思考个问题:下面这种处理方式不就耦合ExceptionHandlingConfigurer了吗?
  77.                 // 为什么不是像其他的sharedObject那样,直接放到HttpSecurityd#sharedObjects中,在ExceptionHandlingConfigurer再自行获取设置。
  78.                 // 答:如果这样的话,会导致BUG。init方法是在执行了用户配置方法之后在HttpSecurity构建过滤器链的时候调用的。有可能将用户配置的覆盖了。
  79.                 ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
  80.                 if (exceptionHandling == null) {
  81.                         return;
  82.                 }
  83.                 exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
  84.                                 getAuthenticationEntryPointMatcher(http));
  85.         }
  86. }
复制代码
从FormLoginConfigurer出发,我们知道UsernamePasswordAuthenticationFilter利用的是SimpleUrlAuthenticationFailureHandler,同时ExceptionTranslationFilter利用的是LoginUrlAuthenticationEntryPoint。但这个设计我没有很明白,个人觉得应该在顶层都利用AuthenticationFailureHandler才合理。不知道是不是为了区分场景。

但SimpleUrlAuthenticationFailureHandler、LoginUrlAuthenticationEntryPoint,内部处理没有太大区别,都是为了跳转到登录页面。
访问拒绝异常处理原理

鉴权相关的,之前我们聊过,忘记的同学可以通过下面的链接再回想复习一下。大概文章的标题大概说的权限配置,但同时也从原理上给各人分析了如何鉴权的。也正是因为有两种方式,以是没有单独写鉴权过滤器。因为基于HttpRequest的配置方式的鉴权原理是通过AuthorizationFilter,也就是过滤器实现的。而另一种权限配置方式-基于方法配置权限-则是通过AOP实现的。

ExceptionTranslationFilter

他主要负责处理的是鉴权过程中发生的异常。这里就包括用户权限不足的AccessDeniedException,以及鉴权时发现用户还没有登录而抛出的认证异常。
  1. public class AuthorizationFilter extends GenericFilterBean {
  2.         private Authentication getAuthentication() {
  3.                 Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
  4.                 if (authentication == null) {
  5.                         throw new AuthenticationCredentialsNotFoundException(
  6.                                         "An Authentication object was not found in the SecurityContext");
  7.                 }
  8.                 return authentication;
  9.         }
  10. }
复制代码
  1. public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
  2.         private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
  3.                         throws IOException, ServletException {
  4.                 try {
  5.                         chain.doFilter(request, response);
  6.                 }
  7.                 catch (IOException ex) {
  8.                         throw ex;
  9.                 }
  10.                 catch (Exception ex) {
  11.                         // 尝试从异常堆栈中找到安全异常
  12.                         Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
  13.                         RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
  14.                                 .getFirstThrowableOfType(AuthenticationException.class, causeChain);
  15.                         if (securityException == null) {
  16.                                 securityException = (AccessDeniedException) this.throwableAnalyzer
  17.                                         .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
  18.                         }
  19.                         if (securityException == null) {
  20.                                 // 不是安全异常,直接重新抛出
  21.                                 rethrow(ex);
  22.                         }
  23.                         if (response.isCommitted()) {
  24.                                 // 如果response已经提交,则抛出servlet异常
  25.                                 throw new ServletException("Unable to handle the Spring Security Exception "
  26.                                                 + "because the response is already committed.", ex);
  27.                         }
  28.                         // 处理安全异常
  29.                         handleSpringSecurityException(request, response, chain, securityException);
  30.                 }
  31.         }
  32.        
  33.         private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
  34.                         FilterChain chain, RuntimeException exception) throws IOException, ServletException {
  35.                 if (exception instanceof AuthenticationException) {
  36.                         // 处理认证异常
  37.                         handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
  38.                 }
  39.                 else if (exception instanceof AccessDeniedException) {
  40.                         // 处理访问异常
  41.                         handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
  42.                 }
  43.         }
  44.         private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
  45.                         FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
  46.                 // 这里是转换方法名,是一种代码追求、也是一种代码的自解释:对于上层方法的作用是处理认证异常,而处理认证异常的手段是发送开始认证(其实就是跳转到登录页面开始登录流程),因为走到这的都是鉴权时不存在凭证导致的认证异常。
  47.                 sendStartAuthentication(request, response, chain, exception);
  48.         }
  49.         private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
  50.                         FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
  51.                 Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
  52.                 boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
  53.                 if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
  54.                         // 对于匿名用户或者不是记住我用户,直接跳转登录页开始登录流程
  55.                         sendStartAuthentication(request, response, chain,
  56.                                         new InsufficientAuthenticationException(
  57.                                                         this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
  58.                                                                         "Full authentication is required to access this resource")));
  59.                 }
  60.                 else {
  61.                         // 正常用户则通过AccessDeniedHandler处理
  62.                         this.accessDeniedHandler.handle(request, response, exception);
  63.                 }
  64.         }
  65.         protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
  66.                         AuthenticationException reason) throws ServletException, IOException {
  67.                 // SEC-112: Clear the SecurityContextHolder's Authentication, as the
  68.                 // existing Authentication is no longer considered valid
  69.                 SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
  70.                 // 清空安全上下文               
  71.                 this.securityContextHolderStrategy.setContext(context);
  72.                 // 记录当前权限不足的请求,登录成功后可能需要自动跳转
  73.                 this.requestCache.saveRequest(request, response);
  74.                 // 通过AuthenticationEntryPoint处理,这里是跳转到登录页面
  75.                 this.authenticationEntryPoint.commence(request, response, reason);
  76.         }
  77. }
复制代码
总结

后记

前阵子搬家,一直在适应新屋子的生活节奏,拖了不少时间,对不住了各位。后面应该会回复正常。
至此,咱们聊了认证、鉴权、session、异常处理,接下来咱们聊聊认证过程中一些小的功能点,比方:登录后跳转到之前异常的哀求、RemenberMe、多处登录控制。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4