使用 spring 安全性的 Spring websocket 身份验证:401 错误

Dav*_*ral 2 spring-security http-status-code-401 spring-websocket

我想使用spring security. 从spring 官方文档 中 23.2 WebSocket Authentication,WebSocket 将重用在建立 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。所以我设置了 spring security 来验证rest service. 如果用户通过了其余的认证,则拥有WebSocket连接的权限,否则无法建立WebSocket连接。以下是代码:

用于登录的休息服务:WssAuthService.java

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * Authentication service.
 */
@RestController
@RequestMapping(path = "/hpdm")
public class WssAuthService {

    @RequestMapping(path = "/login", method = RequestMethod.GET)
    public String login(){
        return "Login success to WssBroker...";
    }
}
Run Code Online (Sandbox Code Playgroud)

spring 安全配置:WebSecurityConfig.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public final static String REALM="MY_TEST_REALM";

    @Autowired
    public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN")
        .and().withUser("test").password("test").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and().httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint())
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//We don't need sessions to be created.
    }

    @Bean
    public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
        return new CustomBasicAuthenticationEntryPoint();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // altough this seems like useless code,
        // its required to prevend spring boot auto-configuration
        return super.authenticationManagerBean();
    }

}
Run Code Online (Sandbox Code Playgroud)

websocket 服务器配置:WssBrokerConfig.java

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WssBrokerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer  {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
            .nullDestMatcher().authenticated()
            .simpSubscribeDestMatchers("/topic/notification").permitAll()
            .simpDestMatchers("/**").authenticated()
//            .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER")
//            .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll()
            .anyMessage().denyAll();

    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/ws");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        /**Note: setAllowedOrigins is important here: since we have both http & websocket servers, cross-origin accesses should be enabled */
        registry.addEndpoint("/hpdm-ws").setAllowedOrigins("*").withSockJS();
    }

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        MappingJackson2HttpMessageConverter converter =
                new MappingJackson2HttpMessageConverter(mapper);
        return converter;
    }

}
Run Code Online (Sandbox Code Playgroud)

websocket客户端:WebSocketClient.java

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.Transport;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;

//@JsonIgnoreProperties(ignoreUnknown = true)
public class WebsocketClient {
    public static void main(String[] args){
        // 1.login to rest service
        authToRest();
        // 2.establish websocket connection
        openConnection();
    }

    private static HttpHeaders getHeaders(){
        String plainCredentials="admin:admin";
        String base64Credentials = Base64.getEncoder().encodeToString(plainCredentials.getBytes());

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Basic " + base64Credentials);
        return headers;
    }

    public static void authToRest(){
        RestTemplate restTemplate = new RestTemplate(); 
        HttpEntity<String> request = new HttpEntity<String>(getHeaders());
        ResponseEntity<String> response = restTemplate.exchange("http://localhost:8082/hpdm/login", HttpMethod.GET, request, String.class);
        System.out.println(response.getBody());
    }

    public static void openConnection(){
        List<Transport> transports = new ArrayList<>(1);
        transports.add(new WebSocketTransport(new StandardWebSocketClient()));
        WebSocketClient transport = new SockJsClient(transports);
        WebSocketStompClient stompClient = new WebSocketStompClient(transport);

        stompClient.setMessageConverter(new MappingJackson2MessageConverter());
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.afterPropertiesSet();
        stompClient.setTaskScheduler(taskScheduler); // for heartbeats
        StompSessionHandler myHandler = new MyStompHandler();
        String url = "ws://localhost:8082/hpdm-ws";
        stompClient.connect(url, myHandler);

        //block the thread
        CountDownLatch latch = new CountDownLatch(1);
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class MyStompHandler extends StompSessionHandlerAdapter {

        @Override
        public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
            session.subscribe("/topic/response", new StompFrameHandler() {
                @Override
                public Type getPayloadType(StompHeaders headers) {
                    return Object.class;
                }
                @Override
                public void handleFrame(StompHeaders headers, Object payload) {
                    System.out.println(payload);
                }
            });
        }

        @Override
        public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload,
                Throwable exception) {
            System.out.println(exception.getMessage());
        }

