领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多嗨,Spring 粉丝们!在本期《Spring 提示》第七季的第一集中,我们将探讨如何使用 Spring Security 锁定 RSocket 服务。
嗨,Spring 粉丝们!在本期节目中,我们将探讨如何将 Spring Security 和 RSocket 结合使用。RSocket 是一种由 Netflix 和 Facebook 的工程师开发的、与负载和平台无关的线协议,它支持线上的 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));
}
}
我之前录制了另外两个关于 RSocket 和 Spring 对 RSocket 的支持 的视频,您可以在观看本视频之前参考它们。第一个介绍了原始 RSocket API,第二个介绍了 Spring 中的组件模型。请参考这些视频,以便了解在这个控制器中基本上发生了什么。
Spring Security 提供了三种保护基于 RSocket 的服务的方法。BASIC 身份验证有点类似于 HTTP BASIC - 它支持用户名和密码。它现在也已弃用。因此,在本视频中我们将重点关注简单身份验证。简单身份验证也基于用户名和密码。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。第一个,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