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

