跳到主要内容

安全增强

安全增强基于 spring-boot-starter-security 做了一层薄扩展,重点是把当前请求的 Authentication 优雅地注入到 Controller 入参对象中,避免在每个方法里手写 SecurityContextHolder.getContext().getAuthentication() 与字段赋值。

主要扩展

  • @CurrentAuthentication 字段注解:声明哪些字段需要从 Authentication 自动填充
  • RequestBody 自动填充:JSON 请求体反序列化后自动回填带注解字段
  • 表单 / 查询参数自动填充:通过 @InitBinder 介入 WebDataBinder,非 JSON 提交同样生效
  • 对外屏蔽:注解元注解叠加 @JsonIgnore + @Hidden,字段不会出现在请求体反序列化期望中,也不会出现在 OpenAPI 文档中

安装

Maven

<dependency>
<groupId>com.jeeapp.spring.boot</groupId>
<artifactId>security-spring-boot-starter</artifactId>
</dependency>

Gradle

implementation 'com.jeeapp.spring.boot:security-spring-boot-starter'

扩展能力

@CurrentAuthentication 注解

com.jeeapp.security.core.annotation.CurrentAuthentication 是一个字段级注解,元注解叠加:

  • @JsonIgnore:Jackson 序列化 / 反序列化忽略
  • @Hidden:Swagger / OpenAPI 文档隐藏
  • @JacksonAnnotationsInside:让上述 Jackson 元注解生效

可选属性:

  • expression:SpEL 表达式,根对象为当前 Authentication,可使用 #this 引用自身。例如 #this.principal.idprincipal.userId
  • errorOnInvalidType:当解析结果与字段类型不兼容时是否抛 ClassCastException,默认 false(静默忽略)。
@Data
public class CreateOrderRequest {

private String productId;

private int quantity;

/** 自动从当前登录用户填充,前端无需也无法传入 */
@CurrentAuthentication(expression = "principal.id")
private Long userId;
}

@RequestBody 自动填充

com.jeeapp.security.web.method.annotation.CurrentAuthenticationRequestBodyAdvice 实现 Spring MVC 的 RequestBodyAdvice,注册为全局 @RestControllerAdvice

  • supports(...):仅介入 Jackson 系列消息转换器(AbstractJackson2HttpMessageConverter)+ @RequestBody 参数
  • afterBodyRead(...):反序列化得到的对象,反射遍历所有字段,命中 @CurrentAuthentication 时:
    • 取当前 Authentication(无登录则跳过)
    • expression 非空,使用 SpelExpressionParser 求值;否则直接使用 Authentication
    • 检查字段类型与结果类型可兼容,赋值;不兼容时按 errorOnInvalidType 抛错或忽略

效果:任何 @RequestBody 的 DTO 内带 @CurrentAuthentication 的字段都会被自动填充,无需在 Controller 内重复写 request.setUserId(...)

表单 / 查询参数自动填充

同一个 Advice 还实现了 @InitBinder

@InitBinder
public void initBinder(WebDataBinder binder) { ... }

它会遍历目标对象的字段,把 @CurrentAuthentication 字段以 MutablePropertyValues 形式追加到 WebDataBinder.bind(...),让通过 @ModelAttribute / 表单 / Query 提交的请求也具备自动填充能力。

按需装配

com.jeeapp.security.autoconfigure.SecurityAutoConfiguration 仅在 ConditionalOnWebApplication(type = SERVLET) 条件下注册 CurrentAuthenticationRequestBodyAdvice,避免对响应式应用产生影响。

完整实例

下面以「下单」接口为例,演示如何用 @CurrentAuthentication 同时覆盖 JSON 提交与表单 / 查询参数提交两种场景。

1. 自定义 Principal

假设登录后存放在 SecurityContext 中的 Authentication.principal 是如下对象:

@Data
@AllArgsConstructor
public class LoginUser {

private Long id;

private String username;

private String tenantId;
}

2. 请求 DTO

通过 @CurrentAuthentication(expression = ...) 让框架在请求体反序列化或表单绑定后,自动从 Authentication 中抽取字段并回填。

@Data
public class CreateOrderRequest {

/** 业务字段,由前端传入 */
private String productId;

private int quantity;

/** 自动填充:当前登录用户 ID */
@CurrentAuthentication(expression = "principal.id")
private Long userId;

/** 自动填充:当前登录用户所属租户 */
@CurrentAuthentication(expression = "principal.tenantId")
private String tenantId;
}

注解叠加的 @JsonIgnore + @HiddenuserId / tenantId

  • 不会出现在 Swagger 接口文档的请求示例中
  • 即便前端故意传值,反序列化阶段也会被忽略,最终一定取自当前登录态

3. Controller

同一个 DTO 在 JSON 与表单两种入口下都能正确填充:

@RestController
@RequestMapping("/orders")
public class OrderController {

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public OrderVO createByJson(@RequestBody CreateOrderRequest request) {
// request.userId / request.tenantId 已由 RequestBodyAdvice 自动填充
return orderService.create(request);
}

@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public OrderVO createByForm(CreateOrderRequest request) {
// request.userId / request.tenantId 已由 @InitBinder 自动填充
return orderService.create(request);
}
}

4. 实际效果

前端只需要传业务字段:

POST /orders HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json

{
"productId": "P-1001",
"quantity": 2
}

服务端拿到的 request 实际等价于:

{
"productId": "P-1001",
"quantity": 2,
"userId": 10086,
"tenantId": "tenant-a"
}

如果前端尝试在请求体中显式传 "userId": 1 试图越权,由于 @CurrentAuthentication 元注解中包含 @JsonIgnore,Jackson 会直接忽略该字段,最终值仍来自 SecurityContextHolder 中的当前登录用户。

相关资源