@PreAuthorize、JSON to Object、Bean Validation的执行顺序

2026-03-09

执行顺序通常是:JSON 反序列化 -> 属性校验 (@Valid) -> @PreAuthorize 权限检查 -> 业务方法执行

这意味着:

  1. 把请求的 JSON 数据转换成 Java 对象。

  2. 然后检查这个对象里的数据是否符合规则(如非空、长度限制等)。

  3. 最后才检查当前用户是否有权限执行该方法(@PreAuthorize)。

如果 JSON 格式错误或者属性校验失败,@PreAuthorize 根本不会执行。

详细执行流程解析

这个顺序是由 Spring MVC 和 Spring Security 的拦截器链(Interceptor Chain)及 AOP 代理机制决定的。以下是基于 Spring Boot (Spring MVC + Spring Security) 的标准请求处理流程:

1. 请求进入 DispatcherServlet

所有 HTTP 请求首先到达 DispatcherServlet

2. 参数解析与 JSON 反序列化 (HttpMessageConverter)

  • 阶段:在调用 Controller 方法之前,Spring MVC 需要处理方法参数(例如 @RequestBody User user)。

  • 动作RequestResponseBodyMethodProcessor 会使用 HttpMessageConverter(如 Jackson)将 HTTP 请求体中的 JSON 字符串转换为 Java 对象。

  • 结果:此时 Java 对象已经创建完成。如果 JSON 格式错误(如类型不匹配),这里直接抛出 HttpMessageNotReadableException,流程终止。

3. 参数校验 (Bean Validation / @Valid)

  • 阶段:参数解析完成后,方法调用前。

  • 动作:如果参数上有 @Valid@Validated 或字段上有校验注解,MethodValidationPostProcessor (或 Spring MVC 内置的校验逻辑) 会立即执行校验。

  • 结果:如果校验失败(例如必填项为空),抛出 MethodArgumentNotValidExceptionConstraintViolationException,流程终止。此时尚未进入 Spring Security 的方法拦截器。

4. Spring Security 拦截器 (MethodSecurityInterceptor)

  • 阶段:参数已准备好且校验通过,准备真正调用目标 Bean 的方法时。

  • 动作

    • Spring Security 通过 AOP(动态代理)包裹了你的 Service 或 Controller Bean。

    • 当代码试图调用目标方法时,首先被代理对象拦截。

    • MethodSecurityInterceptor 获取 @PreAuthorize 注解。

    • 它计算 SpEL 表达式(例如 hasRole('ADMIN')#user.id == authentication.principal.id)。注意:此时因为第 1 步已完成,SpEL 表达式中可以安全地引用方法参数(如 #user)。

  • 结果

    • 如果权限不足,抛出 AccessDeniedException

    • 如果权限通过,才允许执行真正的目标方法。

5. 业务方法执行

  • 只有上述所有步骤都成功,你的业务代码才会运行。

为什么是这个顺序?

  1. 数据依赖@PreAuthorize 的 SpEL 表达式经常需要使用方法参数作为判断依据(例如:@PreAuthorize("#userId == authentication.principal.id")@PreAuthorize("@service.check(#user)"))。如果不先进行 JSON 反序列化和校验,表达式中引用的参数可能不存在或不合法,导致无法进行权限判断。

  2. 职责分离

    • MVC 层 负责处理 HTTP 协议、数据绑定和数据格式的合法性(是不是个合法的对象?)。

    • Security 层 负责业务逻辑入口的安全性(这个人能不能做这件事?)。

    • Service 层 负责具体的业务逻辑。

    • 通常认为,如果数据本身都是错的(校验失败),讨论“谁有权限操作这些错误数据”是没有意义的,因此校验优先于权限检查。

场景 A:发送非法 JSON (如 age: "abc")

  1. JSON 反序列化失败。

  2. 抛出异常。

  3. @PreAuthorize 未执行

场景 B:发送 JSON 但校验失败 (如 name: null,而 UserDto 中 name 有 @NotNull)

  1. JSON 反序列化成功,生成对象。

  2. @Valid 触发校验,发现 name 为空。

  3. 抛出 MethodArgumentNotValidException

  4. @PreAuthorize 未执行

场景 C:发送合法 JSON,但用户角色不对

  1. JSON 反序列化成功。

  2. @Valid 校验通过。

  3. 进入 Spring Security 代理。

  4. @PreAuthorize 检查角色,发现不是 ADMIN。

  5. 抛出 AccessDeniedException (403 Forbidden)。

场景 D:发送合法 JSON,且用户角色正确

  1. JSON 反序列化成功。

  2. @Valid 校验通过。

  3. @PreAuthorize 检查通过。

  4. 执行方法体。

特殊情况说明

虽然标准顺序如上,但在极少数自定义配置下可能会改变:

  • 自定义 ArgumentResolver:如果你完全自定义了参数解析器,并且在该解析器内部手动调用了安全逻辑,顺序可能改变(极不推荐)。

  • Filter 级别的权限控制:如果你在 SecurityFilterChain 中配置了基于 URL 的权限(如 .requestMatchers("/admin/**").hasRole("ADMIN")),那么这个 Filter 会在 JSON 反序列化之前 就执行。但这是针对 URL 路径的粗粒度控制,不是 @PreAuthorize 注解的行为。@PreAuthorize 特指方法级安全,必然在参数解析之后。

总结

对于 @PreAuthorize 注解:

  • JSON 转 Java 对象先执行

  • 属性校验 (@Valid)先执行

  • @PreAuthorize后执行

这种设计确保了你在编写权限表达式(SpEL)时,可以放心地使用已经过校验的方法参数。