Token Authentication

Spring Security OAuth 为基于令牌的安全性提供支持,包括 JSON Web 令牌 (JWT)。你可以在 Web 应用程序中使用它作为身份验证机制,包括通过 WebSocket 进行的 STOMP 交互,如前一节所述(即,通过基于 cookie 的会话维护身份)。

Spring Security OAuth provides support for token based security, including JSON Web Token (JWT). You can use this as the authentication mechanism in Web applications, including STOMP over WebSocket interactions, as described in the previous section (that is, to maintain identity through a cookie-based session).

与此同时,基于 cookie 的会话并不总是最合适的(例如在不维护服务器端会话的应用程序或通常使用标头进行验证的移动应用程序中)。

At the same time, cookie-based sessions are not always the best fit (for example, in applications that do not maintain a server-side session or in mobile applications where it is common to use headers for authentication).

WebSocket 协议,RFC 6455“没有规定服务器在 WebSocket 握手期间验证客户端的任何特定方式。”然而,在实践中,浏览器客户端只能使用标准身份验证标题(即基本的 HTTP 身份验证)或 cookie,并且不能(例如)提供自定义标题。同样,SockJS JavaScript 客户端也没有提供一种方法,可以通过该方法使用 SockJS 传输请求发送 HTTP 标题。请参阅 sockjs-client 问题 196。它允许发送查询参数,你可以用它来发送令牌,但这有其自身的缺点(例如,令牌可能无意中与 URL 一起记录在服务器日志中)。

The WebSocket protocol, RFC 6455 "doesn’t prescribe any particular way that servers can authenticate clients during the WebSocket handshake." In practice, however, browser clients can use only standard authentication headers (that is, basic HTTP authentication) or cookies and cannot (for example) provide custom headers. Likewise, the SockJS JavaScript client does not provide a way to send HTTP headers with SockJS transport requests. See sockjs-client issue 196. Instead, it does allow sending query parameters that you can use to send a token, but that has its own drawbacks (for example, the token may be inadvertently logged with the URL in server logs).

上述限制适用于基于浏览器的客户端,不适用于基于 Spring Java 的 STOMP 客户端,该客户端确实支持为 WebSocket 和 SockJS 请求发送头。

The preceding limitations are for browser-based clients and do not apply to the Spring Java-based STOMP client, which does support sending headers with both WebSocket and SockJS requests.

因此,希望避免使用 cookie 的应用程序可能没有任何很好的替代方案在 HTTP 协议级别进行验证。除了使用 cookie,他们可能更愿意在 STOMP 消息协议级别通过标头进行验证。这样做需要两个简单的步骤:

Therefore, applications that wish to avoid the use of cookies may not have any good alternatives for authentication at the HTTP protocol level. Instead of using cookies, they may prefer to authenticate with headers at the STOMP messaging protocol level. Doing so requires two simple steps:

  1. Use the STOMP client to pass authentication headers at connect time.

  2. Process the authentication headers with a ChannelInterceptor.

下面的示例使用服务器端配置来注册一个自定义验证拦截器。请注意,一个拦截器只需验证并在 CONNECT 消息 上设置用户标头。Spring 会记录并保存经过验证的用户,并将其与同一会话上的后续 STOMP 消息关联。下面的示例展示了如何注册一个自定义验证拦截器:

The next example uses server-side configuration to register a custom authentication interceptor. Note that an interceptor needs only to authenticate and set the user header on the CONNECT Message. Spring notes and saves the authenticated user and associate it with subsequent STOMP messages on the same session. The following example shows how to register a custom authentication interceptor:

  • Java

  • Kotlin

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.interceptors(new ChannelInterceptor() {
			@Override
			public Message<?> preSend(Message<?> message, MessageChannel channel) {
				StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
				if (StompCommand.CONNECT.equals(accessor.getCommand())) {
					// Access authentication header(s) and invoke accessor.setUser(user)
				}
				return message;
			}
		});
	}
}
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfiguration : WebSocketMessageBrokerConfigurer {

	override fun configureClientInboundChannel(registration: ChannelRegistration) {
		registration.interceptors(object : ChannelInterceptor {
			override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
				val accessor = MessageHeaderAccessor.getAccessor(message,
					StompHeaderAccessor::class.java)
				if (StompCommand.CONNECT == accessor!!.command) {
					// Access authentication header(s) and invoke accessor.setUser(user)
				}
				return message
			}
		})
	}
}

另请注意,当使用 Spring Security 对消息进行授权时,目前你需要确保验证 ChannelInterceptor 配置在 Spring Security 之前。最好通过在一个标记为 @Order(Ordered.HIGHEST_PRECEDENCE + 99)WebSocketMessageBrokerConfigurer 的自定义实现中声明自定义拦截器来实现。

Also, note that, when you use Spring Security’s authorization for messages, at present, you need to ensure that the authentication ChannelInterceptor config is ordered ahead of Spring Security’s. This is best done by declaring the custom interceptor in its own implementation of WebSocketMessageBrokerConfigurer that is marked with @Order(Ordered.HIGHEST_PRECEDENCE + 99).