Spring 中的 Websocket 身份验证和授权

新手上路,请多包涵

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

问题

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

我想要的是

  • 使用登录名/密码对用户进行身份验证。
  • 防止匿名用户通过 WebSocket 连接。
  • 添加授权层(用户、管理员……)。
  • 在控制器中有 Principal 可用。

我不想要什么

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

原文由 Anthony Raymond 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.4k
2 个回答

如上所述,文档看起来不清楚(恕我直言),在 Spring 提供一些清晰的文档之前,这是一个样板文件,可以让您免于花两天时间试图了解安全链在做什么。

Rob-Leggett 做了一个非常好的尝试,但是,他正在 分叉一些 Springs 类,我觉得这样做不舒服。

开始前须知:

  • httpWebSocket安全链安全配置 是完全独立的。
  • Spring AuthenticationProvider 完全不参与 Websocket 身份验证。
  • 在我们的例子中,身份验证不会发生在 HTTP 协商端点上,因为我所知道的 JavaScripts STOMP (websocket) 库都不会随 HTTP 请求一起发送必要的身份验证标头。
  • 一旦在 CONNECT 请求上设置, 用户( simpUser ) 将被存储在 websocket 会话中,并且不再需要对进一步的消息进行身份验证。

Maven 部门

<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>

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("*")
    }
}

弹簧安全配置

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

然后,我们将创建一个负责对用户进行身份验证的服务。

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

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

几乎到此为止,现在我们需要创建一个拦截器,它将设置 `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 的情况下构建的,则身份验证将失败,因为没有授予权限的构造函数自动设置 authenticated = false ISPORTANT没有记录在 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);
    }

}

请注意: @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;
    }
}

原文由 Anthony Raymond 发布,翻译遵循 CC BY-SA 4.0 许可协议

对于 Java 客户端,请使用这个经过测试的示例:

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

原文由 tbw 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题