SecurityContext
定义与作用
SecurityContext 是 Spring Security 中封装当前请求认证信息的接口,只定义了两个方法:
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
它代表"当前请求的安全快照"——认证成功后,Authentication 对象被存入 SecurityContext,再被 SecurityContextHolder 绑定到当前线程。后续所有安全决策(授权检查、方法安全注解等)都从这里读取用户信息。
手写过滤器的痛点
手写登录时,认证信息的存储和传递通常很随意:
// 痛点:用户信息存在 HttpSession,但框架层和组件层没有统一访问方式
HttpSession session = request.getSession();
session.setAttribute("user", user);
// 在 Service 层想获取当前用户?
// 方案A:把 HttpSession 一路传下去 → 污染方法签名
// 方案B:从 ThreadLocal 取 → 自己维护,容易内存泄漏
// 方案C:从全局 Context 取 → 没有标准接口,各写各的
public void doBusiness(HttpSession session) { // 参数污染!
User user = (User) session.getAttribute("user");
}
SecurityContext 提供与传输层无关的认证封装,无论用户信息来自 Session、JWT、OAuth2 还是请求头,最终都统一收敛为 SecurityContext 中的 Authentication。
核心原理
在认证流程中的位置
默认实现
Spring Security 提供唯一的标准实现 SecurityContextImpl:
public class SecurityContextImpl implements SecurityContext {
private Authentication authentication;
public SecurityContextImpl() {}
public SecurityContextImpl(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
持久化机制
SecurityContextHolderFilter(或旧版的 SecurityContextPersistenceFilter)负责在请求开始和结束时与持久化存储交互:
示例一:手动设置 SecurityContext 实现系统内认证
场景说明
后台定时任务或系统内部调用(如消息队列消费)没有用户登录请求,但需要以特定系统身份执行操作,并触发方法级安全注解检查。
操作前配置
定时任务中直接调用 Service,绕过 Spring Security:
@Service
public class ScheduledTaskService {
@Autowired
private OrderService orderService;
@Scheduled(cron = "0 0 2 * * ?")
public void nightlyCleanup() {
// 直接调用,但 orderService 上有 @PreAuthorize("hasRole('ADMIN')")
// 由于没有认证信息,会抛出 AccessDeniedException
orderService.archiveOldOrders();
}
}
操作后配置
手动构造 SecurityContext 并注入 SecurityContextHolder:
@Service
public class ScheduledTaskService {
@Autowired
private OrderService orderService;
@Scheduled(cron = "0 0 2 * * ?")
public void nightlyCleanup() {
// 1. 构造系统身份认证信息
UserDetails systemUser = User.builder()
.username("SYSTEM")
.password("")
.roles("ADMIN", "SYSTEM")
.build();
Authentication authentication = new UsernamePasswordAuthenticationToken(
systemUser,
null,
systemUser.getAuthorities()
);
// 2. 创建 SecurityContext 并放入 Holder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
try {
// 3. 执行业务,此时 @PreAuthorize 能正常获取到 SYSTEM 用户的权限
orderService.archiveOldOrders();
} finally {
// 4. 必须清理,避免污染后续操作
SecurityContextHolder.clearContext();
}
}
}
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public void archiveOldOrders() {
// 归档30天前的订单
// 安全注解通过 SecurityContextHolder → SecurityContext → Authentication 获取权限
}
}
配置方法级安全:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig { }
结果分析
SecurityContext是Authentication的轻量级容器,手动构造后即可让框架的安全注解生效- 适用于定时任务、消息队列消费、RPC 回调等"无用户请求"但需要安全上下文的场景
finally块中必须clearContext(),否则该线程后续处理其他任务时携带系统身份,造成越权
示例二:从 SecurityContext 获取信息并用于审计日志
场景说明
在 AOP 拦截层统一记录操作审计日志,需要从 SecurityContext 提取当前用户和权限信息。
操作前配置
在每个 Controller 中手动获取用户并传入日志服务:
@RestController
public class OrderController {
@Autowired
private AuditService auditService;
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestBody OrderDto dto) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
String roles = auth.getAuthorities().toString();
auditService.log("CREATE_ORDER", username, roles, dto.toString());
// 业务逻辑...
}
@DeleteMapping("/orders/{id}")
public ResponseEntity<?> deleteOrder(@PathVariable Long id) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
auditService.log("DELETE_ORDER", username, null, "id=" + id);
// 业务逻辑...
}
}
操作后配置
使用 AOP 统一拦截,从 SecurityContext 自动提取信息:
@Aspect
@Component
public class AuditAspect {
@Autowired
private AuditService auditService;
@Around("@annotation(auditable)")
public Object around(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable {
// 1. 从 SecurityContext 获取当前用户
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
String roles = auth != null ?
auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(","))
: "";
// 2. 提取操作信息
String action = auditable.action();
String args = Arrays.toString(joinPoint.getArgs());
// 3. 执行目标方法
Object result = joinPoint.proceed();
// 4. 记录审计
auditService.log(action, username, roles, args);
return result;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
String action();
}
Controller 中使用注解:
@RestController
public class OrderController {
@Auditable(action = "CREATE_ORDER")
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestBody OrderDto dto) {
// 无需手动获取用户信息,AOP 自动完成审计
return ResponseEntity.ok(orderService.create(dto));
}
@Auditable(action = "DELETE_ORDER")
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/orders/{id}")
public ResponseEntity<?> deleteOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.delete(id));
}
}
结果分析
SecurityContext作为认证信息的统一容器,使得 AOP、拦截器、自定义注解等横切关注点都能标准化获取用户身份- 无需将用户信息作为参数在方法间传递,降低了业务代码与安全逻辑的耦合度
易错场景:SecurityContext 与 Authentication 的修改不可预期
问题描述
开发者在业务代码中直接修改 SecurityContext 中的 Authentication,添加或移除权限,期望影响当前请求的后续授权判断。但发现有时生效,有时不生效。
错误代码
@Service
public class DynamicRoleService {
public void addTempRole(String role) {
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
// 危险:直接修改当前 Authentication 的权限列表
List<GrantedAuthority> updatedAuthorities = new ArrayList<>(auth.getAuthorities());
updatedAuthorities.add(new SimpleGrantedAuthority(role));
// 重新设置回 SecurityContext
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
auth.getPrincipal(), auth.getCredentials(), updatedAuthorities
);
context.setAuthentication(newAuth); // 试图动态加权限
}
}
原因分析
- 如果当前请求仍在同一个线程中,后续的安全检查会读到新的
Authentication - 但请求结束后,
SecurityContext会被持久化到 Session。如果修改发生在持久化之前,下次请求可能携带临时权限 - 更严重的是,如果使用了缓存或复制机制(如
InheritableThreadLocal),子线程可能拿到旧引用,出现权限不一致
正确做法
不要在业务代码中动态修改 SecurityContext 的 Authentication。临时权限应通过其他机制实现:
- 使用
@PreAuthorize的 SpEL 表达式引用 Bean 方法做动态判断 - 在自定义
AccessDecisionVoter中实现动态权限决策 - 如果确实需要临时切换身份,使用
RunAsManager或SecurityContext.runAs()机制
// 正确方式:用 SpEL 表达式动态判断,而不是改 SecurityContext
@PreAuthorize("@permissionService.hasTempRole(authentication.principal.username, 'TEMP_ROLE')")
public void sensitiveOperation() {
// ...
}
面试考点:
SecurityContext和Authentication是什么关系?SecurityContext在请求结束后是如何持久化的?(SecurityContextRepository→HttpSession)- 为什么不要在业务代码中直接修改
SecurityContext?- 定时任务或内部调用中,如何安全地构造
SecurityContext?