Spring Security自定义认证异常和授权异常

简介: Spring Security自定义认证异常和授权异常

一、Spring Security异常分类

Spring Security中的异常主要分为两大类

  • 认证异常(AuthenticationException),这个是所有认证异常的父类
  • 权限异常(AccessDeniedException),这个是所有权限异常的父类

除此之外系统中遇到的异常则抛出,交给spring去处理,比如全局异常处理。

认证异常(AuthenticationException)所有类

权限异常(AccessDeniedException)所有类

二、原理分析

该过滤器主要处理上面说的两类异常AuthenticationException和AccessDeniedException,其他异常则会继续抛出,交给上一层容器去处理。

WebSecurityConfigurerAdapter的getHttp方法中进行HttpSecurity初始化的时候,就调用了exceptionHandling()方法去配置ExceptionTranslationFilter过滤器:

protected final HttpSecurity getHttp() throws Exception {
    // ...
    if (!this.disableDefaults) {
        applyDefaultConfiguration(this.http);
        // ...
    }
    configure(this.http);
  return this.http;
}
 
private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
    http.csrf();
    http.addFilter(new WebAsyncManagerIntegrationFilter());
    http.exceptionHandling();
    http.headers();
    http.sessionManagement();
    http.securityContext();
    http.requestCache();
    http.anonymous();
    http.servletApi();
    http.apply(new DefaultLoginPageConfigurer<>());
    http.logout();
}

exceptionHandling()方法就是调用ExceptionHandlingConfigurer去配置,ExceptionTranslationFilter源码如下

@Override
public void configure(H http) {
    // 获取认证失败时的处理器
    AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
    // 创建ExceptionTranslationFilter过滤器并传入entryPoint
    ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
            getRequestCache(http));
    // 获取权限异常处理器
    AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
    exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
    // 注册到spring容器中
    exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
    // 添加到spring security过滤器链中
    http.addFilter(exceptionTranslationFilter);
}

AuthenticationEntryPoint实例是通过getAuthenticationEntryPoint方法获取到的:

AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
    // 默认情况下,系统的authenticationEntryPoint属性值为null
    AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
    if (entryPoint == null) {
        entryPoint = createDefaultEntryPoint(http);
    }
    return entryPoint;
}
 
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
    // defaultEntryPointMappings -> LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>,
    // 即一个请求匹配器对应一个认证失败处理器,针对不同的请求,可以给出不同的认证失败处理器
    if (this.defaultEntryPointMappings.isEmpty()) {
        return new Http403ForbiddenEntryPoint();
    }
    if (this.defaultEntryPointMappings.size() == 1) {
        return this.defaultEntryPointMappings.values().iterator().next();
    }
    // 如果defaultEntryPointMappings变量中有多项,则使用DelegatingAuthenticationEntryPoint代理类,在代理类中,
    // 会遍历defaultEntryPointMappings中的每一项,查看当前请求是否满足其RequestMatcher,如果满足,则使用对应的
    // 认证失败处理器来处理
    DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
            this.defaultEntryPointMappings);
    entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
    return entryPoint;
}

当新建一个spring security项目时,不做任何配置时,在WebSecurityConfigurerAdapter#configure(HttpSecurity)方法中默认会配置表单登录和HTTP基本认证,而在这个配置过程中,会分别向defaultEntryPointMappings中添加认证失败处理器:

表单登录在AbstractAuthenticationFilterConfigurer#registerAuthenticationEntryPoint方法中向defaultEntryPointMappings变量添加的处理器,对应的AuthenticationEntryPoint实例是LoginUrlAuthenticationEntryPoint,默认情况下访问需要认证才能访问的页面时,会自动跳转到登录页面,就是通过LoginUrlAuthenticationEntryPoint实现的。

HTTP基本认证在HttpBasicConfigurer#registerDefaultEntryPoint方法中向defaultEntryPointMappings变量添加处理器,对应的AuthenticationEntryPoint实例是BasicAuthenticationEntryPoint。

所以默认情况下,defaultEntryPointMappings变量中将存在两个认证失败处理器。

AccessDeniedHandler实例是通过getAccessDeniedHandler方法获取到的:

// 获取流程和AuthenticationEntryPoint基本上一模一样
AccessDeniedHandler getAccessDeniedHandler(H http) {
  AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
  if (deniedHandler == null) {
    deniedHandler = createDefaultDeniedHandler(http);
  }
  return deniedHandler;
}
 
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
  // 不同的是,默认情况下这里的defaultDeniedHandlerMappings变量是空的,所以最终获取到的实例是AccessDeniedHandlerImpl。
  // 在AccessDeniedHandlerImpl#handle方法中处理鉴权失败的情况,如果存在错误页面,就跳转到错误页面,并设置响应码为403;
  // 如果没有错误页面,则直接给出错误响应即可
  if (this.defaultDeniedHandlerMappings.isEmpty()) {
    return new AccessDeniedHandlerImpl();
  }
  if (this.defaultDeniedHandlerMappings.size() == 1) {
    return this.defaultDeniedHandlerMappings.values().iterator().next();
  }
  return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings,
      new AccessDeniedHandlerImpl());
}

