对于最新的稳定版本,请使用 Spring Security 6.4.1! |
WebSocket 安全性
Spring Security 4 增加了对保护 Spring 的 WebSocket 支持的支持。 本节介绍如何使用 Spring Security 的 WebSocket 支持。
WebSocket 配置
Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSockets 的授权支持。
要使用 Java 配置配置授权,只需扩展 并配置 .
例如:AbstractSecurityWebSocketMessageBrokerConfigurer
MessageSecurityMetadataSourceRegistry
-
Java
-
Kotlin
@Configuration
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer { (1) (2)
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/user/**").authenticated() (3)
}
}
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { (1) (2)
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
messages.simpDestMatchers("/user/**").authenticated() (3)
}
}
这将确保:
1 | 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来实施同源策略 |
2 | SecurityContextHolder 在任何入站请求的 simpUser 头属性中填充用户。 |
3 | 我们的消息需要适当的授权。具体来说,任何以 “/user/” 开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参阅 WebSocket 授权 |
Spring Security 还为保护 WebSockets 提供了 XML 命名空间支持。 基于 XML 的类似配置如下所示:
<websocket-message-broker> (1) (2)
(3)
<intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>
这将确保:
1 | 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来实施同源策略 |
2 | SecurityContextHolder 在任何入站请求的 simpUser 头属性中填充用户。 |
3 | 我们的消息需要适当的授权。具体来说,任何以 “/user/” 开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参阅 WebSocket 授权 |
WebSocket 身份验证
WebSockets 重复使用在建立 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。
这意味着 on 将被移交给 WebSockets。
如果您使用的是 Spring Security,则 on 会自动覆盖。Principal
HttpServletRequest
Principal
HttpServletRequest
更具体地说,要确保用户已对您的 WebSocket 应用程序进行身份验证,所需要做的就是确保您设置 Spring Security 以对基于 HTTP 的 Web 应用程序进行身份验证。
WebSocket 授权
Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSockets 的授权支持。
要使用 Java 配置配置授权,只需扩展 并配置 .
例如:AbstractSecurityWebSocketMessageBrokerConfigurer
MessageSecurityMetadataSourceRegistry
-
Java
-
Kotlin
@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)
}
}
@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 | 任何没有目的地的消息(即 MESSAGE 或 SUBSCRIBE 的 Message 类型以外的任何消息)都需要对用户进行身份验证 |
2 | 任何人都可以订阅 /user/queue/errors |
3 | 任何目标以 “/app/” 开头的消息都需要用户具有 ROLE_USER |
4 | 任何以 “/user/” 或 “/topic/friends/” 开头且类型的 SUBSCRIBE 消息都需要ROLE_USER |
5 | MESSAGE 或 SUBSCRIBE 类型的任何其他消息都将被拒绝。由于 6 我们不需要此步骤,但它说明了如何匹配特定的消息类型。 |
6 | 任何其他消息都将被拒绝。这是确保您不会错过任何消息的好主意。 |
Spring Security 还为保护 WebSockets 提供了 XML 命名空间支持。 基于 XML 的类似配置如下所示:
<websocket-message-broker>
(1)
<intercept-message type="CONNECT" access="permitAll" />
<intercept-message type="UNSUBSCRIBE" access="permitAll" />
<intercept-message type="DISCONNECT" access="permitAll" />
<intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> (2)
<intercept-message pattern="/app/**" access="hasRole('USER')" /> (3)
(4)
<intercept-message pattern="/user/**" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
(5)
<intercept-message type="MESSAGE" access="denyAll" />
<intercept-message type="SUBSCRIBE" access="denyAll" />
<intercept-message pattern="/**" access="denyAll" /> (6)
</websocket-message-broker>
这将确保:
1 | 任何 CONNECT、UNSUBSCRIBE 或 DISCONNECT 类型的消息都需要对用户进行身份验证 |
2 | 任何人都可以订阅 /user/queue/errors |
3 | 任何目标以 “/app/” 开头的消息都需要用户具有 ROLE_USER |
4 | 任何以 “/user/” 或 “/topic/friends/” 开头且类型的 SUBSCRIBE 消息都需要ROLE_USER |
5 | MESSAGE 或 SUBSCRIBE 类型的任何其他消息都将被拒绝。由于 6 我们不需要此步骤,但它说明了如何匹配特定的消息类型。 |
6 | 具有目标的任何其他邮件都将被拒绝。这是确保您不会错过任何消息的好主意。 |
WebSocket 授权说明
为了正确保护你的应用程序,了解 Spring 的 WebSocket 支持是很重要的。
消息类型的 WebSocket 授权
了解SUBSCRIBE和MESSAGE类型的消息之间的区别以及它在 Spring 中的工作原理非常重要。
考虑一个聊天应用程序。
-
系统可以通过 “/topic/system/notifications” 的目的地向所有用户发送通知 MESSAGE
-
客户端可以通过 SUBSCRIBE 到 “/topic/system/notifications” 来接收通知。
虽然我们希望客户端能够订阅 “/topic/system/notifications”,但我们不希望它们能够向该目标发送消息。 如果我们允许向 “/topic/system/notifications” 发送消息,则客户端可以直接向该终端节点发送消息并模拟系统。
通常,应用程序通常会拒绝发送到以代理前缀开头的目标(即“/topic/”或“/queue/”)的任何 MESSAGE。
目标上的 WebSocket 授权
了解目的地是如何转换的也很重要。
考虑一个聊天应用程序。
-
用户可以通过向 /app/chat 的目的地发送消息来向特定用户发送消息。
-
应用程序看到消息,确保将 “from” 属性指定为当前用户(我们不能信任客户端)。
-
然后,应用程序使用 将消息发送给收件人。
SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)
-
消息将转换为“/queue/user/messages-<sessionid>”的目标
在上面的应用程序中,我们希望允许我们的客户端监听 “/user/queue” ,它被转换为 “/queue/user/messages-<sessionid>”。 但是,我们不希望客户端能够监听 “/queue/*”,因为这会允许客户端看到每个用户的消息。
通常,应用程序通常会拒绝发送到以代理前缀(即“/topic/”或“/queue/”)开头的消息的任何 SUBSCRIBE。 当然,我们可能会提供例外情况来说明诸如
出站消息
Spring 包含一个标题为“消息流”的部分,该部分描述了消息如何流经系统。
请务必注意,Spring Security 仅保护 .
Spring Security 不会尝试保护 .clientInboundChannel
clientOutboundChannel
最重要的原因是性能。 对于传入的每条消息,通常会有更多的消息传出。 我们鼓励保护终端节点的订阅,而不是保护出站消息。
实施同源策略
需要强调的是,浏览器不会对 WebSocket 连接强制实施同源策略。 这是一个极其重要的考虑因素。
为什么选择 Same Origin?
请考虑以下场景。 用户访问 bank.com 并对其帐户进行身份验证。 同一用户在浏览器中打开另一个选项卡并访问 evil.com。 同源策略可确保 evil.com 无法对 bank.com 进行读取或写入数据。
对于 WebSockets,同源策略不适用。 事实上,除非 bank.com 明确禁止,否则 evil.com 可以代表用户读取和写入数据。 这意味着用户可以通过 webSocket 做的任何事情(即转账),evil.com 可以代表该用户做。
由于 Sockjs 尝试模拟 WebSockets,因此它也绕过了同源策略。 这意味着开发人员在使用 Sockjs 时需要明确保护他们的应用程序免受外部域的影响。
将 CSRF 添加到 Stomp 标头
默认情况下, Spring Security 在任何 CONNECT 消息类型中都需要 CSRF 令牌。 这确保了只有有权访问 CSRF 令牌的站点才能连接。 由于只有 Same Origin 可以访问 CSRF Token,因此不允许外部域建立连接。
通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF 令牌。 但是,Sockjs 不允许这些选项。 相反,我们必须在 Stomp 标头中包含令牌
应用程序可以通过访问名为 _csrf 的 request 属性来获取 CSRF 令牌。
例如,以下内容将允许在 JSP 中访问 :CsrfToken
var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";
如果您使用的是静态 HTML,则可以在 REST 端点上公开 。
例如,以下内容将在 URL /csrf 上公开CsrfToken
CsrfToken
-
Java
-
Kotlin
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
@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
-
Kotlin
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
...
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
// ...
override fun sameOriginDisabled(): Boolean {
return true
}
}
使用 Sockjs
Sockjs 提供回退传输以支持较旧的浏览器。 当使用回退选项时,我们需要放宽一些安全约束,以允许 Sockjs 与 Spring Security 一起使用。
SockJS & 框架选项
Sockjs 可以使用利用 iframe 的传输。 默认情况下,Spring Security 将拒绝对站点进行框架化以防止点击劫持攻击。 要允许基于 Sockjs 帧的传输工作,我们需要配置 Spring Security 以允许同一源构建内容。
您可以使用 frame-options 元素自定义 X-Frame-Options。 例如,以下内容将指示 Spring Security 使用“X-Frame-Options: SAMEORIGIN”,它允许同一域内的 iframe:
<http>
<!-- ... -->
<headers>
<frame-options
policy="SAMEORIGIN" />
</headers>
</http>
同样,您可以使用以下内容自定义框架选项以在 Java 配置中使用相同的来源:
-
Java
-
Kotlin
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
return http.build();
}
}
@EnableWebSecurity
open class WebSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
headers {
frameOptions {
sameOrigin = true
}
}
}
return http.build()
}
}
SockJS & 放松 CSRF
Sockjs 对 CONNECT 消息使用 POST 进行任何基于 HTTP 的传输。 通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF 令牌。 但是,Sockjs 不允许这些选项。 相反,我们必须在 Stomp 标头中包含令牌,如 将 CSRF 添加到 Stomp 标头中所述。
这也意味着我们需要放松对 Web 层的 CSRF 保护。 具体来说,我们希望为我们的连接 URL 禁用 CSRF 保护。 我们不想为每个 URL 禁用 CSRF 保护。 否则我们的网站将容易受到 CSRF 攻击。
我们可以通过提供 CSRF RequestMatcher 轻松实现这一点。 我们的 Java 配置使这变得非常简单。 例如,如果我们的 stomp 端点是 “/chat”,我们可以使用以下配置仅对以 “/chat/” 开头的 URL 禁用 CSRF 保护:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(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
...
)
...
@Configuration
@EnableWebSecurity
open class WebSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf {
ignoringAntMatchers("/chat/**")
}
headers {
frameOptions {
sameOrigin = true
}
}
authorizeRequests {
// ...
}
// ...
如果我们使用基于 XML 的配置,则可以使用 csrf@request-matcher-ref。 例如:
<http ...>
<csrf request-matcher-ref="csrfMatcher"/>
<headers>
<frame-options policy="SAMEORIGIN"/>
</headers>
...
</http>
<b:bean id="csrfMatcher"
class="AndRequestMatcher">
<b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
<b:constructor-arg>
<b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
<b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<b:constructor-arg value="/chat/**"/>
</b:bean>
</b:bean>
</b:constructor-arg>
</b:bean>