Java Spring中的Websocket身份验证和授权

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/45405332/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-12 02:01:12  来源:igfitidea点击:

Websocket Authentication and Authorization in Spring

javaspringspring-bootauthenticationwebsocket

提问by Anthony Raymond

I've been struggling a lot to properly implement Stomp (websocket) Authenticationand Authorizationwith Spring-Security. For posterity i'll answer my own question to provide a guide.

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



The Problem

问题

Spring WebSocket documentation (for Authentication) looks unclear ATM (IMHO). And i couldn't understand how to properly handle Authenticationand Authorization.

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



What i want

我想要的是

  • Authenticate users with login/password.
  • Prevent anonymous users to CONNECT though WebSocket.
  • Add authorization layer (user, admin, ...).
  • Having Principalavailable in controllers.
  • 使用登录名/密码验证用户。
  • 防止匿名用户通过 WebSocket 连接。
  • 添加授权层(用户、管理员、...)。
  • Principal在控制器可用。



What i don't want

我不想要的

  • Authenticate on HTTP negotiation endpoints (since most of JavaScript libraries don't sends authentication headers along with the HTTP negotiation call).
  • 在 HTTP 协商端点上进行身份验证(因为大多数 JavaScript 库不随 HTTP 协商调用一起发送身份验证标头)。

采纳答案by Anthony Raymond

As stated above the documentation (ATM) is unclear, until Spring provide some clear documentation, here is a boilerplate to save you from spending two days trying to understand what the security chain is doing.

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

A really nice attempt was made by Rob-Leggettbut, he was forking some Springs classand i don't feel comfortable doing that.

Rob-Leggett进行了一次非常好的尝试,但是,他分叉了一些 Springs 课程,我觉得这样做不太舒服。

Things to know:

须知:

  • Security chainand Security configfor http and WebSocket are completely independent.
  • Spring AuthenticationProvidertake not part at all in Websocket authentication.
  • The authentication won't happend on HTTP negotiation endpoint because none of the JavaScripts STOMP (websocket) sends the authentication headres along with the HTTP request.
  • Once set on CONNECT request, the user(simpUser) will be stored in the websocket session and no more authentication will be required on further messages.
  • http 和 WebSocket 的安全链安全配置是完全独立的。
  • SpringAuthenticationProvider根本不参与 Websocket 身份验证。
  • 身份验证不会在 HTTP 协商端点上发生,因为 JavaScripts STOMP (websocket) 都不会随 HTTP 请求一起发送身份验证标头。
  • 一旦在 CONNECT 请求上设置,用户( simpUser) 将存储在 websocket 会话中,并且不再需要对进一步消息进行身份验证。

Maven deps

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

WebSocket 配置

The below config register a simple message broker (Note that it has nothing to do with authentication nor authorization).

下面的配置注册了一个简单的消息代理(请注意,它与身份验证或授权无关)。

@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 security config

Spring安全配置

Since the Stomp protocol rely on a first HTTP Request, we'll need to authorize HTTP call to our stomp handshake endpoint.

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


Then we'll create a service responsible for authenticating users.


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

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

Note that: UsernamePasswordAuthenticationTokenMUSThave GrantedAuthorities, if you use another constructor, Spring will auto-set isAuthenticated = false.

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


Almost there, now we need to create an Interceptor that will set the simpUserheader or throw AuthenticationExceptionon CONNECT messages.


差不多了,现在我们需要创建一个拦截器,它将设置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;
    }
}

Note that: preSend()MUSTreturn a UsernamePasswordAuthenticationToken, another element in the spring security chain test this. Note that: If your UsernamePasswordAuthenticationTokenwas built without passing GrantedAuthority, the authentication will fail, because the constructor without granted authorities auto set authenticated = falseTHIS IS AN IMPORTANT DETAIL which is not documented in spring-security.

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


Finally create two more class to handle respectively Authorization and 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);
    }

}

Note that: The @Orderis CRUCIALdon't forget it, it allows our interceptor to be registered first in the security chain.

需要注意的是:本@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;
    }
}

回答by tbw

for java client side use this tested example:

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

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