RSocket 入门:Spring Security

工程 | Ben Wilcock | 2020 年 6 月 17 日 | ...

阅读时间:约 6 分钟 编码时间:约 20 分钟

如果您一直在关注我的 RSocket 系列文章,您已经学会了如何使用 Spring Boot 构建客户端-服务器应用程序。在今天的练习中,您将学习如何为您的 RSocket 应用程序添加安全性。

当您使用 Spring Security 时,保护 RSocket 应用程序的任务会大大简化。Spring Security 是任何生产应用程序必不可少的模块。它允许您轻松插入许多不同的身份验证提供者,并根据用户的身份和角色限制他们对应用程序的访问。

正如您将看到的,保护应用程序所需的代码非常简单。但由于安全性是一个“横切”关注点,因此更改确实会触及代码的几个不同部分。自己进行这些更改并不困难,但像往常一样,完整的代码示例可在GitHub 上找到。

注意:在撰写本文时,RSocket 的安全扩展仍在开发中。您可以在此处关注其进展。在本次练习中,我们将使用简单认证(Simple Authentication),它带有警告:“简单认证以明文形式传输用户名和密码。此外,它不保护与其一起传输的载荷的真实性或机密性。这意味着使用的传输层应提供真实性和机密性,以保护用户名和密码以及相应的载荷。”

步骤 1:添加 Spring Security 依赖项

rsocket-clientrsocket-server 项目的 POM.xml 文件中,添加以下安全依赖项

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-rsocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-messaging</artifactId>
        </dependency>

这些依赖项将 Spring Security 集成到您的 RSocket 应用程序中。包含 spring-boot-starter-security 包意味着大部分配置都会自动完成。

步骤 2:保护您的 RSocket 服务器

保护您的 RSocket 响应器最好分两个阶段完成。首先,添加一个安全配置类;其次,保护您的 RSocket 响应器方法。

注意:这些更改将暂时破坏您在上一个教程中添加的集成测试。别担心;稍后我将向您展示如何再次修复它。

2.1 配置 Spring Security

为了自定义 Spring Security 的配置,请在您的 rsocket-server 项目中添加一个名为 RSocketSecurityConfig.java 的新类,其中包含以下代码。

注意:导入语句缺失。当出现提示时,请让您的 IDE 为您添加。

@Configuration // (1)
@EnableRSocketSecurity // (2)
@EnableReactiveMethodSecurity // (3)
public class RSocketSecurityConfig {

    @Bean // (4)
    RSocketMessageHandler messageHandler(RSocketStrategies strategies) {

        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver());
        handler.setRSocketStrategies(strategies);
        return handler;
    }

    @Bean // (5)
    MapReactiveUserDetailsService authentication() {
        //This is NOT intended for production use (it is intended for getting started experience only)
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("pass")
                .roles("USER")
                .build();

        UserDetails admin = User.withDefaultPasswordEncoder()
                .username("test")
                .password("pass")
                .roles("NONE")
                .build();

        return new MapReactiveUserDetailsService(user, admin);
    }

    @Bean // (6)
    PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) {
        security.authorizePayload(authorize ->
                authorize
                        .anyExchange().authenticated() // all connections, exchanges.
        ).simpleAuthentication(Customizer.withDefaults());
        return security.build();
    }

指定 @Configuration (1) 告诉 Spring Boot 这是一个配置类。@EnableRSocketSecurity 注解 (2) 激活 Spring 用于 RSocket 的安全特性。设置 @EnableReactiveMethodSecurity (3) 允许您保护您的响应式方法。

在 (4) 处配置的 RSocketMessageHandler Bean 会自动将用户凭据转换为 UserDetails 对象。在 (5) 处设置的 MapReactiveUserDetailsService Bean 为 Spring 提供了一个硬编码的用户数据库。以这种方式手动提供用户数据库不是很现实,但足以用于本次演示。您可以在稍后阅读关于如何与其他身份提供者一起完成此操作

最后,在 (6) 处的 PayloadSocketAcceptorInterceptor Bean 指定了用户可以使用应用程序执行的操作。在本例中,用户必须先进行身份验证才能建立连接或获得对任何服务器端功能的访问权限。

2.2 保护您的 RSocket 方法

用户的角色决定了他们可以访问的方法。在这种情况下,使用 Spring Security 的 @PreAuthorize 注解配置这种“基于角色的访问控制”。以下代码显示了此注解的实际示例——保护 RSocketController 类中的“即发即弃”消息映射

    @PreAuthorize("hasRole('USER')") // (1)
    @MessageMapping("fire-and-forget")
    public Mono<Void> fireAndForget(final Message request, @AuthenticationPrincipal UserDetails user) { // (2)
        log.info("Received fire-and-forget request: {}", request);
        log.info("Fire-And-Forget initiated by '{}' in the role '{}'", user.getUsername(), user.getAuthorities());
        return Mono.empty();
    }

@PreAuthorize("hasRole('USER')") 注解 (1) 确保只有具有 ‘ROLE_USER’ 权限的用户才能访问此方法。在上面的 2.1 节中,您创建了一个具有此角色的用户。

如果您眼光特别敏锐,您会注意到 fireAndForget() 方法签名中还有两个其他变化。首先是方法参数现在包含 @AuthenticationPrincipal UserDetails user (2)。Spring 会自动提供此 user 对象。其次,返回参数现在是 Mono<Void> 而不是普通的 'void'。进行此更改是因为 @EnableReactiveMethodSecurity 要求返回值来自 Project Reactor(即 Flux 或 Mono)。

