【问题标题】:Websocket Authentication and Authorization in SpringSpring中的Websocket身份验证和授权
【发布时间】:2018-01-06 09:40:01
【问题描述】:

我一直在努力使用 Spring-Security 正确实现 Stomp (websocket) AuthenticationAuthorization为了后代,我会回答我自己的问题以提供指导。


问题

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


我想要什么

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

我不想要的

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

【问题讨论】:

  • 很棒的文章。将身份验证进一步推迟到第一个 SEND 帧的处理,这是一种更糟糕的方法吗?与 CONNECT 框架相反。我尚不清楚它可能产生的任何好处,但与您在答案中描述的方式相比,这是否可能是否定的?
  • ...首先发送或订阅,事实上
  • 恕我直言,最好在 CONNECT 上进行(以及支持 v1.2 的 STOMP),因为它是一个通用入口点,而不是 SEND、SUBSCRIBE、BEGIN 或任何其他可能在未来 RFC 中引入的 Frame

标签: java spring spring-boot authentication websocket


【解决方案1】:

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

Rob-Leggett 做了一个很好的尝试,但他是forking some Springs class,我觉得这样做不太舒服。

开始之前需要了解的事项:

  • 安全链httpWebSocket安全配置是完全独立的。
  • Spring AuthenticationProvider 根本不参与 Websocket 身份验证。
  • 在我们的例子中,身份验证不会发生在 HTTP 协商端点上,因为我所知道的任何 JavaScript 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("*")
    }
}

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

注意: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,弹簧安全链中的另一个元素对此进行测试。 请注意:如果您的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);
    }

}

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

【讨论】:

  • 引用Spring Security guide "更具体地说,要确保用户已通过您的 WebSocket 应用程序进行身份验证,所需要做的就是确保您设置 Spring Security 来验证基于 HTTP 的 Web应用程序。”因此,重点是,您使用标准 Spring Security 方法验证对 http 端点的访问,然后在 CONNECT 上验证 CSRF 并在配置的 STOMP 目标上使用基于角色的安全性。我仍然不确定上述用例。
  • 事实上,没有一个 javascrip STOMP 库将身份验证标头与 HTTP 握手调用一起传递。 Spring 选择允许用户仅通过 HTTP 进行身份验证。但我们不能为此责怪他们,WebSocket RFC 在这个主题上并不清楚并且非常宽容:该协议没有规定服务器可以在 WebSocket 握手期间对客户端进行身份验证的任何特定方式。
  • Spring 描述的方法状态是: 您应该在访问 HTTP 协商端点(握手端点)时提供足够的信息(登录密码或其他),以允许 Spring 通过 Spring-Security 链对您进行身份验证。但是没有一个 javaScript STOMP 库会在 HTTP 协商调用中发送这些信息。这些标头在nativeHeaders: { login: xxxx, passcode: xxxx} 中与CONNECT 消息一起发送。因此,如果 HTTP 调用没有发送任何信息,则此时您无法进行身份验证。
  • 是的WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made。没错,Websockets(stomp 也是如此)依赖于第一个 HTTP 协商调用,并且 spring 期望身份验证将在此处进行。但是没有一个单一的 stomp JS 库在此协商调用期间转发凭据。因此,您需要在协商后进行身份验证。因此使用 websocket 进行身份验证。这就是为什么我说这两条链是解耦的。
  • 直到今天,您的帖子仍然是唯一有效的方法。 Spring Boot 文档仍然缺乏明确的说明。非常感谢
【解决方案2】:

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

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

【讨论】:

  • 这也适用于 stomp.js。但这些是 STOMP 标头(在 CONNECT 框架上)。最初的问题是 stomp.js 不会设置 Spring Security 用来自动认证的 HTTP websocket-handshake headers。
【解决方案3】:

使用 spring 身份验证很痛苦。你可以用一种简单的方式做到这一点。创建一个网络过滤器并自己读取授权令牌,然后执行身份验证。

@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() {
    }
}

然后在您的配置中使用弹簧机制定义过滤器:

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

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

【讨论】:

  • 我同意 spring 安全性还有很长的路要走,但是这样做你失去了 spring-security 带来的所有便利(能够从请求上下文中的任何地方获取用户,测试模拟,.. .)
  • 复杂性没有方便。在我的方法中,您仍然可以注入您的服务并检查用户名和密码。例如,您可以注入 WebSocketAuthenticatorService 并通过拆分授权进行检查,如果这是基本身份验证,则获取用户名传递。只要可行,这里没有正确或错误的解决方案,我更喜欢这个,因为它对我来说更优雅。其他人可能更喜欢其他人。
猜你喜欢
  • 2011-11-19
  • 2013-09-30
  • 2011-07-17
  • 2010-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多