You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring OAuth实现WebSocket认证授权遇权限拒绝问题排查

问题描述

项目依赖org.springframework.security:spring-security-messaging,已基于OAuth2实现JWT认证,HTTP接口的JWT认证功能正常。添加WebSocket消息代理配置及控制器后,/app/status(非保护接口)可正常调用,但携带Bearer Token调用/app/hello(受保护接口)时始终出现Access Denied异常,且相同Token在HTTP受保护接口可正常使用。

关键配置遗漏点及修复方案

Spring Security WebSocket的JWT认证逻辑与HTTP认证存在差异(WebSocket握手为HTTP请求,但后续消息为WebSocket帧),以下是常见遗漏配置及修复方式:

1. 正确配置WebSocket安全类

需使用@EnableWebSocketSecurity注解(Spring Security 6+)并实现SecurityWebSocketMessageBrokerConfigurer,不能直接复用HTTP的安全配置类:

@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig(
    private val jwtDecoder: JwtDecoder,
    private val jwtAuthenticationConverter: JwtAuthenticationConverter
) : SecurityWebSocketMessageBrokerConfigurer {

    // 配置消息路径的权限规则
    override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
        messages
            .simpDestMatchers("/app/status").permitAll()
            .simpDestMatchers("/app/hello").authenticated()
            .anyMessage().denyAll()
    }

    // 将JWT认证集成到WebSocket消息处理流程
    override fun configureAuthentication(auth: WebSocketAuthenticationManagerConfigurer) {
        auth.jwt { jwtConfig ->
            jwtConfig.decoder(jwtDecoder)
                .jwtAuthenticationConverter(jwtAuthenticationConverter)
        }
    }

    // 允许WebSocket握手请求的传输权限
    override fun configureWebSocketTransport(registry: WebSocketTransportSecurityConfigurer) {
        registry.anyMessage().permitAll()
    }
}

2. 确保JWT权限转换配置生效

如果Token中包含自定义权限声明,需配置JwtAuthenticationConverter完成权限映射,并注入到WebSocket安全类中:

@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val authoritiesConverter = JwtGrantedAuthoritiesConverter().apply {
        setAuthoritiesClaimName("roles") // 对应Token中存储权限的Claim字段名
        setAuthorityPrefix("ROLE_") // 权限前缀,需与接口权限校验规则匹配
    }
    return JwtAuthenticationConverter().apply {
        setJwtGrantedAuthoritiesConverter(authoritiesConverter)
    }
}

3. 确认Token传递方式被正确解析

WebSocket支持两种Token传递方式,需根据客户端实现匹配对应配置:

  • 握手时传递Token:客户端在WebSocket握手请求的HTTP头中携带Authorization: Bearer <token>,此方式无需额外配置,Spring Security会自动解析。
  • 消息帧中传递Token:若Token在WebSocket消息的headers中携带,需自定义ServerAuthenticationConverter提取Token:
class JwtWebSocketAuthConverter(
    private val jwtDecoder: JwtDecoder,
    private val jwtAuthConverter: JwtAuthenticationConverter
) : ServerAuthenticationConverter {

    override fun convert(message: Message<*>): Mono<Authentication> {
        val authHeader = message.headers.getFirst("Authorization") as? String
            ?: return Mono.empty()
        if (!authHeader.startsWith("Bearer ")) {
            return Mono.empty()
        }
        val token = authHeader.substring(7)
        val jwt = jwtDecoder.decode(token)
        return Mono.justOrEmpty(jwtAuthConverter.convert(jwt))
    }
}

然后在configureAuthentication中替换为自定义转换器:

override fun configureAuthentication(auth: WebSocketAuthenticationManagerConfigurer) {
    auth.authenticationConverter(
        JwtWebSocketAuthConverter(jwtDecoder, jwtAuthenticationConverter)
    )
}

4. 保证HTTP安全配置不拦截WebSocket握手路径

在HTTP的SecurityConfig中,需允许WebSocket握手端点(如/ws)的访问:

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val jwtDecoder: JwtDecoder,
    private val jwtAuthenticationConverter: JwtAuthenticationConverter
) : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/ws/**").permitAll() // 允许WebSocket握手路径
            .anyRequest().authenticated()
            .and()
            .oauth2ResourceServer()
            .jwt()
            .decoder(jwtDecoder)
            .jwtAuthenticationConverter(jwtAuthenticationConverter)
    }
}

5. 检查WebSocket消息代理配置正确性

确保@EnableWebSocketMessageBroker配置的路径前缀与控制器映射一致:

@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {

    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        config.enableSimpleBroker("/topic") // 消息代理前缀
        config.setApplicationDestinationPrefixes("/app") // 应用接口前缀,需与控制器@MessageMapping的路径匹配
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/ws").withSockJS() // 握手端点
    }
}

内容的提问来源于stack exchange,提问作者Jackie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 14:05:56