步骤 3:为您的客户端添加安全性

在代码示例中,客户端进行了一些代码更改。其中大部分与安全性无关。主要更改只是为了在使用安全的服务器端 RSocket 响应器时使客户端更易于使用。在本节中,您将只介绍安全相关的更改。有关附加代码,请参阅代码示例

客户端的安全更改都与它如何连接到 RSocket 服务器有关。连接代码已从类构造函数移出,并移到一个新的 login() 方法中。此登录方法要求用户在登录时提供其用户名和密码。这些凭据将成为 RSocket 连接的元数据。登录命令的代码如下

private static final MimeType SIMPLE_AUTH = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); // (1)

@ShellMethod("Login with your username and password.")
    public void login(String username, String password) {
        SocketAcceptor responder = RSocketMessageHandler.responder(rsocketStrategies, new ClientHandler());

        UsernamePasswordMetadata user = new UsernamePasswordMetadata(username, password); // (2)

        this.rsocketRequester = rsocketRequesterBuilder
                .setupRoute("shell-client")
                .setupData(CLIENT_ID)
                .setupMetadata(user, SIMPLE_AUTH) // (3)
                .rsocketStrategies(builder ->
                        builder.encoder(new SimpleAuthenticationEncoder())) // (4)
                .rsocketConnector(connector -> connector.acceptor(responder))
                .connectTcp("localhost", 7000)
                .block();

 // ...connection handling code omitted. See the sample for details.
    }

此代码看起来与旧的构造函数代码非常相似。在添加安全性方面,最相关的代码行如下

SIMPLE_AUTH 静态变量 (1) 声明了当作为连接元数据传递时,您的用户对象应如何编码。定义了一个新的 UsernamePasswordMetadata (2),其中包含用户登录时提供的凭据。连接时 (3),setupMetadata() 方法传递 user 对象和在点 (1) 定义的编码 MIME 类型。新的 SimpleAuthenticationEncoder (4) 被放置在此连接使用的 RSocketStrategies 中。此对象负责将 UsernamePasswordMetadata (2) 编码为正确的 MIME 类型 (1)。

示例代码中的进一步更改允许用户 logout。这意味着用户可以在不同身份之间切换,而无需每次都重新启动客户端。

步骤 4:测试安全功能是否正常

在您添加 Spring Security 的依赖项和安全配置类的那一刻,您的代码变得更安全了。与此同时,您的集成测试停止工作,因为它不遵守新的安全设置。

要修复 RSocketClientToServerITest.java 集成测试,请修改 setupOnce() 方法,以便将用户对象添加到连接元数据中。所需的代码看起来与您在客户端登录方法中刚刚看到的非常相似

@BeforeAll
    public static void setupOnce(@Autowired RSocketRequester.Builder builder,
                                 @LocalRSocketServerPort Integer port,
                                 @Autowired RSocketStrategies strategies) {

        SocketAcceptor responder = RSocketMessageHandler.responder(strategies, new ClientHandler());
        credentials = new UsernamePasswordMetadata("user", "pass");
        mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());

        requester = builder
                .setupRoute("shell-client")
                .setupData(UUID.randomUUID().toString())
                .setupMetadata(credentials, mimeType)
                .rsocketStrategies(b ->
                        b.encoder(new SimpleAuthenticationEncoder()))
                .rsocketConnector(connector -> connector.acceptor(responder))
                .connectTcp("localhost", port)
                .block();
    }

将凭据添加到连接后,测试功能正常。要验证这一点,请在终端中导航到您的 rsocket-server 文件夹并运行 Maven verify 命令。此操作将运行修订后的集成测试。

./mvnw clean verify

恭喜您。您的集成测试现在再次运行并通过了!

更多内容

我在 rsocket-server 示例代码中包含了另外两个集成测试。第一个是 RSocketClientToSecuredServerITest.java,它使用 RSocketSecurityConfig 类中的 test 用户凭据来确认不具有 USER 角色的用户无法访问服务器端方法。测试方法代码如下所示

    @Test
    public void testFireAndForget() {
        // Send a fire-and-forget message
        Mono<Void> result = requester
                .route("fire-and-forget")
                .data(new Message("TEST", "Fire-And-Forget"))
                .retrieveMono(Void.class);

        // Assert that the user 'test' is DENIED access to the method.
        StepVerifier
                .create(result)
                .verifyErrorMessage("Denied"); // (1)
    }

测试断言,即发即弃调用的结果应该是一个异常,指示用户被“拒绝”访问 (1)。

另一个新测试断言,使用虚假凭据的用户无法建立 RSocket 连接。此测试的代码位于文件 RSocketClientDeniedConnectionToSecuredServerITest.java 中。

最后,请随时在命令行尝试更新后的 rsocket-client。您可以使用各种凭据登录,并亲自尝试访问服务器端方法。

cd rsocket-client
./mvnw clean package spring-boot:run

# To get help with all the available commands
shell:> help

# To access to all features.
shell:> login user pass 

# To access no features.
shell:> login test pass

# To exit the client
shell:> exit

关于 RSocket 和 Spring Security 的介绍就到这里。希望您觉得它有用。您还可以在这个 Spring Tips 视频中查看 Josh Long 如何处理同一主题。像往常一样,欢迎点赞、分享并在下方留言。要获取未来的新闻和更新,何不在 Twitter 上关注我

获取 Spring 邮件列表

订阅 Spring 邮件列表,保持联系

订阅

保持领先

VMware 提供培训和认证,助力您加速发展。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部