【问题标题】:Passing JWT token to SockJS将 JWT 令牌传递给 SockJS
【发布时间】:2019-09-21 17:03:59
【问题描述】:

当 SockJS 发生握手时,我需要发送令牌。我已经尝试了许多建议的实现,但称为相同的异常

java.lang.IllegalArgumentException: JWT String argument cannot be null or empty.

在后台WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
@CrossOrigin
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/socket");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket").withSockJS();
    }
}

尝试与套接字建立连接的函数。纯 JavaScript。

function connect() {
    var socket = new SockJS('http://localhost:8889/websocket',
             null,
            {
                transports: ['xhr-streaming'], 
                headers: {'Authorization': 'Bearer eyJhbGciOiJIUzUxMiJ9...' }
            });
    stompClient = Stomp.over(socket);
    stompClient.connect({},function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/socket/event', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}

问题在于握手,这些标头似乎没有正确传递令牌。我尝试了多种握手方式,但在我的情况下找不到正确的方式。

在我尝试在握手后使用标头之前,我从这里得到了实现的想法,但我发现它立即需要令牌。

https://github.com/sockjs/sockjs-client/issues/196#issuecomment-61469141

编辑:添加 WebSecurityConfig

@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
        .cors()
        .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
        .and()
        .csrf()
        .disable()
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeRequests()
        .antMatchers("/login/**").permitAll()
        .antMatchers("/websocket/**").permitAll()
        .anyRequest().authenticated();
        // Custom JWT based security filter
        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
        httpSecurity
        .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }//end configure(HttpSecurity)

创建身份验证令牌

@ApiOperation(value = "Login with the user credentials",
            response = JwtAuthenticationResponse.class)
    @ApiResponses(value = {
            @ApiResponse(code = 401, message = "Unauthorized"),
            @ApiResponse(code = 404, message = "Not Found",response = ExceptionResponse.class),
            @ApiResponse(code = 400, message = "Bad Request",response = ExceptionResponse.class),
            @ApiResponse(code = 200 , message = "OK", response = JwtAuthenticationResponse.class)
    })
    @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(
            @ApiParam(value = "User's email and password", required = true)
            @RequestBody JwtAuthenticationRequest authenticationRequest) 
            throws AuthenticationException {
        ResponseEntity<?> response;
        //authenticate the user
        final User user = userService.getByEmail(authenticationRequest.getEmail());
        try {
            authenticate(user.getUsername(), authenticationRequest.getPassword(),user.getId(),user.getAuthority().getName());
            // Reload password post-security so we can generate the token
            final UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
            final String token = jwtTokenUtil.generateToken(userDetails);
            // Return the token
            response  = ResponseEntity.ok(new JwtAuthenticationResponse(token,user.getUsername(),user.getFirstName(),user.getLastName(),
                    user.getEmail(),user.getId(),user.getAuthority().getName(),jwtTokenUtil.getExpirationTime(token)));
        }catch(NullPointerException e) {
            response = new ResponseEntity<>(new ExceptionResponse(404,"User Not Found","Authentication Failure"),HttpStatus.NOT_FOUND);
        }catch(AuthenticationException e) {
            response = new ResponseEntity<>(new ExceptionResponse(400,"Invalid E-mail or Password","Authentication Failure"),HttpStatus.BAD_REQUEST);
        }//end try
                return response;
    }//end createAuthenticationToken(JwtAuthenticationRequest)

堆栈跟踪(当握手和连接发生在与后端的 websocket 时,同样的异常被捕获了四次)。我将它添加到 pastebin 上,因为它会破坏帖子。

例外

2019-05-16 11:36:17.936  WARN 11584 --- [nio-8889-exec-9] a.d.s.JwtAuthorizationTokenFilter        : couldn't find bearer string, will ignore the header
2019-05-16 11:36:17.937 ERROR 11584 --- [nio-8889-exec-9] a.d.s.JwtAuthorizationTokenFilter        : an error occured during getting username from token

java.lang.IllegalArgumentException: JWT String argument cannot be null or empty.
    at io.jsonwebtoken.lang.Assert.hasText(Assert.java:135) ~[jjwt-0.9.0.jar:0.9.0]
    at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:479) ~[jjwt-0.9.0.jar:0.9.0]
    at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541) ~[jjwt-0.9.0.jar:0.9.0]
    at package.security.JwtTokenUtil.getAllClaimsFromToken(JwtTokenUtil.java:59) ~[classes/:na]
    at package.security.JwtTokenUtil.getClaimFromToken(JwtTokenUtil.java:52) ~[classes/:na]
    at package.security.JwtTokenUtil.getUsernameFromToken(JwtTokenUtil.java:34) ~[classes/:na]
    at package.security.JwtAuthorizationTokenFilter.extractUsername(JwtAuthorizationTokenFilter.java:79) [classes/:na]
    at package.security.JwtAuthorizationTokenFilter.doFilterInternal(JwtAuthorizationTokenFilter.java:44) [classes/:na]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ...
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    ...
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1457) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_201]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_201]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at java.lang.Thread.run(Unknown Source) [na:1.8.0_201]

