Spring Tips: RSocket 和 Spring Security

工程 | Josh Long | 2020 年 2 月 20 日 | ...

你好,Spring 爱好者!在 Spring Tips 第 7 季 的第一集中,我们学习了如何使用 Spring Security 来保护 RSocket 服务。

作者:Josh Long (@starbuxman)

你好,Spring 爱好者!在本集中,我们将探讨 Spring Security 和 RSocket 的结合使用。RSocket 是一个与平台无关的、面向消息的协议,由 Netflix 和 Facebook 的工程师开发,支持线路上(on the wire)的 Reactive Streams 概念。该协议是一个有状态的、以连接为中心的协议:一个请求节点连接到一个响应节点并保持连接。一旦连接建立,任何一方都可以随时传输信息。连接是多路复用的,这意味着一个连接可以处理多个请求。RSocket 从根本上就是为了支持在传输消息负载的同时,传播诸如头信息和服务器健康状况等带外信息而设计的。因此,一个用户可以使用一个连接与一个服务交互,或者多个用户可以使用同一个连接。

在本视频中,我们基于 Spring Framework 5.2 的核心 RSocket 支持(以及非常方便的 @MessageMapping 组件模型)构建了一个 RSocket 客户端,然后以安全的方式连接到一个 RSocket 服务。

让我们来介绍一个基本的 RSocket 服务。你需要访问 Spring Initializr,并生成一个新的项目,同时选择 RSocket 和 Security,并且——重要的是——使用 Spring Boot 2.3 或更高版本。

package com.example.greetingsservice;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Supplier;
import java.util.stream.Stream;

@SpringBootApplication
public class GreetingsServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(GreetingsServiceApplication.class, args);
	}
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class GreetingResponse {
	private String message;
}

@Controller
class GreetingController {

	@MessageMapping("greetings")
	Flux<GreetingResponse> greet(@AuthenticationPrincipal Mono<UserDetails> user) {
		return user.map(UserDetails::getUsername).flatMapMany(GreetingController::greet);
	}

	private static Flux<GreetingResponse> greet(String name) {
		return Flux.fromStream(
			Stream
				.generate(() -> new GreetingResponse("Hello " + name + " @ " + Instant.now().toString())))
			.delayElements(Duration.ofSeconds(1));
	}
}

我之前制作了两个关于 RSocketSpring 对 RSocket 的支持 的视频,你可以在观看本视频之前参考它们。第一个视频介绍了原始的 RSocket API,第二个视频介绍了 Spring 中的组件模型。请参考它们来了解在本控制器中基本上正在发生什么。

Spring Security 提供了三种保护基于 RSocket 的服务的机制。BASIC 认证类似于 HTTP BASIC——它支持用户名和密码。它现在也已被弃用。因此,我们将在本视频中重点介绍 Simple 认证。Simple 认证也是基于用户名和密码的。RSocket 还支持基于 JWT 的认证。JWT 支持基于令牌的认证,可能是更复杂的安全用例更有吸引力的机制。(基于 RSocket 的 JWT 认证可能会成为另一个视频的主题。)

由于 RSocket 连接是有状态的且可以共享的,我们需要决定:是在连接创建时进行认证,还是在连接上传输的每条消息都进行认证?如果连接是共享的,我们希望每个用户为每个请求都提供自己的认证。

Spring Security 处理两个问题:认证和授权。它们是相关但正交的问题。认证回答了“谁在向系统发起请求?”这个问题。授权回答了“一旦他们进入系统,他们被允许做什么?”这个问题。

让我们来介绍应用程序的 Spring Security 配置。


@Configuration
@EnableRSocketSecurity
class RSocketSecurityConfiguration {

	@Bean
	RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
		var mh = new RSocketMessageHandler();
		mh.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver());
		mh.setRSocketStrategies(strategies);
		return mh;
	}

	@Bean
	MapReactiveUserDetailsService authentication() {
		var jlong = User.withDefaultPasswordEncoder().username("jlong").password("pw").roles("USER").build();
		var rwinch = User.withDefaultPasswordEncoder().username("rwinch").password("pw").roles("ADMIN", "USER").build();
		return new MapReactiveUserDetailsService(jlong, rwinch);
    }
    
    @Bean
	PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) {
		return security
			.authorizePayload(spec ->
				spec
					.route("greetings").authenticated()
					.anyExchange().permitAll()
			)
			.simpleAuthentication(Customizer.withDefaults())
			.build();
	}
}

安全配置包含三个 Bean。第一个 Bean,messageHandler,激活了 Spring Security 组件模型的一部分,它允许我们将认证的用户(使用 @AuthenticatedPrincipal 注解)注入到我们的处理程序方法中(那些用 @MessageMapping 注解的方法)。

第二个 Bean,authentication,安装了一个简单的用户名和密码字典。你可以连接到任何数量的不同身份提供者,但为了演示方便,我配置了一个内存中的 MapReactiveUserDetailsService

第三个 Bean,authorization,对我来说至少是最有趣的。这个 Bean 的目标是告诉框架哪些 RSocket 路由(在本例中是 greetings)是对请求可访问的。这应该是不言自明的:所有到 greetings 的请求都必须经过认证。否则,任何其他请求都可以不受检查地通过。

既然我们已经让它启动并运行了,让我们来看看客户端。

package com.example.greetingsclient;

import io.rsocket.metadata.WellKnownMimeType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.rsocket.metadata.SimpleAuthenticationEncoder;
import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import reactor.core.publisher.Mono;

@Log4j2
@SpringBootApplication
public class GreetingsClientApplication {

	private final MimeType mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
	private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("jlong", "pw");

	@SneakyThrows
	public static void main(String[] args) {
		SpringApplication.run(GreetingsClientApplication.class, args);
		System.in.read();
	}

	@Bean
	RSocketStrategiesCustomizer rSocketStrategiesCustomizer() {
		return strategies -> strategies.encoder(new SimpleAuthenticationEncoder());
	}

	@Bean
	RSocketRequester rSocketRequester(RSocketRequester.Builder builder) {
		return builder
//			.setupMetadata(this.credentials , this.mimeType)
			.connectTcp("localhost", 8888)
			.block();
	}

	@Bean
	ApplicationListener<ApplicationReadyEvent> ready(RSocketRequester greetings) {
		return event ->
			greetings
				.route("greetings")
				.metadata(this.credentials, this.mimeType)
				.data(Mono.empty())
				.retrieveFlux(GreetingResponse.class)
				.subscribe(gr -> log.info("secured response: " + gr.toString()));
	}
}


@Data
@AllArgsConstructor
@NoArgsConstructor
class GreetingResponse {
	private String message;
}

我们将向服务发送元数据。我们有两个选择。如果 RSocket 连接是共享的,那么我们希望为每个请求发送元数据。这就是我们在示例中所做的,因为它更可能发生。另一方面,如果你只需要认证一次,那么你可以在 rSocketRequester Bean 的连接建立时发送元数据。

我们在事件监听器中使用 RSocketRequester 客户端,在那里我们调用服务上的 greetings 路由。它基本上和以前一样,只是我们对请求中的元数据进行了编码以进行认证。

这篇博客只是触及了皮毛——观看视频以获取更多详细信息! :D

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有