安全增强
安全增强基于 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.id、principal.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 + @Hidden 让 userId / 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 中的当前登录用户。