【问题讨论】:

  • 在哪一行你得到IllegalArgumentException/你有异常的所有堆栈跟踪吗?
  • @smilyface 我在 WebSecurityConfig 上添加了配置方法。每次调用方法底部的addFilterBefore 时都会发生这种情况。这是有道理的,因为当我进行 HTTP 调用并且没有令牌时,也会发生同样的事情。但是使用 SockJS 我试图传递令牌,但它仍然没有到达后端。 websocket 通信没有任何问题。
  • 嗯,没关系。但是你没有回答我的问题(至少 10-15 行堆栈跟踪)
  • @smilyface 我在 pastebin 上添加了完整的堆栈跟踪。同样的例外已经被咳了四次。可能是标头触发了异常。
  • 还提供创建令牌的方法(如果可能,也提供示例令牌)。我想它可能类似于 .. Jwts.builder().something.. 来创建令牌。请在问题中也包括这一点

标签: javascript java spring-boot websocket sockjs


【解决方案1】:

用于注册自定义身份验证拦截器的服务器端配置。请注意,拦截器只需要在 CONNECT 消息上进行身份验证和设置用户标头。 Spring 记录并保存经过身份验证的用户,并将其与同一会话上的后续 STOMP 消息相关联。以下示例展示了如何注册自定义身份验证拦截器:

  @Configuration
    @EnableWebSocketMessageBroker
    public class MyConfig implements WebSocketMessageBrokerConfigurer {

        @Override
        public void configureClientInboundChannel(ChannelRegistration registration) {
            registration.interceptors(new ChannelInterceptor() {
                @Override
                public Message<?> preSend(Message<?> message, MessageChannel channel) {
                    StompHeaderAccessor accessor =
                            MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                    if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                        Authentication user = ... ; // access authentication header(s)
                        accessor.setUser(user);
                    }
                    return message;
                }
            });
        }
    }

另外,请注意,当您使用 Spring Security 的消息授权时,目前,您需要确保身份验证 ChannelInterceptor 配置在 Spring Security 之前排序。这最好通过在其自己的 WebSocketMessageBrokerConfigurer 实现中声明自定义拦截器来完成,该实现用@Order(Ordered.HIGHEST_PRECEDENCE + 99) 标记。

另一种方式: 同样,SockJS JavaScript 客户端不提供通过 SockJS 传输请求发送 HTTP 标头的方法。正如您可以看到 sockjs-client 问题 196。相反,它确实允许发送可用于发送令牌的查询参数,然后使用 Spring 您可以设置一些过滤器,该过滤器将使用提供的令牌识别会话。 ,但这有其自身的缺点(例如,令牌可能会无意中与服务器日志中的 URL 一起记录)。

Ref

【讨论】:

  • 我看到了查询解决方案,但不喜欢它,所以我将传递它。我知道授权标头应该在通信中,但是 Spring 在握手时需要一个令牌。我想现在我必须找到一种在握手发生时不会出现异常的方法。对套接字的安全方面不满意。
  • 不管怎样,既然赏金会丢失,我就把它给你。但我仍然将它留给未来,也许会出现一个解决方案或一个工作示例。
【解决方案2】:

Websocket 在 HTTP 标头中不遵循相同的模式。这就是为什么,即使您在标头中发送令牌,它也找不到。我之前遇到过同样的问题,我改变了 websocket 安全结构。

我的示例代码是这样的:

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.setInterceptors(new ChannelInterceptorAdapter() {

        @Override
        public Message<?> preSend(Message<?> message, MessageChannel channel) {
            StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
            MessageHeaders headers = message.getHeaders();
            SimpMessageType type = (SimpMessageType) headers.get("simpMessageType");
            List<String> tokenList = accessor.getNativeHeader("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))
            try{
                JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(new RawAccessJwtToken(tokenExtractor.extract(token)));
                Authentication yourAuth = jwtAuthenticationProvider.authenticate(jwtAuthenticationToken);
                accessor.setUser(yourAuth);
            } catch (Exception e) {
                throw new IllegalArgumentException(e.getMessage());
            }




            // not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
            accessor.setLeaveMutable(true);
            return MessageBuilder.createMessage(message.getPayload(), accessor.getMessageHeaders());
        }
    });

}

【讨论】:

  • 我根据您的实现实现了该方法,但遗憾的是该方法似乎没有被调用。也许我错过了一些东西。编辑:似乎它毕竟被调用了,我会更多地弄清楚它是如何工作的,然后会回来。
  • 那个方法是用于连接的,我们需要在握手时传递header。
  • 使用此代码,您将在每次发送消息时检查它。
  • 我知道并且很感激,但我正在寻找握手的东西。如果我设法找到握手的解决方法,我会回复你。
猜你喜欢
  • 1970-01-01
  • 2016-06-22
  • 2018-03-01
  • 2023-04-02
  • 2018-12-15
  • 2019-07-25
  • 2014-04-19
  • 2020-01-06
  • 2021-01-19
相关资源
最近更新 更多