Spring中的Websocket身份validation和授权

我一直在努力用Spring-Security正确实现Stomp(websocket) 身份validation授权对于后代,我会回答我自己的问题,提供指导。

问题

Spring WebSocket文档(用于身份validation)看起来不清楚ATM(恕我直言)。 我无法理解如何正确处理身份validation授权

我想要的是

  • 使用登录名/密码validation用户。
  • 防止匿名用户通过WebSocket进行连接。
  • 添加授权层(用户,管理员,…)。
  • Principal可以在控制器中使用。

我不想要的

  • 在HTTP协商端点上进行身份validation(因为大多数JavaScript库不会与HTTP协商调用一起发送身份validation标头)。

如上所述,文档(ATM)尚不清楚,直到Spring提供了一些明确的文档,这里有一个样板,可以帮助您节省两天时间,试图了解安全链正在做什么。

Rob-Leggett做了一个非常好的尝试,但是他正在分配一些Springs课程 , 你应该尽可能地避免这种情况

要知道的事情:

  • http和WebSocket的安全链安全配置是完全独立的。
  • Spring AuthentionProvider在Websocket身份validation中完全不参与。
  • 一旦设置为CONNECT请求,将存储用户simpUser ),并且不再需要进一步的身份validation。

Maven deps

  org.springframework.boot spring-boot-starter-websocket   org.springframework spring-messaging   org.springframework.boot spring-boot-starter-security   org.springframework.security spring-security-messaging  

WebSocket配置

下面的配置注册了一个简单的消息代理(注意它与认证和授权无关)。

 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(final MessageBrokerRegistry config) { // These are endpoints the client can subscribes to. config.enableSimpleBroker("/queue/topic"); // Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(final StompEndpointRegistry registry) { // Handshake endpoint registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*") } } 

Spring安全配置

由于Stomp协议依赖于第一个HTTP请求,因此我们需要授权对我们的stomp握手端点进行HTTP调用。

 @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(final HttpSecurity http) throws Exception { // This is not for websocket authorization, and this should most likely not be altered. http .httpBasic().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests().antMatchers("/stomp").permitAll() .anyRequest().denyAll(); } } 

然后我们将创建一个负责validation用户的服务。

 @Component public class WebSocketAuthenticatorService { // This method MSUT return a UsernamePasswordAuthenticationToken, another component in the security chain is testing it with 'instanceof' public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException { if (username == null || username.trim().length()) { throw new AuthenticationCredentialsNotFoundException("Username was null or empty."); } if (password == null || password.trim().length()) { throw new AuthenticationCredentialsNotFoundException("Password was null or empty."); } // Add your own logic for retrieving user in fetchUserFromDb() if (fetchUserFromDb(username, password) == null) { throw new BadCredentialsException("Bad credentials for user " + username); } // null credentials, we do not pass the password along return new UsernamePasswordAuthenticationToken( username, null, Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role ); } } 

注意: UsernamePasswordAuthenticationToken 必须有GrantedAuthorities,如果你使用另一个构造函数,Spring将自动设置isAuthenticated = false

几乎在那里,现在我们需要创建一个Interceptor,它将设置simpUser头或在CONNECT消息上抛出AuthenticationException

 @Component public class AuthChannelInterceptorAdapter extends ChannelInterceptor { private static final String USERNAME_HEADER = "login"; private static final String PASSWORD_HEADER = "passcode"; private final WebSocketAuthenticatorService webSocketAuthenticatorService; @Inject public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) { this.webSocketAuthenticatorService = webSocketAuthenticatorService; } @Override public Message preSend(final Message message, final MessageChannel channel) throws AuthenticationException { final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT == accessor.getCommand()) { final String username = accessor.getFirstNativeHeader(USERNAME_HEADER); final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER); final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password); accessor.setUser(user); } return message; } } 

注意: preSend() 必须返回一个UsernamePasswordAuthenticationToken ,Spring安全链中的另一个元素测试这个。 请注意:如果您的UsernamePasswordAuthenticationToken是在未传递GrantedAuthority情况下构建的,则身份validation将失败,因为没有授予权限的构造函数自动设置authenticated = false 这是一个重要的详细信息,未在spring-security中记录

最后再创建两个类来分别处理授权和认证。

 @Configuration @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer { @Inject private AuthChannelInterceptorAdapter authChannelInterceptorAdapter; @Override public void registerStompEndpoints(final StompEndpointRegistry registry) { // Endpoints are already registered on WebSocketConfig, no need to add more. } @Override public void configureClientInboundChannel(final ChannelRegistration registration) { registration.setInterceptors(authChannelInterceptorAdapter); } } 

注意: @OrderCRUCIAL不要忘记它,它允许我们的拦截器首先在安全链上注册。

 @Configuration public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) { // You can customize your authorization mapping here. messages.anyMessage().authenticated(); } // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint. @Override protected boolean sameOriginDisabled() { return true; } } 

祝你好运 !

对于java客户端,请使用此测试示例:

 StompHeaders connectHeaders = new StompHeaders(); connectHeaders.add("login", "test1"); connectHeaders.add("passcode", "test"); stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler);