我发现了一种在测试中效果很好的黑客程序。绕过内置的Spring连接级Spring auth机制。相反,可以通过在客户端的Stomp标头中发送身份验证令牌来在消息级别设置身份验证令牌(这很好地反映了常规HTTP XHR调用已在执行的操作),例如:
stompClient.connect({'X-Authorization': 'token'}, ...);stompClient.subscribe(..., {'X-Authorization': 'token'});stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);在服务器端,使用以下命令从Stomp消息中获取令牌: ChannelInterceptor
@Overridepublic void configureClientInboundChannel(ChannelRegistration registration) { registration.setInterceptors(new ChannelInterceptorAdapter() { Message<*> preSend(Message<*> message, MessageChannel channel) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); List tokenList = accessor.getNativeHeader("X-Authorization"); String token = null; if(tokenList == null || tokenList.size < 1) { return message; } else { token = tokenList.get(0); if(token == null) { return message; } } // validate and convert to a Principal based on your own requirements e.g. // authenticationManager.authenticate(JwtAuthentication(token)) Principal yourAuth = [...]; accessor.setUser(yourAuth); // not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler! accessor.setLeaveMutable(true); return MessageBuilder.createMessage(message.payload, accessor.messageHeaders) } })这很简单,可以让我们达到85%的方式,但是,这种方法不支持向特定用户发送消息。这是因为Spring的将用户关联到会话的机制不受的结果影响
ChannelInterceptor。Spring WebSocket假定身份验证是在传输层而不是消息层完成的,因此忽略了消息级身份验证。
使这项工作反正砍,就是创造我们的情况
DefaultSimpUserRegistry和
DefaultUserDestinationResolver,揭露那些环境,然后用拦截器来更新这些仿佛Spring本身是这样做。换句话说,类似:
@Configuration@EnableWebSocketMessageBroker@Order(HIGHEST_PRECEDENCE + 50)class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer() { private DefaultSimpUserRegistry userRegistry = new DefaultSimpUserRegistry(); private DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry); @Bean @Primary public SimpUserRegistry userRegistry() { return userRegistry; } @Bean @Primary public UserDestinationResolver userDestinationResolver() { return resolver; } @Override public configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue", "/topic"); } @Override public registerStompEndpoints(StompEndpointRegistry registry) { registry .addEndpoint("/stomp") .withSockJS() .setWebSocketEnabled(false) .setSessioncookieNeeded(false); } @Override public configureClientInboundChannel(ChannelRegistration registration) { registration.setInterceptors(new ChannelInterceptorAdapter() { Message<*> preSend(Message<*> message, MessageChannel channel) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); List tokenList = accessor.getNativeHeader("X-Authorization"); accessor.removeNativeHeader("X-Authorization"); String token = null; if(tokenList != null && tokenList.size > 0) { token = tokenList.get(0); } // validate and convert to a Principal based on your own requirements e.g. // authenticationManager.authenticate(JwtAuthentication(token)) Principal yourAuth = token == null ? null : [...]; if (accessor.messageType == SimpMessageType.CONNECT) { userRegistry.onApplicationEvent(SessionConnectedEvent(this, message, yourAuth)); } else if (accessor.messageType == SimpMessageType.SUBSCRIBE) { userRegistry.onApplicationEvent(SessionSubscribeEvent(this, message, yourAuth)); } else if (accessor.messageType == SimpMessageType.UNSUBSCRIBE) { userRegistry.onApplicationEvent(SessionUnsubscribeEvent(this, message, yourAuth)); } else if (accessor.messageType == SimpMessageType.DISCONNECT) { userRegistry.onApplicationEvent(SessionDisconnectEvent(this, message, accessor.sessionId, CloseStatus.NORMAL)); } accessor.setUser(yourAuth); // not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler! accessor.setLeaveMutable(true); return MessageBuilder.createMessage(message.payload, accessor.messageHeaders); } }) }}现在,Spring完全意识到了身份验证,即它将注入Principal到需要它的任何控制器方法中,将其公开给Spring Security 4.x的上下文,并将用户与WebSocket会话相关联,以向特定的用户/会话发送消息。
Spring Security Messaging
最后,如果你使用
Spring Security 4.x Messaging支持,请确保将@Order你的
AbstractWebSocketMessageBrokerConfigurer设置为高于
Spring Security的值
AbstractSecurityWebSocketMessageBrokerConfigurer(
Ordered.HIGHEST_PRECEDENCE + 50将起作用,如上所示)。这样,你的拦截器将Principal在
Spring Security执行其检查之前设置并设置安全上下文。
上面的代码中的这一行似乎使很多人感到困惑:
// validate and convert to a Principal based on your own requirements e.g. // authenticationManager.authenticate(JwtAuthentication(token)) Principal yourAuth = [...];
这不是问题的范围,因为它不是特定于Stomp的,但是无论如何我都会对其进行一些扩展,因为它与在Spring中使用auth令牌有关。使用基于令牌的身份验证时,Principal通常需要的
JwtAuthentication是扩展Spring Security的
AbstractAuthenticationToken类的自定义类。
AbstractAuthenticationToken实现
Authentication扩展该
Principal接口的接口,并包含将令牌与
Spring Security集成的大多数机制。
因此,在Kotlin代码中(很抱歉,我没有时间或意愿将其转换回Java),你
JwtAuthentication可能看起来像这样,这是一个简单的包装
AbstractAuthenticationToken:
import my.model.UserEntityimport org.springframework.security.authentication.AbstractAuthenticationTokenimport org.springframework.security.core.GrantedAuthorityclass JwtAuthentication( val token: String, // UserEntity is your application's model for your user val user: UserEntity? = null, authorities: Collection<GrantedAuthority>? = null) : AbstractAuthenticationToken(authorities) { override fun getCredentials(): Any? = token override fun getName(): String? = user?.id override fun getPrincipal(): Any? = user}现在你需要一个
AuthenticationManager知道如何处理它的人。在Kotlin中,这可能看起来像以下内容:
@Componentclass CustomTokenAuthenticationManager @Inject constructor( val tokenHandler: TokenHandler, val authService: AuthService) : AuthenticationManager { val log = logger() override fun authenticate(authentication: Authentication?): Authentication? { return when(authentication) { // for login via username/password e.g. crash shell is UsernamePasswordAuthenticationToken -> { findUser(authentication).let { //checkUser(it) authentication.withGrantedAuthorities(it).also { setAuthenticated(true) } } } // for token-based auth is JwtAuthentication -> { findUser(authentication).let { val tokenTypeClaim = tokenHandler.parseToken(authentication.token)[CLAIM_TOKEN_TYPE] when(tokenTypeClaim) { TOKEN_TYPE_ACCESS -> { //checkUser(it) authentication.withGrantedAuthorities(it).also { setAuthenticated(true) } } TOKEN_TYPE_REFRESH -> { //checkUser(it) JwtAuthentication(authentication.token, it, listOf(SimpleGrantedAuthority(Authorities.REFRESH_TOKEN))) } else -> throw IllegalArgumentException("Unexpected token type claim $tokenTypeClaim.") } } } else -> null } } private fun findUser(authentication: JwtAuthentication): UserEntity = authService.login(authentication.token) ?: throw BadCredentialsException("No user associated with token or token revoked.") private fun findUser(authentication: UsernamePasswordAuthenticationToken): UserEntity = authService.login(authentication.principal.toString(), authentication.credentials.toString()) ?: throw BadCredentialsException("Invalid login.") @Suppress("unused", "UNUSED_PARAMETER") private fun checkUser(user: UserEntity) { // TODO add these and lock account on x attempts //if(!user.enabled) throw DisabledException("User is disabled.") //if(user.accountLocked) throw LockedException("User account is locked.") } fun JwtAuthentication.withGrantedAuthorities(user: UserEntity): JwtAuthentication { return JwtAuthentication(token, user, authoritiesOf(user)) } fun UsernamePasswordAuthenticationToken.withGrantedAuthorities(user: UserEntity): UsernamePasswordAuthenticationToken { return UsernamePasswordAuthenticationToken(principal, credentials, authoritiesOf(user)) } private fun authoritiesOf(user: UserEntity) = user.authorities.map(::SimpleGrantedAuthority)}注入的内容将
TokenHandler抽象出JWT令牌解析,但应使用通用的JWT令牌库,如jjwt。注入的内容
AuthService是你的抽象,它实际上是
UserEntity根据令牌中的声明创建你的抽象的,并且可以与你的用户数据库或其他后端系统对话。
现在,回到我们开始与线,它可能是这个样子,哪里
authenticationManager是一个
AuthenticationManager注入由spring我们适配器,是一个实例
CustomTokenAuthenticationManager上面定义我们:
Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));
然后,将此主体附加到消息中,如上所述。HTH!