AuthenticationEntryPointAccessDeniedHandler都有了之后,接下来就是ExceptionTranslationFilter中的处理逻辑了。

默认情况下,ExceptionTranslationFilter过滤器在整个spring security过滤器链中排名倒数第二,倒数第一是FilterSecurityInterceptor。在FilterSecurityInterceptor中将会对用户的身份进行校验,如果用户身份不合法,就会抛出异常,抛出来的异常,刚好就在ExceptionTranslationFilter过滤器中进行处理了:

 

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException, ServletException {
  // 用一个try/catch块将chain.doFilter包裹起来,如果后面有异常抛出,就直接在这里捕获到了
  try {
    // 直接执行了chain.doFilter方法,让当前请求继续执行剩下的过滤器(即FilterSecurityInterceptor)
    chain.doFilter(request, response);
  }
  catch (IOException ex) {
    throw ex;
  }
  catch (Exception ex) {
    // throwableAnalyzer对象是一个异常分析器,由于异常在抛出的过程中可能被层层转包,需要还原最初的异常,
    // 通过throwableAnalyzer.determineCauseChain方法可以获得整个异常链
    Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
    // 获取到异常链后调用getFirstThrowableOfType方法查看异常链中是否有认证失败类型的异常AuthenticationException,
    // 注意查找顺序,先找认证异常,再找鉴权异常,如果存在这两种类型的异常,则调用handleSpringSecurityException方法
    // 进行异常处理,否则将异常抛出交给上层容器去处理
    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);
  }
}
 
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
    FilterChain chain, RuntimeException exception) throws IOException, ServletException {
  // 判断异常类型是不是AuthenticationException,如果是,则进入handleAuthenticationException方法中处理认证失败
  if (exception instanceof AuthenticationException) {
    handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
  }
  // 如果异常类型是AccessDeniedException,则进入handleAccessDeniedException方法中处理
  else if (exception instanceof AccessDeniedException) {
    handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
  }
}
 
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
    FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
  sendStartAuthentication(request, response, chain, exception);
}
 
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
    FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
  // 从SecurityContextHolder中取出当前认证主体
  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  // 判断是否是匿名用户
  boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
  // 如果是匿名用户,或者当前认证是通过remember-me完成的,那么也认为是认证异常,需要重新创建一个
  // InsufficientAuthenticationException类型的异常对象,然后进入sendStartAuthentication
  // 方法进行处理,否则就认为是鉴权异常,调用accessDeniedHandler.handle方法进行处理
  if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
    sendStartAuthentication(request, response, chain,
        new InsufficientAuthenticationException(
            this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
                "Full authentication is required to access this resource")));
  }
  else {
    this.accessDeniedHandler.handle(request, response, exception);
  }
}
 
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
    AuthenticationException reason) throws ServletException, IOException {
  // 清除SecurityContextHolder的认证主体,因为现有的认证不再被认为是有效的
  SecurityContext context = SecurityContextHolder.createEmptyContext();
  SecurityContextHolder.setContext(context);
  // 保存当前请求
  this.requestCache.saveRequest(request, response);
  // 调用authenticationEntryPoint.commence方法完成认证失败处理
  this.authenticationEntryPoint.commence(request, response, reason);
}
// 至此,AuthenticationEntryPoint和AccessDeniedHandler在这里就都派上用场了

三、如何自定义异常处理

Spring security中默认提供的异常处理器不一定满足项目的需求,那这时一般都会在Spring Security 的 自定义配置类( WebSecurityConfigurerAdapter )中使用HttpSecurity 提供的 exceptionHandling() 方法用来提供异常处理。该方法构造出 ExceptionHandlingConfigurer 异常处理配置类。该配置类提供了两个实用接口:

AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常
AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常

我们只要实现并配置这两个异常处理类即可实现对 Spring Security 认证授权相关的异常进行统一的自定义处理。

 

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin()
                //开启异常处理
                .and().exceptionHandling()
                //处理认证异常
                .authenticationEntryPoint((request,response,e)->{
                    if(e instanceof LockedException){
                        //e是走的父类AuthenticationException,针对指定子类异常可以自定义一些逻辑
                    }
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("没有认证");
                })
                //处理授权异常
                .accessDeniedHandler((request,response,e)->{
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("授权异常");
                })
                .and().csrf().disable();
    }
}

