Spring Security 自定义异常失效?从源码分析到解决方案

简介: Spring Security 自定义异常失效?从源码分析到解决方案

问题描述


在基于 Spring Boot 3 + Spring Security 6 的权限管理系统开源项目 youlai-boot 中,根据官方提供的思路重写 AccessDeniedHandler 实现自定义异常处理,但发现该实现并没有生效,而是被全局异常捕获。期望的响应是 {"code":"A0301","msg":"访问未授权"},但实际上获得的响应与期望的不符,具体响应如下图所示:

3.png

项目关键代码


下面贴出关键代码,完整代码:youlai-boot


自定义异常处理器

package com.youlai.system.core.security.exception;
/**
 * Spring Security 访问异常处理器
 *
 * @author haoxr
 * @since 2022/10/18
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
    }
}

Spring Security 配置

package com.youlai.system.core.security.config;
/**
 * Spring Security 权限配置
 *
 * @author haoxr
 * @since 2023/2/17
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final MyAuthenticationEntryPoint authenticationEntryPoint;
    private final MyAccessDeniedHandler accessDeniedHandler;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // ...
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer
                                .authenticationEntryPoint(authenticationEntryPoint) // 未认证异常处理
                                .accessDeniedHandler(accessDeniedHandler) // 未授权访问异常处理
                )
                // ...
        ;
        return http.build();
    }
}

全局异常处理器

package com.youlai.system.common.exception;
/**
 * 全局系统异常处理器
 **/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    // ...
    // 捕获 Exception
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> handleException(Exception e) {
        log.error("unknown exception: {}", e.getMessage());
        return Result.failed(e.getLocalizedMessage());
    }
}

访问权限测试接口


在删除角色接口中,使用了 Spring Security 提供的权限控制注解 @PreAuthorize 来进行权限校验。使用该注解可以在方法级别上对用户的权限进行校验,只有具有相应权限的用户才能执行该方法,否则会提示用户无权访问。

@Operation(summary = "删除角色")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:role:delete')")
public Result deleteRoles(
       @Parameter(description ="删除角色,多个以英文逗号(,)分割") @PathVariable String ids
) {
    boolean result = roleService.deleteRoles(ids);
    return Result.judge(result);
}

问题分析


现象:当存在全局异常处理器时,自定义的 Spring Security 权限异常拦截处理器失效;而当移除全局异常处理器时,自定义的权限异常拦截处理器将正常生效。


猜测:全局异常处理器会干扰到 Spring Security 的权限异常处理流程,导致自定义的处理器无法按预期执行。


根据猜测,直接定位 Spring Security 异常处理和转换的过滤器 ExceptionTranslationFilter 的 doFilter 方法。

4.png

通过分析代码逻辑,可以确定问题的根源在于无权限访问异常被全局异常处理器捕获并处理掉了,因此不会进入 catch 分支执行 handleSpringSecurityException 方法。具体执行过程放在下文的源码解析章节。

964d09f55759b1766b0beb129b82dc2c.png

解决方案


为了确保异常能够传递给 Spring Security 的自定义异常处理器,你可以对全局异常处理器(GlobalExceptionHandler)进行修改。如果异常是 AuthenticationException 或 AccessDeniedException,则继续将其抛出以便交给自定义处理器处理。

package com.youlai.system.common.exception;
//...
/**
 * 全局系统异常处理器
 **/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> handleException(Exception e) throws Exception{
        // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
        if (e instanceof AccessDeniedException
                || e instanceof AuthenticationException) {
            throw e; 
        }
        log.error("unknown exception: {}", e.getMessage());
        return Result.failed(e.getLocalizedMessage());
    }
}

通过这样的修改,当遇到 AuthenticationException 或者 AccessDeniedException 异常时,它们将被继续抛出,从而能够进入到 Spring Security 的自定义异常处理器进行处理。其他类型的异常仍然会在当前异常处理逻辑中进行处理。


修改后,按照自定义异常处理的输出,测试正常。

5.png

源码阅读


接下来通过源码阅读分析:为什么在存在全局异常处理器的情况下,Spring Security的无权限访问异常无法被自定义异常处理器处理?

6.png


ExceptionTranslationFilter#doFilter


在没有全局异常处理器的情况下,异常没有被拦截处理,会进入 catch 分支,由自定义异常处理器处理。


但如果存在全局异常处理器,则不会抛出异常,也就不会进入 catch 分支。


因此,需要进一步探究 chain.doFilter(request, response) 的后续处理,以了解为什么在有全局异常处理器的情况下没有异常抛出。

7.png

DispatcherServlet#doDispatch

10.png

DispatcherServlet#processHandlerException


DispatcherServlet#processDispatchResult 调用 DispatcherServlet#processHandlerException 方法处理异常


有无全局异常处理器在这个方法可以体现出区别了。


有全局异常处理器

9.png

无全局异常处理器

11.png

有全局异常处理器 得到 exMv 不为 null ,后续不抛出异常,所以需要在 exMv = resolver.resolveException(request, response, handler, ex); 断点看下里面的逻辑。


ExceptionHandlerExceptionResolver


调用栈 HandlerExceptionResolverComposite#resolveException →

AbstractHandlerExceptionResolver#resolveException → ExceptionHandlerExceptionResolver#doResolveHandlerMethodException


有全局异常处理器

12.png

无全局异常处理器

13.png

结语


在对异常处理流程进行详细的源码解读后,我们不仅解决了Spring Security权限异常处理在存在全局异常处理器时失效的问题,还对Spring MVC的异常处理机制有了更深入的了解。这一探索不仅有助于更好地理解框架的内部机制,也为未来的项目调优提供了有益的经验。


相关文章
|
5天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
21 1
|
13天前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
28 1
|
21天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
30 1
|
22天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
25 1
|
1月前
|
缓存 JavaScript Java
Spring之FactoryBean的处理底层源码分析
本文介绍了Spring框架中FactoryBean的重要作用及其使用方法。通过一个简单的示例展示了如何通过FactoryBean返回一个User对象,并解释了在调用`getBean()`方法时,传入名称前添加`&`符号会改变返回对象类型的原因。进一步深入源码分析,详细说明了`getBean()`方法内部对FactoryBean的处理逻辑,解释了为何添加`&`符号会导致不同的行为。最后,通过具体代码片段展示了这一过程的关键步骤。
Spring之FactoryBean的处理底层源码分析
|
1月前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
19天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
21 0
|
1月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
163 2