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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 00:38:57  来源:igfitidea点击:

JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket

springspring-securitywebsocketjwtsockjs

提问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 AbstractSecurityWebSocketMessageBrokerConfigurerto 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 @RestControllerannotated classes to handle various bits of functionality and these are secured successfully via the JWTTokenFilterregistered in my WebSecurityConfigurationclass.

还有几个带@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 /infoand/or handshake requests.

但是我似乎无法使用 JWT 来保护 WebSocket。我在浏览器中使用SockJS 1.1.0STOMP 1.7.1,但不知道如何传递令牌。它看来,SockJS不允许参数与最初的发送/info和/或握手请求。

The Spring Security for WebSockets documentation statesthat the AbstractSecurityWebSocketMessageBrokerConfigurerensures 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 determineUsermethod 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 DefaultSimpUserRegistryand 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:

无论如何,使这项工作的技巧是创建我们的DefaultSimpUserRegistryand实例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 Principalinto 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 @Orderof your AbstractWebSocketMessageBrokerConfigurerto a higher value than Spring Security's AbstractSecurityWebSocketMessageBrokerConfigurer(Ordered.HIGHEST_PRECEDENCE + 50would work, as shown above). That way, your interceptor sets the Principalbefore Spring Security executes its check and sets the security context.

最后,如果您使用 Spring Security 4.x 消息支持,请确保将@OrderAbstractWebSocketMessageBrokerConfigurer的值设置为高于 Spring Security 的值AbstractSecurityWebSocketMessageBrokerConfigurerOrdered.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 Principalyou need will generally be a custom JwtAuthenticationclass that extends Spring Security's AbstractAuthenticationTokenclass. AbstractAuthenticationTokenimplements the Authenticationinterface which extends the Principalinterface, 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 JwtAuthenticationmight 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 AuthenticationManagerthat 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 TokenHandlerabstracts away the JWT token parsing, but should use a common JWT token library like jjwt. The injected AuthServiceis your abstraction that actually creates your UserEntitybased 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 authenticationManageris an AuthenticationManagerinjected into our adapter by Spring, and is an instance of CustomTokenAuthenticationManagerwe defined above:

现在,回到我们开始的那一行,它可能看起来像这样,其中authenticationManagerAuthenticationManagerSpring 注入到我们的适配器中,并且是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 CONNECTcommand 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