# WebSocket 安全
Spring Security4增加了对保护[Spring’s WebSocket support](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html)的支持。本节介绍如何使用 Spring Security的 WebSocket 支持。
直接支持JSR-356
Spring 安全性不提供直接的JSR-356支持,因为这样做将提供很少的价值。这是因为格式未知,所以有[little Spring can do to secure an unknown format](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-intro-sub-protocol)。此外,JSR-356不提供截获消息的方法,因此安全性将是侵入性的。
## WebSocket 配置
Spring Security4.0通过 Spring 消息抽象引入了对WebSockets的授权支持。要使用 Java 配置配置来配置授权,只需扩展`AbstractSecurityWebSocketMessageBrokerConfigurer`并配置`MessageSecurityMetadataSourceRegistry`。例如:
Java
```
@Configuration
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer { (1) (2)
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/user/**").authenticated() (3)
}
}
```
Kotlin
```
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { (1) (2)
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
messages.simpDestMatchers("/user/**").authenticated() (3)
}
}
```
这将确保:
|**1**|任何入站连接消息都需要一个有效的CSRF令牌来执行[同源政策](#websocket-sameorigin)|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|对于任何入站请求,SecurityContextholder都在Simpuser header属性中填充用户。|
|**3**|我们的信息需要得到适当的授权。具体地说,任何以“/user/”开头的入站消息都需要角色\_user。有关授权的更多详细信息,请参见[WebSocket 授权](#websocket-authorization)。|
Spring 安全性还提供了[XML命名空间](../appendix/namespace/websocket.html#nsa-websocket-security)用于保护WebSockets的支持。类似的基于XML的配置如下所示:
```
(1) (2)
(3)
```
这将确保:
|**1**|任何入站连接消息都需要一个有效的CSRF令牌来执行[同源政策](#websocket-sameorigin)|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|对于任何入站请求,SecurityContextholder都在Simpuser header属性中填充用户。|
|**3**|我们的信息需要得到适当的授权。具体地说,任何以“/user/”开头的入站消息都需要角色\_user。有关授权的更多详细信息,请参见[WebSocket Authorization](#websocket-authorization)。|
## WebSocket 认证
WebSockets重用在建立 WebSocket 连接时在HTTP请求中找到的相同的身份验证信息。这意味着`Principal`上的`HttpServletRequest`将被传递给WebSockets。如果使用 Spring 安全性,则`HttpServletRequest`上的`Principal`将自动被重写。
更具体地说,为了确保用户已经对你的 WebSocket 应用程序进行了身份验证,所有必要的是确保设置 Spring 安全性以对基于HTTP的Web应用程序进行身份验证。
## WebSocket Authorization
Spring Security4.0通过 Spring 消息抽象引入了对WebSockets的授权支持。要使用 Java 配置配置来配置授权,只需扩展`AbstractSecurityWebSocketMessageBrokerConfigurer`并配置`MessageSecurityMetadataSourceRegistry`。例如:
Java
```
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated() (1)
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
.simpDestMatchers("/app/**").hasRole("USER") (3)
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
.anyMessage().denyAll(); (6)
}
}
```
Kotlin
```
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
messages
.nullDestMatcher().authenticated() (1)
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
.simpDestMatchers("/app/**").hasRole("USER") (3)
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
.anyMessage().denyAll() (6)
}
}
```
这将确保:
|**1**|任何没有目的地的消息(即消息类型或订阅以外的任何消息)都需要对用户进行身份验证。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|任何人都可以订阅/user/queue/errors|
|**3**|任何具有以“/app/”开头的目标的消息都将要求用户具有角色\_user|
|**4**|任何以“/user/”或“/topic/friends/”开头、类型为Subscribe的消息都需要角色\_user|
|**5**|任何其他类型为消息或订阅的消息都将被拒绝。由于6,我们不需要这个步骤,但它说明了如何在特定的消息类型上进行匹配。|
|**6**|任何其他消息都将被拒绝。这是一个好主意,以确保你不会错过任何消息。|
Spring 安全性还提供了[XML命名空间](../appendix/namespace/websocket.html#nsa-websocket-security)用于保护WebSockets的支持。类似的基于XML的配置如下所示:
```
(1)
(2)
(3)
(4)
(5)
(6)
```
这将确保:
|**1**|任何类型为“连接”、“取消订阅”或“断开连接”的消息都需要对用户进行身份验证。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|任何人都可以订阅/user/queue/errors|
|**3**|任何具有以“/app/”开头的目标的消息都将要求用户具有角色\_user|
|**4**|任何以“/user/”或“/topic/friends/”开头、类型为Subscribe的消息都需要角色\_user|
|**5**|任何其他类型为消息或订阅的消息都将被拒绝。由于6,我们不需要这个步骤,但它说明了如何在特定的消息类型上进行匹配。|
|**6**|具有目的的任何其他消息都将被拒绝。这是一个好主意,以确保你不会错过任何消息。|
### WebSocket 授权说明
为了正确地保护你的应用程序,理解 Spring 的 WebSocket 支持非常重要。
#### WebSocket 对消息类型的授权
理解消息的订阅和消息类型之间的区别以及它在 Spring 中的工作方式非常重要。
考虑一个聊天应用程序。
* 系统可以通过目标“/topic/system/notifications”向所有用户发送通知消息。
* 客户端可以通过订阅“/topic/system/notification”来接收通知。
虽然我们希望客户机能够订阅“/topic/system/notifications”,但我们并不希望使他们能够向该目的地发送消息。如果我们允许向“/topic/system/notifications”发送消息,那么客户端可以直接向该端点发送消息并模拟系统。
通常,应用程序会拒绝发送到以[代理前缀](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp)(即“/topic/”或“/queue/”)开头的目标的任何消息。
#### WebSocket 目的地授权
了解目的地是如何转变的也很重要。
考虑一个聊天应用程序。
* 用户可以通过向“/app/chat”的目的地发送消息来向特定用户发送消息。
* 应用程序看到消息,确保将“from”属性指定为当前用户(我们不能信任客户端)。
* 然后,应用程序使用`SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`将消息发送给收件人。
* 消息被转换为“/queue/user/messages-\”的目标。
对于上面的应用程序,我们希望允许我们的客户机侦听“/user/queue”,它被转换为“/queue/user/messages-\”。但是,我们不希望客户机能够侦听“/queue/\*”,因为这将允许客户机查看每个用户的消息。
通常,应用程序会拒绝发送到以[代理前缀](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp)(即“/topic/”或“/queue/”)开头的消息的任何订阅。当然,我们可能会提供例外情况,以解释诸如
### 出站消息
Spring 包含一个标题为[消息流](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow)的部分,该部分描述了消息如何在系统中流动。需要注意的是, Spring 安全性仅保护`clientInboundChannel`。 Spring 安全性不试图保护`clientOutboundChannel`。
这其中最重要的原因是业绩。对于每一条传入的消息,通常都会有更多的消息传出。我们鼓励保护对端点的订阅,而不是保护出站消息。
## 执行同源政策
需要强调的是,对于 WebSocket 连接,浏览器并不强制执行[同源政策](https://en.wikipedia.org/wiki/Same-origin_policy)。这是一个极其重要的考虑因素。
### 为什么是同源?
考虑以下场景。用户访问bank.com并对其帐户进行身份验证。同一个用户在浏览器中打开另一个标签,然后访问Evil.com。相同的源策略确保Evil.com不能将数据读写到bank.com。
对于WebSockets,相同的源策略不适用。事实上,除非bank.com明确禁止,否则evil.com可以代表用户读写数据。这意味着用户可以在 WebSocket 上做的任何事情(即转账),Evil.com都可以代表用户做。
由于Sockjs试图模拟WebSockets,因此它也绕过了相同的源策略。这意味着开发人员在使用Sockjs时需要显式地保护其应用程序不受外部域的影响。
### Spring WebSocket 允许原产地
幸运的是,由于 Spring 4.1.5 Spring 的 WebSocket 和Sockjs支持限制了对[当前域](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins)的访问。 Spring 安全性增加了额外的保护层,以提供[纵深防御](https://en.wikipedia.org/wiki/Defense_in_depth_(computing))。
### 将CSRF添加到Stomp头
Spring 默认情况下,安全性要求在任何连接消息类型中使用[CSRF token](../../features/exploits/csrf.html#csrf)。这确保只有能够访问CSRF令牌的站点才能进行连接。由于只有**同源**可以访问CSRF令牌,因此不允许外部域进行连接。
通常,我们需要在HTTP报头或HTTP参数中包含CSRF令牌。然而,Sockjs不允许这些选项。相反,我们必须在Stomp头中包含令牌。
应用程序可以通过访问名为\_CSRF的请求属性[获取CSRF令牌](../exploits/csrf.html#servlet-csrf-include)。例如,下面将允许访问JSP中的`CsrfToken`:
```
var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";
```
如果使用静态HTML,则可以在REST端点上公开`CsrfToken`。例如,下面将公开URL/CSRF上的`CsrfToken`
Java
```
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
```
Kotlin
```
@RestController
class CsrfController {
@RequestMapping("/csrf")
fun csrf(token: CsrfToken): CsrfToken {
return token
}
}
```
JavaScript可以对端点进行REST调用,并使用响应来填充headername和令牌。
我们现在可以在我们的STOMP客户端中包含令牌。例如:
```
...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {
...
}
```
### 禁用WebSockets中的CSRF
如果你希望允许其他域访问你的站点,则可以禁用 Spring Security的保护。例如,在 Java 配置中,你可以使用以下方法:
Java
```
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
...
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
```
Kotlin
```
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
// ...
override fun sameOriginDisabled(): Boolean {
return true
}
}
```
## 与Sockjs合作
[SockJS](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback)提供后备传输以支持较旧的浏览器。在使用后备选项时,我们需要放松一些安全约束,以允许Sockjs使用 Spring 安全性。
### sockjs&frame-选项
Sockjs可以使用[利用iframe的传输](https://github.com/sockjs/sockjs-client/tree/v0.3.4)。 Spring 默认情况下,安全性将[deny](../../features/exploits/headers.html#headers-frame-options)网站框起来,以防止点击劫持攻击。为了允许基于SockJS帧的传输工作,我们需要配置 Spring 安全性,以允许相同的源来帧内容。
你可以使用[框架-选项](../appendix/namespace/http.html#nsa-frame-options)元素自定义X-frame-options。例如,下面将指示 Spring Security使用“X-Frame-Options:SameOrigin”,它允许在相同的域内使用IFrames:
```
```
类似地,你可以使用以下方式自定义框架选项,以便在 Java 配置中使用相同的原点:
Java
```
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
}
}
```
Kotlin
```
@EnableWebSecurity
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
// ...
headers {
frameOptions {
sameOrigin = true
}
}
}
}
}
```
### Sockjs&放松的CSRF
对于任何基于HTTP的传输,Sockjs都会在Connect消息上使用POST。通常,我们需要在HTTP报头或HTTP参数中包含CSRF令牌。然而,Sockjs不允许这些选项。相反,我们必须像[将CSRF添加到Stomp头](#websocket-sameorigin-csrf)中描述的那样,在stomp头中包含令牌。
这也意味着我们需要放松对Web层的CSRF保护。具体地说,我们希望禁用我们的连接URL的CSRF保护。我们不希望禁用每个URL的CSRF保护。否则,我们的网站将容易受到CSRF攻击。
我们可以通过提供CSRF请求匹配器轻松地实现这一点。我们的 Java 配置使这一点变得非常容易。例如,如果我们的Stomp端点是“/chat”,那么我们可以使用以下配置,仅禁用以“/chat/”开头的URL的CSRF保护:
Java
```
@Configuration
@EnableWebSecurity
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
// ignore our stomp endpoints since they are protected using Stomp headers
.ignoringAntMatchers("/chat/**")
)
.headers(headers -> headers
// allow same origin to frame our site to support iframe SockJS
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
)
.authorizeHttpRequests(authorize -> authorize
...
)
...
```
Kotlin
```
@Configuration
@EnableWebSecurity
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
csrf {
ignoringAntMatchers("/chat/**")
}
headers {
frameOptions {
sameOrigin = true
}
}
authorizeRequests {
// ...
}
// ...
```
如果我们使用基于XML的配置,我们可以使用[[电子邮件保护]](../acception/namespace/http.html#NSA-csrf-request-matcher-ref)。例如:
```
...
```
[Spring MVC](mvc.html)[Spring’s CORS Support](cors.html)