1. 簡介
在上一篇文章中,我們演示瞭如何將 WebSockets 添加到 Spring MVC 項目中。在這裏,我們將描述如何 在 Spring MVC 中為 Spring WebSockets 添加安全功能。
在繼續之前,請確保您已經有基本的 Spring MVC 安全覆蓋 – 如果沒有,請查看這篇文章。
2. Maven 依賴項
有兩個主要組的 Maven 依賴項需要用於我們的 WebSocket 實現。
首先,我們指定 Spring Framework 和 Spring Security 的總體版本:
6.0.12
6.1.5
6.0.2
其次,我們添加核心 Spring MVC 和 Spring Security 庫,用於實現基本身份驗證和授權:
org.springframework
spring-core
${spring.version}
org.springframework
spring-web
${spring.version}
org.springframework
spring-webmvc
${spring.version}
org.springframework.security
spring-security-web
${spring-security.version}
org.springframework.security
spring-security-config
${spring-security.version}
最新的 spring-core, spring-web, spring-webmvc, spring-security-web, spring-security-config 可以找到在 Maven Central。
最後,我們添加所需的依賴項:
org.springframework
spring-websocket
${spring.version}
org.springframework
spring-messaging
${spring.version}
org.springframework.security
spring-security-messaging
${spring-security-messaging.version}
您可以在 Maven Central 找到最新的版本 spring-websocket, spring-messaging, 和 spring-security-messaging。
3. WebSocket 安全基礎
要配置 WebSocket 安全,請包含 @EnableWebSocketSecurity 註解併發佈一個 AuthorizationManager<Message<?>> Bean。這可以通過使用 AuthorizationManagerMessageMatcherRegistry 來指定端點模式,例如:
@Configuration
@EnableWebSocketSecurity
public class SocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages.simpDestMatchers("/secured/**", "/secured/**/**")
.authenticated()
.anyMessage()
.authenticated();
return messages.build();
}
}
4. 安全套接字路由配置
現在我們已經介紹了基本的套接字安全性和類型匹配配置,可以結合套接字安全、視圖、STOMP(文本消息協議)、消息代理和套接字控制器,從而在我們的 Spring MVC 應用程序中啓用安全的 WebSockets。
首先,讓我們為基本的 Spring Security 覆蓋設置套接字視圖和控制器:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
@ComponentScan("com.baeldung.springsecuredsockets")
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry
.requestMatchers("/", "/index", "/authenticate").permitAll()
.requestMatchers("/secured/**/**", "/secured/**/**/**", "/secured/socket",
"/secured/success").authenticated()
.anyRequest().authenticated())
.formLogin(httpSecurityFormLoginConfigurer ->
httpSecurityFormLoginConfigurer.loginPage("/login").permitAll()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/authenticate")
.successHandler(loginSuccessHandler())
.failureUrl("/denied").permitAll())
//...
}
}
其次,讓我們為帶有身份驗證要求的實際消息目的地設置:
@Configuration
@EnableWebSocketSecurity
public class SocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages.simpDestMatchers("/secured/**", "/secured/**/**")
.authenticated()
.anyMessage()
.authenticated();
return messages.build();
}
}
現在,在我們的WebSocketMessageBrokerConfigurer 中,我們可以註冊實際的消息和 STOMP 端點:
@Configuration
@EnableWebSocketMessageBroker
public class SocketBrokerConfig
implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/secured/history");
config.setApplicationDestinationPrefixes("/spring-security-mvc-socket");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/secured/chat")
.withSockJS();
}
}
讓我們定義一個示例套接字控制器和我們上面為之提供了安全覆蓋的端點:
@Controller
public class SocketController {
@MessageMapping("/secured/chat")
@SendTo("/secured/history")
public OutputMessage send(Message msg) throws Exception {
return new OutputMessage(
msg.getFrom(),
msg.getText(),
new SimpleDateFormat("HH:mm").format(new Date()));
}
}
5. 同源策略
同源策略要求所有與端點的交互必須來自發起交互的同一域。
例如,如果您的 WebSocket 實現託管在 foo.com,並且您正在強制執行同源策略。如果用户連接到您在 foo.com 託管的客户端,然後在另一個瀏覽器中打開 bar.com,則 bar.com 將無法訪問您的 WebSocket 實現。
5.1. 繞過同源策略
Spring WebSockets 內置了同源策略,而普通 WebSocket 沒有。
事實上,Spring Security 需要一個 CSRF (跨站請求偽造) 令牌,用於任何有效的 CONNECT 消息類型:
@Controller
public class CsrfTokenController {
@GetMapping("/csrf")
public @ResponseBody String getCsrfToken(HttpServletRequest request) {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
return csrf.getToken();
}
}
通過調用以 / 結尾的端點,客户端可以獲取令牌並通過 CSRF 安全層進行身份驗證。
在僅使用 @EnableWebSocketSecurity 時,無法配置 CSRF,儘管這可能在未來的版本中添加。但是,如果您使用的是遺留的 AbstractSecurityWebSocketMessageBrokerConfigurer,則可以通過在 AbstractSecurityWebSocketMessageBrokerConfigurer 中添加以下配置來覆蓋 Spring 的同源策略:
@Override
protected boolean sameOriginDisabled() {
return true;
}
5.2. STOMP、SockJS 支持和幀選項
使用 STOMP 和 SockJS 實現客户端對 Spring WebSockets 的支持很常見。
默認情況下,SockJS 配置為禁止通過 HTML iframe 元素進行傳輸,這是為了防止點擊劫持的威脅。
但是,在允許 iframe 訪問 SockJS 傳輸的情況下,存在某些用例,可以帶來好處。要做到這一點,您可以創建一個 SecurityFilterChain 託管 Bean:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
//...
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()))
.authorizeHttpRequests(Customizer.withDefaults());
return http.build();
}
注意,在這個例子中,我們儘管允許通過 iframe 元素進行傳輸,仍然遵循 同源策略。
6. Oauth2 覆蓋
Oauth2 針對 Spring WebSockets 的支持是通過在標準 WebSecurityConfigurerAdapter 覆蓋之外,並對其進行擴展來實現的。 這是一個 Oauth2 的實現示例。
要對 WebSocket 端點進行身份驗證並訪問,可以在從客户端連接到後端 WebSocket 時,將 Oauth2 access_token 作為查詢參數傳遞。
這是一個演示該概念的示例,使用 SockJS 和 STOMP:
var endpoint = '/ws/?access_token=' + auth.access_token;
var socket = new SockJS(endpoint);
var stompClient = Stomp.over(socket);