        @Override
        public void handleTransportError(StompSession session, Throwable exception) {
            exception.printStackTrace();
            System.out.println("transport error.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但结果是我可以得到其余的响应但无法与WebSocket服务器建立连接。这是错误信息:

14:31:35.368 [main] DEBUG org.springframework.web.client.RestTemplate - GET request for "http://localhost:8082/hpdm/login" resulted in 200 (null)
14:31:35.369 [main] DEBUG org.springframework.web.client.RestTemplate - Reading [java.lang.String] as "text/plain;charset=UTF-8" using [org.springframework.http.converter.StringHttpMessageConverter@4b553d26]
Login success to WssBroker...
14:31:35.415 [main] INFO org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler - Initializing ExecutorService 
14:31:35.569 [main] DEBUG org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport - Executing SockJS Info request, url=http://localhost:8082/hpdm-ws/info
14:31:35.569 [main] DEBUG org.springframework.web.client.RestTemplate - Created GET request for "http://localhost:8082/hpdm-ws/info"
14:31:35.574 [main] DEBUG org.springframework.web.client.RestTemplate - GET request for "http://localhost:8082/hpdm-ws/info" resulted in 401 (null); invoking error handler
14:31:35.578 [main] ERROR org.springframework.web.socket.sockjs.client.SockJsClient - Initial SockJS "Info" request to server failed, url=ws://localhost:8082/hpdm-ws
org.springframework.web.client.HttpClientErrorException: 401 null
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:667)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:620)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:595)
    at org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport.executeInfoRequestInternal(RestTemplateXhrTransport.java:138)
    at org.springframework.web.socket.sockjs.client.AbstractXhrTransport.executeInfoRequest(AbstractXhrTransport.java:155)
    at org.springframework.web.socket.sockjs.client.SockJsClient.getServerInfo(SockJsClient.java:286)
    at org.springframework.web.socket.sockjs.client.SockJsClient.doHandshake(SockJsClient.java:254)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:274)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:255)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:235)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:219)
    at com.hpi.hpdm.console.message.WebsocketClient.openConnection(WebsocketClient.java:66)
    at com.hpi.hpdm.console.message.WebsocketClient.main(WebsocketClient.java:35)
14:31:35.583 [main] DEBUG org.springframework.messaging.simp.stomp.DefaultStompSession - Failed to connect session id=d8fda5d4-ba5a-7d22-f517-74e939096bfa
org.springframework.web.client.HttpClientErrorException: 401 null
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:667)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:620)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:595)
    at org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport.executeInfoRequestInternal(RestTemplateXhrTransport.java:138)
    at org.springframework.web.socket.sockjs.client.AbstractXhrTransport.executeInfoRequest(AbstractXhrTransport.java:155)
    at org.springframework.web.socket.sockjs.client.SockJsClient.getServerInfo(SockJsClient.java:286)
    at org.springframework.web.socket.sockjs.client.SockJsClient.doHandshake(SockJsClient.java:254)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:274)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:255)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:235)
    at org.springframework.web.socket.messaging.WebSocketStompClient.connect(WebSocketStompClient.java:219)
    at com.hpi.hpdm.console.message.WebsocketClient.openConnection(WebsocketClient.java:66)
    at com.hpi.hpdm.console.message.WebsocketClient.main(WebsocketClient.java:35)
Run Code Online (Sandbox Code Playgroud)

有人可以帮助我吗?谢谢。

更新

这篇文章来看,问题似乎是我没有保护websocket endpoint. 我尝试了这两种解决方案,但没有奏效,也许我的方法有误。愿以正确的方式来保护websocket endponit

Dav*_*ral 7

好吧,我WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.spring 文档中误解了。

我应该验证升级到 WebSocket 而不是休息服务的 HTTP 请求。同时,WebSocket 配置需要更改一些内容。

1.在 WebSocket 中禁用 CSRF

添加sameOriginDisabled()WssBrokerConfig.

@Configuration
public class WssBrokerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

2.连接 WebSocketHttpHeaders

构建WebSocketHttpHeaders和之前添加用户凭证头connect()。用户名和密码应使用 base64 加密。

String plainCredentials="admin:admin";
String base64Credentials = Base64.getEncoder().encodeToString(plainCredentials.getBytes());

final WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
headers.add("Authorization", "Basic " + base64Credentials);

stompClient.connect(url, headers, myHandler);
Run Code Online (Sandbox Code Playgroud)