Spring中的Websocket身份验证和授权

男娘i 2023-10-14 23:01 556阅读 0赞

一、需要了解的事项

  • http和WebSocket的安全链和安全配置是完全独立的。
  • SpringAuthenticationProvider根本不参与 Websocket 身份验证。
  • 将要给出的示例中,身份验证不会发生在 HTTP 协商端点上,因为 JavaScript STOMP(websocket)库不会随 HTTP 请求一起发送必要的身份验证标头。
  • 一旦在 CONNECT 请求上设置,用户( simpUser) 将被存储在 websocket 会话中,并且以后的消息将不再需要进行身份验证。

二、依赖

  1. java复制代码<dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework</groupId>
  7. <artifactId>spring-messaging</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-security</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.springframework.security</groupId>
  15. <artifactId>spring-security-messaging</artifactId>
  16. </dependency>

三、WebSocket 配置

3.1 、简单的消息代理

  1. java复制代码@Configuration
  2. @EnableWebSocketMessageBroker
  3. public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
  4. @Override
  5. public void configureMessageBroker(final MessageBrokerRegistry config) {
  6. config.enableSimpleBroker("/queue/topic");
  7. config.setApplicationDestinationPrefixes("/app");
  8. }
  9. @Override
  10. public void registerStompEndpoints(final StompEndpointRegistry registry) {
  11. registry.addEndpoint("stomp");
  12. setAllowedOrigins("*")
  13. }
  14. }

3.2 、Spring安全配置

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

  1. java复制代码@Configuration
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(final HttpSecurity http) throws Exception
  5. http.httpBasic().disable()
  6. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
  7. .authorizeRequests().antMatchers("/stomp").permitAll()
  8. .anyRequest().denyAll();
  9. }
  10. }

然后创建一个负责验证用户身份的服务。

  1. java复制代码@Component
  2. public class WebSocketAuthenticatorService {
  3. public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException {
  4. if (username == null || username.trim().isEmpty()) {
  5. throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
  6. }
  7. if (password == null || password.trim().isEmpty()) {
  8. throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
  9. }
  10. if (fetchUserFromDb(username, password) == null) {
  11. throw new BadCredentialsException("Bad credentials for user " + username);
  12. }
  13. return new UsernamePasswordAuthenticationToken(
  14. username,
  15. null,
  16. Collections.singleton((GrantedAuthority) () -> "USER") // 必须给至少一个角色
  17. );
  18. }
  19. }

接着需要创建一个拦截器,它将设置“simpUser”标头或在 CONNECT 消息上抛出“AuthenticationException”。

  1. java复制代码@Component
  2. public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
  3. private static final String USERNAME_HEADER = "login";
  4. private static final String PASSWORD_HEADER = "passcode";
  5. private final WebSocketAuthenticatorService webSocketAuthenticatorService;
  6. @Inject
  7. public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
  8. this.webSocketAuthenticatorService = webSocketAuthenticatorService;
  9. }
  10. @Override
  11. public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
  12. final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
  13. if (StompCommand.CONNECT == accessor.getCommand()) {
  14. final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
  15. final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);
  16. final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);
  17. accessor.setUser(user);
  18. }
  19. return message;
  20. }
  21. }

请注意:preSend() 必须返回 UsernamePasswordAuthenticationToken,Spring 安全链中会对此进行测试。如果UsernamePasswordAuthenticationToken构建没有通过GrantedAuthority,则身份验证将失败,因为没有授予权限的构造函数自动设置authenticated = false 这是一个重要的细节,在 spring-security 中没有记录。

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

  1. java复制代码@Configuration
  2. @Order(Ordered.HIGHEST_PRECEDENCE + 99)
  3. public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer {
  4. @Inject
  5. private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
  6. @Override
  7. public void registerStompEndpoints(final StompEndpointRegistry registry) {
  8. // 这里不用给任何东西
  9. }
  10. @Override
  11. public void configureClientInboundChannel(final ChannelRegistration registration) {
  12. registration.setInterceptors(authChannelInterceptorAdapter);
  13. }
  14. }

请注意:这@Order是至关重要的,它允许我们的拦截器首先在安全链中注册。

  1. java复制代码@Configuration
  2. public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
  3. @Override
  4. protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
  5. // 添加自己的映射
  6. messages.anyMessage().authenticated();
  7. }
  8. // 这里请自己按需求修改
  9. @Override
  10. protected boolean sameOriginDisabled() {
  11. return true;
  12. }
  13. }

之后编写客户端进行连接,我们就可以这样指定客户端进行消息的发送。

  1. java复制代码 @MessageMapping("/greeting")
  2. public void greetingReturn(@Payload Object ojd){
  3. simpMessagingTemplate.convertAndSendToUser(username,"/topic/greeting",ojd);
  4. }

发表评论

表情:
评论列表 (有 0 条评论,556人围观)

还没有评论,来说两句吧...

相关阅读

    相关 身份验证

    传统身份验证的方法 HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端