Spring中的Websocket身份验证和授权

Ant*_*ond 26 java authentication spring websocket spring-boot

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


问题

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


我想要的是

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


我不想要的

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

Ant*_*ond 49

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

Rob-Leggett做了一个非常好的尝试,但是他正在为一些斯普林特课程做点什么,我觉得这样做并不舒服.

要知道的事情:

  • http和WebSocket的安全链安全配置是完全独立的.
  • Spring AuthenticationProvider在Websocket身份验证中根本不参与.
  • 身份验证不会在HTTP协商端点上发生,因为JavaScripts STOMP(websocket)都不会发送身份验证标头和HTTP请求.
  • 一旦在CONNECT请求上设置,user(simpUser)将存储在websocket会话中,并且不再需要对其他消息进行身份验证.

Maven deps

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-messaging</artifactId>
</dependency>
Run Code Online (Sandbox Code Playgroud)

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("*")
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)


然后我们将创建一个负责验证用户的服务.

@Component
public class WebSocketAuthenticatorService {
    // This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
    public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String  username, final String password) throws AuthenticationException {
        if (username == null || username.trim().isEmpty()) {
            throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
        }
        if (password == null || password.trim().isEmpty()) {
            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
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

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


几乎在那里,现在我们需要创建一个Interceptor来设置simpUser标头或抛出AuthenticationExceptionCONNECT消息.

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:preSend() 必须返回一个UsernamePasswordAuthenticationToken,弹簧安全链测试中的另一个元素.请注意:如果您的UsernamePasswordAuthenticationToken构建没有通过GrantedAuthority,则身份验证将失败,因为没有授予权限的构造函数会自动设置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);
    }

}
Run Code Online (Sandbox Code Playgroud)

需要注意的是:本@Order至关重要的,不要忘记它,它让我们的拦截器在安全链中第一个进行注册.


@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

祝好运 !

  • 直到今天,你的帖子仍然是唯一有效的。Spring Boot 文档仍然缺乏明确的说明。多谢 (7认同)
  • 是的,“WebSocket 重用建立 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息”。确实如此,Websockets(stomp 也是如此)依赖于第一个 HTTP 协商调用,并且 spring 期望身份验证将在此处进行。但在此协商调用期间,没有一个 stomp JS 库转发凭据。因此,您需要在协商后进行身份验证。从而使用websocket进行身份验证。这就是为什么我说这两条链是解耦的。 (4认同)
  • 事实是,没有一个 javascript STOMP 库在 HTTP 握手调用中传递身份验证标头。Spring 选择只允许用户通过 HTTP 进行身份验证。但我们不能为此责怪他们,WebSocket RFC 在这个问题上不清楚并且非常宽容:*该协议没有规定服务器可以在 WebSocket 握手期间对客户端进行身份验证的任何特定方式。* (3认同)
  • Spring 描述的方法声明: *您应该在访问 HTTP necoiation 端点(握手端点)时提供足够的信息(登录密码或其他信息),以允许 Spring 通过 Spring-Security 链对您进行身份验证*。但是没有一个 javaScript STOMP 库确实将这些信息与 HTTP negiciation 调用一起发送。这些标头与 `nativeHeaders: { login: xxxx, passcode: xxxx}` 中的 `CONNECT` 消息一起发送。因此,如果在 HTTP 调用上没有发送任何信息,则此时您无法进行身份验证。 (3认同)
  • 引用 [Spring Security 指南](https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html)“_更具体地说,确保用户已通过 WebSocket 身份验证应用程序中,所需要做的就是确保您设置 Spring Security 来验证基于 HTTP 的 Web 应用程序。_”因此,重点是,您使用标准 Spring Security 方法验证对“http”端点的访问,然后验证 CSRF在“CONNECT”上,并在配置的 STOMP 目标上使用基于角色的安全性。我仍然不确定上述的用例。 (2认同)