当前位置: 首页 > 面试题库 >

带有基于Spring的SockJS / STOMP Web套接字的JSON Web令牌(JWT)

翁宜年
2023-03-14
问题内容

背景
我正在使用Spring Boot(1.3.0.BUILD-SNAPSHOT)设置一个RESTful Web应用程序,该应用程序包括一个STOMP / SockJS WebSocket,我打算从iOS应用程序和Web浏览器中使用它。我想使用JSON Web令牌(JWT)来保护REST请求和WebSocket接口,但后者遇到了困难。

该应用程序受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);
    }

}

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();
    }

}

我还有一个子类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;
    }

}

还有两个带@RestController注释的类可以处理各种功能,并且可以通过JWTTokenFilter在我的WebSecurityConfiguration课程中注册的方式成功保护它们。

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

在Spring Security进行的WebSockets文档指出的是,AbstractSecurityWebSocketMessageBrokerConfigurer确保:

任何入站CONNECT消息均需要有效的CSRF令牌来实施“同源起点策略”

这似乎暗示着初始握手应该是不安全的,并且在接收到STOMP CONNECT消息时将调用身份验证。不幸的是,我似乎找不到任何有关执行此操作的信息。另外,此方法将需要其他逻辑来断开打开WebSocket连接且从不发送STOMP CONNECT的流氓客户端的连接。

作为Spring的(新手)我也不确定Spring Sessions是否适合于此。尽管文档非常详细,但似乎没有一个很好的,简单的(又称白痴)指南来说明各个组件如何组合在一起/如何相互影响。


问题答案:

我发现了一种在测试中效果很好的黑客程序。绕过内置的Spring连接级Spring auth机制。相反,可以通过在客户端的Stomp标头中发送身份验证令牌来在消息级别设置身份验证令牌(这很好地反映了常规HTTP XHR调用已在执行的操作),例如:

stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);

服务器端,使用以下命令从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)
    }
  })

这很简单,可以让我们达到85%的方式,但是,这种方法不支持向特定用户发送消息。这是因为Spring的将用户关联到会话的机制不受的结果影响ChannelInterceptor。Spring WebSocket假定身份验证是在传输层而不是消息层完成的,因此忽略了消息级身份验证。

使这项工作反正砍,就是创造我们的情况DefaultSimpUserRegistryDefaultUserDestinationResolver,揭露那些环境,然后用拦截器来更新这些仿佛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的值AbstractSecurityWebSocketMessageBrokerConfigurerOrdered.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.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
}

现在你需要一个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)
}

注入的内容将TokenHandler抽象出JWT令牌解析,但应使用通用的JWT令牌库,如jjwt。注入的内容AuthService是你的抽象,它实际上是UserEntity根据令牌中的声明创建你的抽象的,并且可以与你的用户数据库或其他后端系统对话。

现在,回到我们开始与线,它可能是这个样子,哪里authenticationManager是一个AuthenticationManager注入由spring我们适配器,是一个实例CustomTokenAuthenticationManager上面定义我们:

Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));

然后,将此主体附加到消息中,如上所述。HTH!



 类似资料:
  • 我们正在将SpringWebSockets集成到我们的应用程序中,我运行了HelloWorld示例,令人惊讶的是,spring为我们连接了一切,以便将服务器端通知推送到客户端。 不过,我有一些简单的问题 1) 队列是如何创建的?我使用的是ActiveMQ,队列名称与我在目的地中指定的不同(例如,像greetings-user3n9\u jn3i)。 2)目标名称是否不同于队列? 3) 我正在使用A

  • 问题内容: 老派:我正在使用Java SE 5(或Javav1.5)(请不要告诉我进行升级,因为对于我正在从事的工作[私有],我需要使用此版本的Java )。 我需要设置Web客户端/服务器应用程序的帮助。我在网上寻找的每个变体都缩小了使用websockets /sockets的范围,但是我使用的Java版本还没有套接字。 有没有人有不使用套接字来设置客户端/服务器模型应用程序的教程,或者一些示例

  • 问题内容: 我有一个REST API,我正在使用Spring Security基本授权进行身份验证,客户端会为每个请求发送用户名和密码。现在,我想实现基于令牌的身份验证,当用户最初通过身份验证时,我将在响应标头中发送令牌。对于进一步的请求,客户端可以在令牌中包含该令牌,该令牌将用于对资源进行用户身份验证。我有两个身份验证提供程序tokenAuthenticationProvider和daoAuth

  • 在daoAuthenticationProvider中,我设置了自定义userDetailsService并通过从数据库中获取用户登录详细信息对其进行身份验证(只要使用授权传递用户名和密码就可以正常工作:basic bgllqxbpvxnlcjogn21wxidmqjrdturtr04pag==作为头) 但是当我使用X-AUTH-TOKEN(即constants.auth_header_name)

  • 问题内容: 有没有一种方法可以将WebSocket与SockJS客户端和Spring 4服务器一起使用,而不能与STOMP一起使用? 基于Spring网站上的本教程,我知道如何使用Stomp和Spring 4设置基于WebSocket的应用程序。在客户端,我们有: 在服务器端,控制器中包含以下内容: 现在,我了解可以确保如果将消息发送到目的地,则将调用该方法。并且由于已订阅,因此会将邮件发送回。