接着Spring 5.2.2 WebSockets之STOMP的WebSocket服务端、信息流和注释的控制器继续讲STOMP。
8、简单代理
内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目的地的已连接客户端。代理支持类似路径的目的地,包括对Ant样式的目的地模式的订阅。
如果配置了任务定时器,那么简单代理支持STOMP心跳。为此,你可以声明自己的定时任务程序,也可以使用自动声明并在内部使用的定时任务程序。以下示例显示如何声明自己的计划程序:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { private TaskScheduler messageBrokerTaskScheduler; @Autowired public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) { this.messageBrokerTaskScheduler = taskScheduler; } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue/", "/topic/") .setHeartbeatValue(new long[] {10000, 20000}) .setTaskScheduler(this.messageBrokerTaskScheduler); // ... }}
9、外部代理
简单代理非常适合入门,但只支持STOMP命令的一个子集(它不支持ack、receipts和其他一些特性),依赖于一个简单的消息发送循环,不适合集群。另一种选择是,你可以升级应用程序以使用功能齐全的message broker。
请参阅所选消息代理(如RabbitMQ、ActiveMQ等)的STOMP文档,安装代理,并在启用STOMP支持的情况下运行它。然后可以在Spring配置中启用STOMP代理转发(而不是简单代理)。
下面的示例配置启用功能齐全的代理:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/topic", "/queue"); registry.setApplicationDestinationPrefixes("/app"); }}
以下示例显示了与前一个示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:message-broker application-destination-prefix="/app"> <websocket:stomp-endpoint path="/portfolio" /> <websocket:sockjs/> websocket:stomp-endpoint> <websocket:stomp-broker-relay prefix="/topic,/queue" /> websocket:message-broker>beans>
前面配置中的stomp broker转发是一个spring MessageHandler
,它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的TCP连接,将所有消息转发给它,然后通过WebSocket会话将从代理接收到的所有消息转发给客户端。本质上,它充当一个双向转发消息的“中继”。
添加io.projectreactor.netty:reactor-netty和io.netty:netty-all依赖项以进行TCP连接管理。
此外,应用程序组件(如HTTP请求处理方法、业务服务等)也可以向代理中继发送消息,如发送消息中所述,向订阅的WebSocket客户端广播消息。实际上,代理中继支持健壮和可伸缩的消息广播。
10、连接代理
STOMP代理中继维护与代理的单个“系统”TCP连接。此连接仅用于来自服务器端应用程序的消息,不用于接收消息。你可以连接配置STOMP凭据(即STOMP frame login
和passcode
headers)。这在XML命名空间和Java配置中都作为默认值guest
和guest
的systemLogin和systemPasscode
属性公开。
STOMP代理转发还为每个连接的WebSocket客户端创建单独的TCP连接。可以配置用于代表客户端创建的所有TCP连接的STOMP凭据。这在XML命名空间和Java配置中都作为clientLogin 和`clientPasscode
属性公开,默认值为guest和`guest。
stomp broker中继总是在它代表客户端转发给代理的每个CONNECT
frame上设置login
和passcode
headers。因此,WebSocket客户端不需要设置这些headers。它们被忽略了。正如身份验证部分所解释的,WebSocket客户端应该依赖HTTP身份验证来保护WebSocket端点并建立客户端标识。
stomp broker中继还通过“system”TCP连接向message broker发送和接收心跳信号。可以配置发送和接收心跳信号的间隔(默认情况下每个间隔10秒)。如果与代理的连接丢失,代理中继每隔5秒继续尝试重新连接,直到成功。
任何Spring bean都可以实现ApplicationListener在与代理的“system”连接丢失和重新建立时接收通知。例如,当没有活动的“system”连接时, Stock Quote的stock quotes服务可以停止尝试发送消息。
默认情况下,STOMP代理中继始终连接到同一主机和端口,并在连接丢失时根据需要重新连接。如果希望提供多个地址,则每次尝试连接时,可以配置地址供应商,而不是固定主机和端口。下面的示例演示如何执行此操作:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { // ... @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient()); registry.setApplicationDestinationPrefixes("/app"); } private ReactorNettyTcpClient<byte[]> createTcpClient() { return new ReactorNettyTcpClient<>( client -> client.addressSupplier(() -> ... ), new StompReactorNettyCodec()); }}
还可以使用virtualHost
属性配置STOMP代理中继。此属性的值被设置为每个CONNECT
frame的host
头,并且可能很有用(例如,在建立TCP连接的实际主机与提供基于云的STOMP服务的主机不同的云环境中)。
11、点作为分隔符
当消息路由到@MessageMapping方法时,它们将与AntPathMatcher匹配。默认情况下,模式应该使用斜杠 (/
)作为分隔符。这是web应用程序中的一个很好的规定,类似于httpurl。但是,如果你更习惯于消息传递规定,可以切换到使用点(.
)作为分隔符。
下面的示例演示如何在Java配置中执行此操作:
@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // ... @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setPathMatcher(new AntPathMatcher(".")); registry.enableStompBrokerRelay("/queue", "/topic"); registry.setApplicationDestinationPrefixes("/app"); }}
以下示例显示了与前一个示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher"> <websocket:stomp-endpoint path="/stomp"/> <websocket:stomp-broker-relay prefix="/topic,/queue" /> websocket:message-broker> <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"> <constructor-arg index="0" value="."/> bean>beans>
之后,控制器可以在@MessageMapping方法中使用点(.
)作为分隔符,如下例所示:
@Controller@MessageMapping("red")public class RedController { @MessageMapping("blue.{green}") public void handleGreen(@DestinationVariable String green) { // ... }}
客户端发送消息/app/red.blue.green123.
在前面的示例中,我们没有更改“代理中继”上的前缀,因为这些前缀完全依赖于外部消息代理。另一方面,“简单代理”确实依赖于配置的PathMatcher,因此,如果你切换分隔符,也适用于代理以及代理将目标从消息匹配到订阅模式的方式。
12、身份验证
每个stomp over websocket消息传递会话都从一个HTTP请求开始。这可以是升级到WebSockets的请求(即WebSocket握手),或者,在SockJS回退的情况下,是一系列SockJS HTTP传输请求。
许多web应用程序已经有了身份验证和授权来保护HTTP请求。通常,用户通过Spring Security某种机制进行身份验证,比如登录页面、HTTP基本身份验证或其他方式。已验证用户的安全上下文保存在HTTP会话中,并与同一基于cookie的会话中的后续请求相关联。
因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经有一个经过身份验证的用户可以通过HttpServletRequest#getUserPrincipal()进行访问。Spring会自动将该用户与为其创建的WebSocket或SockJS会话相关联,并且随后与通过用户头在该会话上传输的所有STOMP消息相关联。
简言之,一个典型的web应用程序只需要做它已经做的安全工作。通过基于cookie的HTTP会话(然后与为该用户创建的WebSocket或SockJS会话关联)维护的安全上下文在HTTP请求级别对用户进行身份验证,并在流经应用程序的每个Message
上生成一个用户headers 。
请注意,STOMP协议在CONNECT
帧上确实有login
和passcode
headers 。它们最初是为TCP上的STOMP而设计的,现在仍然需要。但是,对于STOMP over WebSocket,默认情况下,Spring忽略STOMP协议级别的授权headers ,假设用户已经在HTTP传输级别进行了身份验证,并期望WebSocket或SockJS会话包含经过身份验证的用户。
Spring Security提供WebSocket子协议授权,它使用ChannelInterceptor
根据消息中的用户headers 对消息进行授权。另外,Spring Session提供了一个WebSocket集成,确保当WebSocket会话仍然处于活动状态时,用户HTTP会话不会过期。
STOMP未完待续!
敬请持续关注。
欢迎关注和转发Spring中文社区(加微信群,可以关注后加我微信):