乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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?
上一页
表单登录
下一页
SecurityContextHolder