对于最新的稳定版本,请使用 Spring Security 6.4.1! |
WebSocket 安全性
Spring Security 4 增加了对保护 Spring 的 WebSocket 支持的支持。 本节介绍如何使用 Spring Security 的 WebSocket 支持。
WebSocket 身份验证
WebSockets 重复使用在建立 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。
这意味着Principal
在HttpServletRequest
将移交给 WebSockets。
如果您使用的是 Spring Security,则Principal
在HttpServletRequest
将自动覆盖。
更具体地说,要确保用户已对您的 WebSocket 应用程序进行身份验证,所需要做的就是确保您设置 Spring Security 以对基于 HTTP 的 Web 应用程序进行身份验证。
WebSocket 授权
Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSockets 的授权支持。
在 Spring Security 5.8 中,此支持已更新为使用AuthorizationManager
应用程序接口。
要使用 Java 配置配置授权,只需包含@EnableWebSocketSecurity
注解并发布AuthorizationManager<Message<?>>
bean 或在 XML 中使用use-authorization-manager
属性。
一种方法是使用AuthorizationManagerMessageMatcherRegistry
要指定终端节点模式,请执行以下作:
-
Java
-
Kotlin
@Configuration
@EnableWebSocketSecurity (1) (2)
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/user/**").authenticated() (3)
return messages.build();
}
}
@Configuration
@EnableWebSocketSecurity (1) (2)
open class WebSocketSecurityConfig { (1) (2)
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
messages.simpDestMatchers("/user/**").authenticated() (3)
return messages.build()
}
}
1 | 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来实施同源策略。 |
2 | 这SecurityContextHolder 中填充了simpUser header 属性。 |
3 | 我们的消息需要适当的授权。具体而言,任何以/user/ 将要求ROLE_USER .您可以在 WebSocket 授权中找到有关授权的其他详细信息 |
Spring Security 还为保护 WebSockets 提供了 XML 命名空间支持。 基于 XML 的类似配置如下所示:
<websocket-message-broker use-authorization-manager="true"> (1) (2)
<intercept-message pattern="/user/**" access="authenticated"/> (3)
</websocket-message-broker>
这将确保:
1 | 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来实施同源策略 |
2 | SecurityContextHolder 在任何入站请求的 simpUser 头属性中填充用户。 |
3 | 我们的消息需要适当的授权。具体来说,任何以 “/user/” 开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参阅 WebSocket 授权 |
自定义授权
使用AuthorizationManager
,自定义非常简单。
例如,您可以发布AuthorizationManager
这要求所有消息都具有 “USER” 角色,使用AuthorityAuthorizationManager
,如下所示:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSocketSecurity (1) (2)
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
return AuthorityAuthorizationManager.hasRole("USER");
}
}
@Configuration
@EnableWebSocketSecurity (1) (2)
open class WebSocketSecurityConfig {
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
return AuthorityAuthorizationManager.hasRole("USER") (3)
}
}
<bean id="authorizationManager" class="org.example.MyAuthorizationManager"/>
<websocket-message-broker authorization-manager-ref="myAuthorizationManager"/>
有几种方法可以进一步匹配消息,如下面的更高级示例所示:
-
Java
-
Kotlin
-
Xml
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder 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)
return messages.build();
}
}
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
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)
return messages.build();
}
}
<websocket-message-broker use-authorization-manager="true">
(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/**" type="SUBSCRIBE" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" type="SUBSCRIBE" 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 | 任何没有目的地的消息(即 MESSAGE 或 SUBSCRIBE 的 Message 类型以外的任何消息)都需要对用户进行身份验证 |
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 中的工作方式。
考虑一个聊天应用程序:
-
系统可以发送通知
MESSAGE
通过目标/topic/system/notifications
. -
客户端可以通过以下方式接收通知
SUBSCRIBE
到/topic/system/notifications
.
虽然我们希望客户能够SUBSCRIBE
自/topic/system/notifications
,我们不希望他们能够发送MESSAGE
到那个目的地。
如果我们允许发送MESSAGE
自/topic/system/notifications
,客户端可以直接向该终端节点发送消息并模拟系统。
通常,应用程序通常会拒绝任何MESSAGE
发送到以 broker 前缀开头的目标 (/topic/
或/queue/
).
目标上的 WebSocket 授权
您还应该了解目标是如何转换的。
考虑一个聊天应用程序:
-
用户可以通过向
/app/chat
目的地。 -
应用程序会看到该消息,确保
from
属性指定为当前用户(我们不能信任客户端)。 -
然后,应用程序使用
SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)
. -
消息将转换为
/queue/user/messages-<sessionid>
.
通过这个聊天应用程序,我们希望让我们的客户倾听/user/queue
,它被转换为/queue/user/messages-<sessionid>
.
但是,我们不希望客户端能够监听/queue/*
,因为这样可以让客户端看到每个用户的消息。
通常,应用程序通常会拒绝任何SUBSCRIBE
发送到以 broker 前缀开头的消息 (/topic/
或/queue/
).
我们可能会提供例外情况来说明以下情况:
出站消息
Spring Framework 参考文档包含一个名为“消息流”的部分,该部分描述了消息如何流经系统。
请注意,Spring Security 仅保护clientInboundChannel
.
Spring Security 不会尝试保护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 令牌,因此不允许外部域建立连接。
通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF 令牌。 但是,Sockjs 不允许这些选项。 相反,我们必须在 Stomp 标头中包含令牌。
应用程序可以通过访问名为_csrf
.
例如,以下允许访问CsrfToken
在 JSP 中:
var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";
如果您使用静态 HTML,则可以公开CsrfToken
在 REST 端点上。
例如,以下将公开CsrfToken
在/csrf
网址:
-
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
此时,CSRF 在使用@EnableWebSocketSecurity ,但这可能会在未来发行版中添加。 |
要禁用 CSRF,而不是使用@EnableWebSocketSecurity
,你可以使用 XML 支持或自己添加 Spring Security 组件,如下所示:
-
Java
-
Kotlin
-
Xml
@Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
AuthorizationManager<Message<?>> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated();
AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules);
AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context);
authz.setAuthorizationEventPublisher(publisher);
registration.interceptors(new SecurityContextChannelInterceptor(), authz);
}
}
@Configuration
open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer {
@Override
override fun addArgumentResolvers(argumentResolvers: List<HandlerMethodArgumentResolver>) {
argumentResolvers.add(AuthenticationPrincipalArgumentResolver())
}
@Override
override fun configureClientInboundChannel(registration: ChannelRegistration) {
var myAuthorizationRules: AuthorizationManager<Message<?>> = AuthenticatedAuthorizationManager.authenticated()
var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules)
var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context)
authz.setAuthorizationEventPublisher(publisher)
registration.interceptors(SecurityContextChannelInterceptor(), authz)
}
}
<websocket-message-broker use-authorization-manager="true" same-origin-disabled="true">
<intercept-message pattern="/**" access="authenticated"/>
</websocket-message-broker>
另一方面,如果您使用的是遗产AbstractSecurityWebSocketMessageBrokerConfigurer
并且您希望允许其他域访问您的站点,则可以禁用 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
}
}
自定义表达式处理程序
有时,自定义access
表达式在intercept-message
XML 元素。
为此,您可以创建一个SecurityExpressionHandler<MessageAuthorizationContext<?>>
并在 XML 定义中引用它,如下所示:
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler"/>
如果要从websocket-message-broker
实现一个SecurityExpressionHandler<Message<?>>
您可以:
1. 此外,实现createEvaluationContext(Supplier, Message)
方法,然后
2. 将该值包装在MessageAuthorizationContextSecurityExpressionHandler
这样:
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler">
<b:constructor-arg>
<b:bean class="org.example.MyLegacyExpressionHandler"/>
</b:constructor-arg>
</b:bean>
使用 Sockjs
Sockjs 提供回退传输以支持较旧的浏览器。 当使用回退选项时,我们需要放宽一些安全约束,以允许 Sockjs 与 Spring Security 一起工作。
SockJS & 框架选项
Sockjs 可以使用利用 iframe 的传输。 默认情况下,Spring Security 拒绝对站点进行框架化以防止点击劫持攻击。 要允许 Sockjs 基于帧的传输工作,我们需要配置 Spring Security 以允许同一源帧内容。
您可以自定义X-Frame-Options
替换为 frame-options 元素。
例如,下面指示 Spring Security 使用X-Frame-Options: SAMEORIGIN
,它允许同一域内的 iframe:
<http>
<!-- ... -->
<headers>
<frame-options
policy="SAMEORIGIN" />
</headers>
</http>
同样,您可以使用以下内容自定义框架选项以在 Java 配置中使用相同的来源:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
return http.build();
}
}
@Configuration
@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/
使用以下配置:
-
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
.ignoringRequestMatchers("/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 {
ignoringRequestMatchers("/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>
旧版 WebSocket 配置
在 Spring Security 5.8 之前,使用 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 授权 |
如果您有一个自定义的SecurityExpressionHandler
延伸AbstractSecurityExpressionHandler
和覆盖createEvaluationContextInternal
或createSecurityExpressionRoot
.
为了推迟Authorization
lookup,新的AuthorizationManager
API 在计算表达式时不会调用这些表达式。
如果您使用的是 XML,则只需不使用use-authorization-manager
元素或将其设置为false
.