JSON Web Token (JWT) 和基于 Spring 的 SockJS / STOMP Web Socket
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/30887788/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket
提问by Steve Wilford
Background
背景
I am in the process of setting up a RESTful web application using Spring Boot (1.3.0.BUILD-SNAPSHOT) that includes a STOMP/SockJS WebSocket, which I intend to consume from an iOS app as well as web browsers. I want to use JSON Web Tokens(JWT) to secure the REST requests and the WebSocket interface but I'm having difficulty with the latter.
我正在使用包含 STOMP/SockJS WebSocket 的 Spring Boot (1.3.0.BUILD-SNAPSHOT) 设置 RESTful Web 应用程序,我打算从 iOS 应用程序和 Web 浏览器使用它。我想使用JSON Web Tokens(JWT) 来保护 REST 请求和 WebSocket 接口,但我对后者有困难。
The app is secured with Spring Security:-
该应用程序受 Spring Security 保护:-
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public WebSecurityConfiguration() {
super(true);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("steve").password("steve").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.headers().cacheControl().and().and()
// Relax CSRF on the WebSocket due to needing direct access from apps
.csrf().ignoringAntMatchers("/ws/**").and()
.authorizeRequests()
//allow anonymous resource requests
.antMatchers("/", "/index.html").permitAll()
.antMatchers("/resources/**").permitAll()
//allow anonymous POSTs to JWT
.antMatchers(HttpMethod.POST, "/rest/jwt/token").permitAll()
// Allow anonymous access to websocket
.antMatchers("/ws/**").permitAll()
//all other request need to be authenticated
.anyRequest().hasRole("USER").and()
// Custom authentication on requests to /rest/jwt/token
.addFilterBefore(new JWTLoginFilter("/rest/jwt/token", authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
// Custom JWT based authentication
.addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
The WebSocket configuration is standard:-
WebSocket 配置是标准的:-
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
I also have a subclass of AbstractSecurityWebSocketMessageBrokerConfigurer
to secure the WebSocket:-
我还有一个子类AbstractSecurityWebSocketMessageBrokerConfigurer
来保护 WebSocket:-
@Configuration
public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().hasRole("USER");
}
@Override
protected boolean sameOriginDisabled() {
// We need to access this directly from apps, so can't do cross-site checks
return true;
}
}
There is also a couple of @RestController
annotated classes to handle various bits of functionality and these are secured successfully via the JWTTokenFilter
registered in my WebSecurityConfiguration
class.
还有几个带@RestController
注释的类来处理各种功能,并且通过JWTTokenFilter
在我的WebSecurityConfiguration
类中注册成功地保护了这些类。
Problem
问题
However I can't seem to get the WebSocket to be secured with JWT. I am using SockJS 1.1.0and STOMP 1.7.1in the browser and can't figure out how to pass the token. It would appear thatSockJS does not allow parameters to be sent with the initial /info
and/or handshake requests.
但是我似乎无法使用 JWT 来保护 WebSocket。我在浏览器中使用SockJS 1.1.0和STOMP 1.7.1,但不知道如何传递令牌。它看来,SockJS不允许参数与最初的发送/info
和/或握手请求。
The Spring Security for WebSockets documentation statesthat the AbstractSecurityWebSocketMessageBrokerConfigurer
ensures that:
在Spring Security进行的WebSockets文档指出的是,AbstractSecurityWebSocketMessageBrokerConfigurer
确保:
Any inbound CONNECT message requires a valid CSRF token to enforce Same Origin Policy
任何入站 CONNECT 消息都需要有效的 CSRF 令牌来执行同源策略
Which seems to imply that the initial handshake should be unsecured and authentication invoked at the point of receiving a STOMP CONNECT message. Unfortunately I can't seem to find any information with regards to implementing this. Additionally this approach would require additional logic to disconnect a rogue client that opens a WebSocket connection and never sends a STOMP CONNECT.
这似乎意味着初始握手应该是不安全的,并且在接收到 STOMP CONNECT 消息时调用身份验证。不幸的是,我似乎无法找到任何有关实现这一点的信息。此外,这种方法需要额外的逻辑来断开打开 WebSocket 连接并且从不发送 STOMP CONNECT 的流氓客户端。
Being (very) new to Spring I'm also not sure if or how Spring Sessions fits into this. While the documentation is very detailed there doesn't appear to a nice and simple (aka idiots) guide to how the various components fit together / interact with each other.
作为 Spring 的(非常)新手,我也不确定 Spring Sessions 是否或如何适应这一点。虽然文档非常详细,但似乎没有一个很好和简单(又名白痴)的指南来说明各种组件如何组合在一起/相互交互。
Question
题
How do I go about securing the SockJS WebSocket by providing a JSON Web Token, preferably at the point of handshake (is it even possible)?
我如何通过提供 JSON Web 令牌来保护 SockJS WebSocket,最好是在握手时(甚至可能)?
采纳答案by Rossen Stoyanchev
Seems like support for a query string was added to the SockJS client, see https://github.com/sockjs/sockjs-client/issues/72.
似乎对查询字符串的支持已添加到 SockJS 客户端,请参阅https://github.com/sockjs/sockjs-client/issues/72。
回答by Raman
Current Situation
现在的情况
UPDATE 2016-12-13: the issue referenced below is now marked fixed, so the hack below is no longer necessary which Spring 4.3.5 or above. See https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/websocket.adoc#token-authentication.
更新 2016-12-13:下面引用的问题现在被标记为已修复,因此 Spring 4.3.5 或更高版本不再需要下面的 hack。请参阅https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/websocket.adoc#token-authentication。
Previous Situation
以前的情况
Currently (Sep 2016), this is not supported by Spring except via query parameter as answered by @rossen-stoyanchev, who wrote a lot (all?) of the Spring WebSocket support. I don't like the query parameter approach because of potential HTTP referrer leakage and storage of the token in server logs. In addition, if the security ramifications don't bother you, note that I have found this approach works for true WebSocket connections, butif you are using SockJS with fallbacks to other mechanisms, the determineUser
method is never called for the fallback. See Spring 4.x token-based WebSocket SockJS fallback authentication.
目前(2016 年 9 月),Spring 不支持此功能,除非通过 @rossen-stoyanchev 回答的查询参数,他写了很多(全部?)Spring WebSocket 支持。我不喜欢查询参数方法,因为潜在的 HTTP 引用泄漏和令牌存储在服务器日志中。此外,如果安全后果不打扰您,请注意我发现这种方法适用于真正的 WebSocket 连接,但是如果您使用 SockJS 与其他机制的回退,determineUser
则永远不会为回退调用该方法。请参阅Spring 4.x 基于令牌的 WebSocket SockJS 回退身份验证。
I've created a Spring issue to improve support for token-based WebSocket authentication: https://jira.spring.io/browse/SPR-14690
我创建了一个 Spring 问题来改进对基于令牌的 WebSocket 身份验证的支持:https: //jira.spring.io/browse/SPR-14690
Hacking It
破解它
In the meantime, I've found a hack that works well in testing. Bypass the built-in Spring connection-level Spring auth machinery. Instead, set the authentication token at the message-level by sending it in the Stomp headers on the client side (this nicely mirrors what you are already doing with regular HTTP XHR calls) e.g.:
与此同时,我发现了一个在测试中运行良好的黑客。绕过内置的 Spring 连接级 Spring auth 机制。相反,通过在客户端的 Stomp 标头中发送它来在消息级别设置身份验证令牌(这很好地反映了您已经使用常规 HTTP XHR 调用所做的事情)例如:
stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);
On the server-side, obtain the token from the Stomp message using a ChannelInterceptor
在服务器端,使用一个从 Stomp 消息中获取令牌 ChannelInterceptor
@Override
public 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)
}
})
This is simple and gets us 85% of the way there, however, this approach does not support sending messages to specific users. This is because Spring's machinery to associate users to sessions is not affected by the result of the ChannelInterceptor
. Spring WebSocket assumes authentication is done at the transport layer, not the message layer, and thus ignores the message-level authentication.
这很简单,让我们完成了 85% 的工作,但是,这种方法不支持向特定用户发送消息。这是因为 Spring 将用户与会话相关联的机制不受ChannelInterceptor
. Spring WebSocket 假设身份验证是在传输层而不是消息层完成的,因此忽略了消息级身份验证。
The hack to make this work anyway, is to create our instances of DefaultSimpUserRegistry
and DefaultUserDestinationResolver
, expose those to the environment, and then use the interceptor to update those as if Spring itself was doing it. In other words, something like:
无论如何,使这项工作的技巧是创建我们的DefaultSimpUserRegistry
and实例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);
}
})
}
}
Now Spring is fully aware of the the authentication i.e. it injects the Principal
into any controller methods that require it, exposes it to the context for Spring Security 4.x, and associates the user to the WebSocket session for sending messages to specific users/sessions.
现在 Spring 完全了解身份验证,即将身份验证注入Principal
到任何需要它的控制器方法中,将其暴露给 Spring Security 4.x 的上下文,并将用户关联到 WebSocket 会话以向特定用户/会话发送消息。
Spring Security Messaging
Spring 安全消息传递
Lastly, if you use Spring Security 4.x Messaging support, make sure to set the @Order
of your AbstractWebSocketMessageBrokerConfigurer
to a higher value than Spring Security's AbstractSecurityWebSocketMessageBrokerConfigurer
(Ordered.HIGHEST_PRECEDENCE + 50
would work, as shown above). That way, your interceptor sets the Principal
before Spring Security executes its check and sets the security context.
最后,如果您使用 Spring Security 4.x 消息支持,请确保将@Order
您AbstractWebSocketMessageBrokerConfigurer
的值设置为高于 Spring Security 的值AbstractSecurityWebSocketMessageBrokerConfigurer
(Ordered.HIGHEST_PRECEDENCE + 50
如上所示)。这样,您的拦截器会Principal
在 Spring Security 执行其检查并设置安全上下文之前设置。
Creating a Principal (Update June 2018)
创建委托人(2018 年 6 月更新)
Lots of people seem to be confused by this line in the code above:
很多人似乎对上面代码中的这一行感到困惑:
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
This is pretty much out of scope for the question as it is not Stomp-specific, but I'll expand on it a little bit anyway, because its related to using auth tokens with Spring. When using token-based authentication, the Principal
you need will generally be a custom JwtAuthentication
class that extends Spring Security's AbstractAuthenticationToken
class. AbstractAuthenticationToken
implements the Authentication
interface which extends the Principal
interface, and contains most of the machinery to integrate your token with Spring Security.
这几乎超出了问题的范围,因为它不是 Stomp 特定的,但无论如何我都会对其进行扩展,因为它与在 Spring 中使用身份验证令牌有关。使用基于令牌的身份验证时,Principal
您通常需要一个JwtAuthentication
扩展 Spring SecurityAbstractAuthenticationToken
类的自定义类。AbstractAuthenticationToken
实现Authentication
扩展Principal
接口的接口,并包含将您的令牌与 Spring Security 集成的大部分机制。
So, in Kotlin code (sorry I don't have the time or inclination to translate this back to Java), your JwtAuthentication
might look something like this, which is a simple wrapper around AbstractAuthenticationToken
:
因此,在 Kotlin 代码中(抱歉,我没有时间或不想将其转换回 Java),您JwtAuthentication
可能看起来像这样,这是一个简单的包装器AbstractAuthenticationToken
:
import my.model.UserEntity
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
class 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
}
Now you need an AuthenticationManager
that knows how to deal with it. This might look something like the following, again in Kotlin:
现在你需要一个AuthenticationManager
知道如何处理它的人。在 Kotlin 中,这可能类似于以下内容:
@Component
class 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)
}
The injected TokenHandler
abstracts away the JWT token parsing, but should use a common JWT token library like jjwt. The injected AuthService
is your abstraction that actually creates your UserEntity
based on the claims in the token, and may talk to your user database or other backend system(s).
注入TokenHandler
的 JWT 令牌解析抽象出来,但应该使用像jjwt这样的通用 JWT 令牌库。注入的AuthService
是您的抽象,它UserEntity
根据令牌中的声明实际创建您的抽象,并且可能与您的用户数据库或其他后端系统进行对话。
Now, coming back to the line we started with, it might look something like this, where authenticationManager
is an AuthenticationManager
injected into our adapter by Spring, and is an instance of CustomTokenAuthenticationManager
we defined above:
现在,回到我们开始的那一行,它可能看起来像这样,其中authenticationManager
是AuthenticationManager
Spring 注入到我们的适配器中,并且是CustomTokenAuthenticationManager
我们上面定义的实例:
Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));
This principal is then attached to the message as described above. HTH!
然后,如上所述,将此主体附加到消息中。哼!
回答by alextunyk
With the latest SockJS 1.0.3 you can pass query parameters as a part of connection URL. Thus you can send some JWT token to authorize a session.
使用最新的 SockJS 1.0.3,您可以将查询参数作为连接 URL 的一部分传递。因此,您可以发送一些 JWT 令牌来授权会话。
var socket = new SockJS('http://localhost/ws?token=AAA');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/echo', function(data) {
// topic handler
});
}
}, function(err) {
// connection error
});
Now all the requests related to websocket will have parameter "?token=AAA"
现在所有与 websocket 相关的请求都会有参数“?token=AAA”
http://localhost/ws/info?token=AAA&t=1446482506843
http://localhost/ws/info?token=AAA&t=1446482506843
http://localhost/ws/515/z45wjz24/websocket?token=AAA
http://localhost/ws/515/z45wjz24/websocket?token=AAA
Then with Spring you can setup some filter which will identify a session using provided token.
然后使用 Spring,您可以设置一些过滤器,该过滤器将使用提供的令牌识别会话。
回答by DruidKuma
As of now, it is possible either to add auth token as a request parameter and handle it on a handshake, or add it as a header on a connection to stomp endpoint, and handle it on the CONNECT
command in the interceptor.
截至目前,可以将身份验证令牌添加为请求参数并在握手时处理它,或者将其添加为连接到 stomp 端点的标头,并CONNECT
在拦截器中的命令上处理它。
Best thing would be to use header, but the problem is that you can't access native header on the handshake step, so you wouldn't be able to handle the auth there then.
最好的办法是使用标头,但问题是您无法在握手步骤访问本机标头,因此您将无法在那里处理身份验证。
Let me give some example code:
让我举一些示例代码:
Config:
配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-test")
.setHandshakeHandler(new SecDefaultHandshakeHandler())
.addInterceptors(new HttpHandshakeInterceptor())
.withSockJS()
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new JwtChannelInterceptor())
}
}
Handshake interceptor:
握手拦截器:
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) {
attributes.put("token", request.getServletRequest().getParameter("auth_token")
return true
}
}
Handshake handler:
握手处理程序:
public class SecDefaultHandshakeHandler extends DefaultHandshakeHandler {
@Override
public Principal determineUser(ServerHttpRequest request, WebSocketHandler handler, Map<String, Object> attributes) {
Object token = attributes.get("token")
//handle authorization here
}
}
Channel Interceptor:
通道拦截器:
public class JwtChannelInterceptor implements ChannelInterceptor {
@Override
public void postSend(Message message, MessageChannel channel, Boolean sent) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class)
if (StompCommand.DISCONNECT == accessor.getCommand()) {
//retrieve Principal here via accessor.getUser()
//or get auth header from the accessor and handle authorization
}
}
}
Sorry for possible compile mistakes, I was converting manually from Kotlin code =)
对于可能的编译错误,我很抱歉,我是从 Kotlin 代码手动转换的 =)
As you mentioned that you have both web and mobile clients for your WebSockets, please mind that there are some difficulties maintaining same codebase for all clients. Please see my thread: Spring Websocket ChannelInterceptor not firing CONNECT event
正如您提到的,您的 WebSocket 既有 Web 客户端,也有移动客户端,请注意为所有客户端维护相同的代码库存在一些困难。请参阅我的主题:Spring Websocket ChannelInterceptor not fire CONNECT event