在 WebSocket 控制器中获取登录用户的主体

问题描述 投票:0回答:2

我有一个 Spring Boot 应用程序,它使用 Spring Security 来使用 JWT 保护端点,这对于标准 REST 控制器来说都可以正常工作。 该应用程序还具有 WebSocket 连接,我已使用相同的 JWT 成功保护了该连接。

问题

如果我尝试将Principal注入到WebSocket控制器方法中,则Principal默认为null。我发现了许多通过覆盖 DefaultHandshakeHandler 来设置主体的解决方案,但是大多数解决方案都会生成随机主体名称。我宁愿使用现有登录用户名来生成主体名称,以便我可以看到哪个用户正在向 WebSocket 发送数据并相应地处理响应。

这里有一个回复,我认为通过添加可以让我更接近,但是当将逻辑应用到我的 preSend 方法时,即使我将其添加为我的 StompJS 连接方法的参数,我也会得到一个空用户名: WebSocket Stomp over SockJS - http 自定义标头

我还觉得奇怪的是,在拦截器本身内,我能够使用正确的用户名访问主体,但是一旦它到达控制器,它要么为空,要么为 preSend 握手方法中定义的任何内容。

以下是连接 WebSocket 的方法:

  _connect() {
    console.log("Initialize WebSocket Connection");
    if (this.authenticationService.isUserLoggedIn()) {
      let ws = new SockJS(this.webSocketEndPoint);
      this.stompClient = Stomp.over(ws);
      const _this = this;
      _this.stompClient.connect({
        'Authorization': "Bearer " + this.authenticationService.getLoggedInUserToken(),
        'username': this.authenticationService.getLoggedInUserName()
      }, function (frame) {
          _this.stompClient.subscribe(_this.board, function (data) {
              _this.onDataReceived(data);
          });
      }, this.errorCallBack);
    } else {
      console.log("Unable to connect: must be logged in.");
    }
  }

这是我当前的握手代码,带有注释结果:

    public class MyHandshakeHandler extends DefaultHandshakeHandler {
        @Override
        protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, 
                                          Map<String, Object> attributes) {
            Principal principal = SecurityContextHolder.getContext().getAuthentication();
            System.out.println("PRINCIPAL HANDSHAKE START:" + principal); //prints annonymous user
            final String ATTR_PRINCIPAL = "__principal__";
            
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
            String username = httpServletRequest.getParameter("username");
            
            //System.out.println("Username is: " + username); // null
            final String name;
            if (!attributes.containsKey(ATTR_PRINCIPAL)) {
                name = "My made up name"; //Would like this to be the logged in user, but is currently null
                attributes.put(ATTR_PRINCIPAL, name);
            } else {
                name = (String) attributes.get(ATTR_PRINCIPAL);
            }
            return new Principal() {
                @Override
                public String getName() {
                    return name;
                }
            };
        }
    }

最后是我的拦截器,它授权 WebSocket 请求并显示正确的登录用户:

@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99) 
public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private UserService userService;

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                Principal principal = SecurityContextHolder.getContext().getAuthentication();
                System.out.println("PRINCIPAL PRE-SEND START:" + principal); // shows annonymous user
                
                StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
                List<String> tokenList = accessor.getNativeHeader("Authorization");
                String jwt = null;
                if (tokenList == null || tokenList.size() < 1) {
                  return message;
                } else {
                    jwt = tokenList.get(0).substring(7);
                  if (jwt == null) {
                    return message;
                  }
                }
                String username = jwtUtil.extractUsername(jwt); //Decoder class that can retrieve username from token
                
                UserDetails userDetails = userService.loadUserByUsername(username);
                if (jwtUtil.validateToken(jwt, userDetails)) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = 
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                    
                    //Used only to see which principal is found at this stage
                    principal = SecurityContextHolder.getContext().getAuthentication();
                    System.out.println("PRINCIPAL PRE-SEND END:" + principal); // shows correct logged in user
                    
                    accessor.setUser(authentication);
                }
                return message;
            }
        });
    }
}

这是当前正在为主体测试的控制器方法:

    @MessageMapping("/test")
    public void test(@Payload String message, Principal principal) throws Exception {
        System.out.println("PRINCIPAL TEST INJECTED: " + principal.getName());
        Principal retrievedPrincipal = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("PRINCIPAL TEST RETRIEVED: " + retrievedPrincipal); //null
        webSocket.convertAndSend("/topic/test", new ResponseMessage("TestSuccess", "Thanks for the request " + principal.getName())); // shows "made up username" from handshake
    }

以下是名为“example”的登录用户连接到 WebSocket 并尝试访问 /app/test 端点时的控制台输出:

PRINCIPAL HANDSHAKE START:AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
PRINCIPAL PRE-SEND START:null
PRINCIPAL PRE-SEND END:UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=example, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
PRINCIPAL PRE-SEND START:null
PRINCIPAL PRE-SEND START:null
PRINCIPAL TEST INJECTED: My made up name
PRINCIPAL TEST RETRIEVED: SecurityContextImpl [Null authentication]

为了清楚起见,如果您需要任何进一步的代码,请告诉我。

java spring-boot spring-security websocket sockjs
2个回答
2
投票

这是我经过大量研究后所做的。 我通过搜索 google @MessageMapping Principal is null 作为查询到达这个位置(

https://coderedirect.com/questions/310917/secure-spring-webscoket-using-spring-security-and-access-principal-from-websocke
)看看我们的实现有什么不同,我发现而不是

StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

当我尝试时

StompHeaderAccessor accessor = MessageHeaderAccessor
                               .getAccessor(message, StompHeaderAccessor.class);
控制器内的

Principal
现在不为空。 我不确定
spring-messaging
MessageHeaderAccessor

的深度

我搜索的初始线程的更多参考是JSON Web Token(JWT)与基于Spring的SockJS / STOMP Web Socket最终到达主要答案的真相来源

https://github.com/spring-projects/spring-framework/blob/main/src/docs/asciidoc/web/websocket.adoc#token-authentication


0
投票

你知道为什么我检索到的委托人总是为空,而注入的委托人却不是...... 好像在preSend方法中进入了4次。前2次, 主要预发送开始:不为空

最后两次为空 主要预发送开始:null

© www.soinside.com 2019 - 2024. All rights reserved.