# 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)