Spring中的WebSocket认证和授权

lvmkulzt  于 2023-06-21  发布在  Spring
关注(0)|答案(4)|浏览(193)

我一直在努力用Spring-Security正确实现Stomp(WebSocket)AuthenticationAuthorization。* 我将回答自己的问题,为后人提供指引 *
问题
Spring WebSocket文档(用于身份验证)看起来不清楚ATM(恕我直言)。我不知道如何正确处理身份验证授权
我想要的

  • 使用登录名/密码验证用户。
  • 防止匿名用户通过WebSocket连接。
  • 添加授权层(user,admin,...)。
  • 在控制器中提供Principal

我不想要什么

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

0x6upsns1#

如上所述,文档看起来不清楚(恕我直言),直到Spring提供一些清晰的文档,这里有一个样板文件,可以保存您花两天时间试图理解安全链正在做什么。
一个非常好的尝试是由Rob-Leggett,但他是forking some Springs class,我不觉得这样做很舒服。
开始之前需要知道的事情:

*httpWebSocket的 * 安全链 * 和 * 安全配置 * 完全独立。

  • Spring AuthenticationProvider完全不参与WebSocket身份验证。
  • 在我们的例子中,身份验证不会发生在HTTP协商端点上,因为我所知道的JavaScripts STOMP(WebSocket)库中没有一个会在HTTP请求的沿着发送必要的身份验证头。
  • 一旦在CONNECT请求上设置,usersimpUser)将被存储在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>

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

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

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

注意:UsernamePasswordAuthenticationTokenMUST至少有一个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()MUST返回一个UsernamePasswordAuthenticationToken,spring安全链中的另一个元素对此进行测试。注意:如果你的UsernamePasswordAuthenticationToken是在没有通过GrantedAuthority的情况下构建的,那么身份验证将失败,因为没有授权的构造函数会自动设置authenticated = false这是一个重要的细节,spring-security中没有记录
最后再创建两个类来分别处理Authorization和Authentication。

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

wj8zmpe12#

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

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

tv6aics13#

使用Spring身份验证是一种痛苦。你可以用一种简单的方法来做。创建Web过滤器并自行读取授权令牌,然后执行身份验证。

@Component
public class CustomAuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String authorization = request.getHeader("Authorization");
            if (/*Your condition here*/) {
                // logged
                filterChain.doFilter(servletRequest, servletResponse);
            } else {
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.getWriter().write("{\"message\": "\Bad login\"}");
            }
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

然后在配置中使用Spring机制定义过滤器:

@Configuration
public class SomeConfig {
    @Bean
    public FilterRegistrationBean<CustomAuthenticationFilter> securityFilter(
            CustomAuthenticationFilter customAuthenticationFilter){
        FilterRegistrationBean<CustomAuthenticationFilter> registrationBean
                = new FilterRegistrationBean<>();

        registrationBean.setFilter(customAuthenticationFilter);
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}
vql8enpb

vql8enpb4#

如果你使用的是this Stomp Client library for Android,那么你实际上可以在初始握手请求中添加Http头。
下面是一个例子:

Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Your auth token");
    
StompClient stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, 
        "ws://example.com/api/websocket", headers);

相关问题