You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Boot WebSocket STOMP + JWT 认证下@PreAuthorize无法获取SecurityContext中Authentication对象的问题

Spring Boot WebSocket STOMP + JWT 认证下@PreAuthorize无法获取SecurityContext中Authentication对象的问题

我完全理解你遇到的困扰——在STOMP CONNECT阶段通过JWT成功认证了用户,但处理@MessageMapping方法时,Spring Security的SecurityContextHolder里始终拿不到对应的认证对象,导致@PreAuthorize注解抛出异常。这是WebSocket与Spring Security集成时的典型问题,核心原因和解决方案都很明确:

问题根源

WebSocket的消息处理线程模型和普通HTTP请求有本质区别:

  1. STOMP CONNECT请求的处理线程和后续@MessageMapping方法的处理线程几乎不可能是同一个(WebSocket依赖线程池处理消息)。
  2. SecurityContextHolder默认采用线程绑定的存储策略(MODE_THREADLOCAL),所以在CONNECT线程中设置的认证信息不会自动传递到处理消息的线程。
  3. 你在ChannelInterceptor中把认证对象绑定到了StompHeaderAccessor,但@PreAuthorize这类方法级安全注解默认只会从SecurityContextHolder读取认证信息,不会主动从StompHeaderAccessor获取用户。

解决方案:双拦截器+线程安全的SecurityContext同步

你已经完成了第一步(CONNECT阶段的JWT认证),现在需要补充一个全局拦截器,把StompHeaderAccessor中的认证信息同步到每个消息处理线程的SecurityContextHolder中,同时注意线程复用的问题(必须清理上下文避免用户信息泄漏)。

步骤1:确认CONNECT阶段的认证拦截器逻辑正确

先确保你的自定义拦截器在CONNECT时能生成完全认证的Authentication对象,并正确绑定到StompHeaderAccessor。可以参考这个优化后的示例:

@Component
public class StompJwtConnectInterceptor implements ChannelInterceptor {

    private final JwtTokenValidator jwtTokenValidator; // 你的JWT验证工具类

    public StompJwtConnectInterceptor(JwtTokenValidator jwtTokenValidator) {
        this.jwtTokenValidator = jwtTokenValidator;
    }

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        assert accessor != null;

        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            // 提取并处理Authorization头
            List<String> authHeaders = accessor.getNativeHeader("Authorization");
            if (authHeaders == null || authHeaders.isEmpty()) {
                throw new AuthenticationCredentialsNotFoundException("STOMP CONNECT请求缺少Authorization头");
            }

            String jwtToken = authHeaders.get(0).replace("Bearer ", "").trim();
            // 验证JWT并生成已认证的Authentication对象
            Authentication authenticatedUser = jwtTokenValidator.validateTokenAndGetAuthentication(jwtToken);
            
            // 关键:将认证对象绑定到STOMP帧的HeaderAccessor
            accessor.setUser(authenticatedUser);
            // 标记头为可修改,确保后续流程能获取到更新后的用户信息
            accessor.setLeaveMutable(true);
        }
        return message;
    }
}

⚠️ 注意:务必保证authenticatedUser.isAuthenticated()返回true,比如使用UsernamePasswordAuthenticationToken构造时传入true,或直接使用Spring Security提供的JwtAuthenticationToken

步骤2:实现SecurityContext传播拦截器

这个拦截器会在每个入站STOMP消息处理前,把StompHeaderAccessor中的认证信息同步到当前线程的SecurityContextHolder,处理完成后强制清理上下文(避免线程池复用导致的用户信息串流):

@Component
public class SecurityContextPropagationInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (accessor != null && accessor.getUser() instanceof Authentication authentication) {
            // 创建新的SecurityContext并设置认证信息
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            context.setAuthentication(authentication);
            SecurityContextHolder.setContext(context);
        }
        return message;
    }

    @Override
    public void afterCompletion(Message<?> message, MessageChannel channel, Exception ex) {
        // 必须清理:WebSocket线程池会复用线程,避免残留上一个用户的认证信息
        SecurityContextHolder.clearContext();
    }
}

步骤3:正确注册拦截器到WebSocket入站通道

在WebSocket配置类中,把两个拦截器注册到clientInboundChannel顺序至关重要:先处理CONNECT认证,再处理SecurityContext同步:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final StompJwtConnectInterceptor authInterceptor;
    private final SecurityContextPropagationInterceptor securityContextInterceptor;

    // 构造注入拦截器
    public WebSocketConfig(StompJwtConnectInterceptor authInterceptor,
                           SecurityContextPropagationInterceptor securityContextInterceptor) {
        this.authInterceptor = authInterceptor;
        this.securityContextInterceptor = securityContextInterceptor;
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        // 顺序:先完成CONNECT请求的认证,再为所有入站消息同步SecurityContext
        registration.interceptors(authInterceptor, securityContextInterceptor);
    }

    // 其他WebSocket配置(消息 broker、端点注册等)
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*") // 生产环境请替换为具体可信域名
                .withSockJS();
    }
}

步骤4:启用方法级安全

确保你的Spring Security配置类上添加了@EnableMethodSecurity(替代旧版的@EnableGlobalMethodSecurity),这样@PreAuthorize注解才能被Spring识别并生效:

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    // 其他HTTP安全配置...
    // 注意:WebSocket端点不需要CSRF保护,可在规则中排除
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.ignoringRequestMatchers("/ws/**"))
                // 你的其他HTTP安全规则...
        return http.build();
    }
}

关键检查点

  1. 日志验证:在SecurityContextPropagationInterceptorpreSend方法中添加日志,确认accessor.getUser()SecurityContextHolder.getContext().getAuthentication()一致且权限正确。
  2. 线程清理:必须在afterCompletion中清理SecurityContextHolder,否则线程池中的线程会携带上一个用户的认证信息,导致严重的权限混乱。
  3. Authentication状态:再次确认你的Authentication对象的isAuthenticated()返回true——如果是false,@PreAuthorize会直接判定用户未认证。

针对你的GitHub项目的额外建议

查看你的项目代码时,可以重点检查:

  • clientInboundChannel的拦截器注册顺序是否符合“先认证、后同步”的要求
  • 配置类上是否启用了@EnableMethodSecurity
  • JWT生成的Authentication对象是否包含SCOPE_WRITE权限(匹配你控制器上的@PreAuthorize规则)

按照上述配置完成后,每个@MessageMapping方法处理时,SecurityContextHolder中都会存在正确的Authentication对象,@PreAuthorize注解就能正常工作了。

内容来源于stack exchange

火山引擎 最新活动