当然,开发者也可以为不同的接口配置不同的异常处理器:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AntPathRequestMatcher matcher1 = new AntPathRequestMatcher("/qq/**");
        AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/wx/**");
 
        http.authorizeRequests()
                .antMatchers("/wx/**").hasRole("wx")
                .antMatchers("/qq/**").hasRole("qq")
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .defaultAuthenticationEntryPointFor((request, response, authException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("请登录, QQ用户");
                }, matcher1)
                .defaultAuthenticationEntryPointFor((request, response, accessDeniedException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("请登录, WX用户");
                }, matcher2)
                .defaultAccessDeniedHandlerFor((request, response, accessDeniedException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("权限不足, QQ用户");
                }, matcher1)
                .defaultAccessDeniedHandlerFor((request, response, accessDeniedException) -> {
                    response.setContentType("text/html;charset=utf-8");
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.getWriter().write("权限不足, WX用户");
                }, matcher2)
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

配置完成后,不同接口将会给出不同的异常响应。

注意

如果是springboot+security项目,并且定义了全局异常,那上面的授权异常AccessDeniedException及子类和认证异常AuthenticationException及子类都会有优先被全局异常,导致security配置的异常处理不起作用,主要原因是执行先后的问题,这里放一张 图

可以看到当发生授权异常AccessDeniedException及子类和认证异常AuthenticationException及子类都会有优先被全局异常ControllerAdvice处理,所以如果还是想让security自己处理,我们在全局异常中捕获到上述异常,直接抛出就行,代码如下


/**
 * 全局异常处理
 * spring+security项目中的异常
 * 1、security中的两大异常授权异常(AccessDeniedException)+认证异常(AuthenticationException)交由security自己处理
 * 2、其他异常自己处理
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    //security的授权异常(AccessDeniedException及子类)抛出交由security AuthenticationEntryPoint 处理
    @ExceptionHandler(AccessDeniedException.class)
    public void accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
        throw e;
    }
 
    //security的认证异常(AuthenticationException及子类)抛出由security AccessDeniedHandler 处理
    @ExceptionHandler(AuthenticationException.class)
    public void authenticationException(AuthenticationException e) throws AuthenticationException {
        throw e;
    }
 
    //未知异常
    @ExceptionHandler(Exception.class)
    public Result otherException(Exception e) {
        e.printStackTrace();
        logger.error("系统异常 全局拦截异常信息:{}",e.getMessage());
        return Result.error(e.getMessage());
    }
}


相关文章
|
27天前
|
安全 Java 数据库
安全无忧!在 Spring Boot 3.3 中轻松实现 TOTP 双因素认证
【10月更文挑战第8天】在现代应用程序开发中,安全性是一个不可忽视的重要环节。随着技术的发展,双因素认证(2FA)已经成为增强应用安全性的重要手段之一。本文将详细介绍如何在 Spring Boot 3.3 中实现基于时间的一次性密码(TOTP)双因素认证,让你的应用安全无忧。
62 5
|
3天前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
14 1
|
28天前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
1月前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
141 10
|
1月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
|
3月前
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
952 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
|
3月前
|
Java Spring
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
|
3月前
|
Java 数据安全/隐私保护 Spring
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
揭秘Spring Boot自定义注解的魔法:三个实用场景让你的代码更加优雅高效
|
3月前
|
Java Spring 安全
Spring 框架邂逅 OAuth2:解锁现代应用安全认证的秘密武器,你准备好迎接变革了吗?
【8月更文挑战第31天】现代化应用的安全性至关重要,OAuth2 作为实现认证和授权的标准协议之一,被广泛采用。Spring 框架通过 Spring Security 提供了强大的 OAuth2 支持,简化了集成过程。本文将通过问答形式详细介绍如何在 Spring 应用中集成 OAuth2,包括 OAuth2 的基本概念、集成步骤及资源服务器保护方法。首先,需要在项目中添加 `spring-security-oauth2-client` 和 `spring-security-oauth2-resource-server` 依赖。
50 0
|
3月前
|
监控 安全 Java
【开发者必备】Spring Boot中自定义注解与处理器的神奇魔力:一键解锁代码新高度!
【8月更文挑战第29天】本文介绍如何在Spring Boot中利用自定义注解与处理器增强应用功能。通过定义如`@CustomProcessor`注解并结合`BeanPostProcessor`实现特定逻辑处理,如业务逻辑封装、配置管理及元数据分析等,从而提升代码整洁度与可维护性。文章详细展示了从注解定义、处理器编写到实际应用的具体步骤,并提供了实战案例,帮助开发者更好地理解和运用这一强大特性,以实现代码的高效组织与优化。
150 0