RSocket 入门:Spring Security

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

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

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

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

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

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

步骤 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)处配置的RSocketMessageHandlerbean会自动将用户凭据转换为UserDetails对象。在(5)处设置的MapReactiveUserDetailsServicebean为 Spring 提供了一个硬编码的用户数据库。以这种方式手动提供用户数据库不太现实,但对于此演示来说已经足够了。您可以阅读稍后如何使用其他身份提供程序完成此操作

最后,(6)处的PayloadSocketAcceptorInterceptorbean指定了用户可以使用应用程序执行的操作。在这种情况下,用户必须在连接或被授予任何服务器端功能的访问权限之前进行身份验证。

2.2 保护您的 RSocket 方法

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

    @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)点定义的编码 mimetype。新的SimpleAuthenticationEncoder(4)放置在用于此连接的RSocketStrategies中。此对象负责将 UsernamePasswordMetadata(2)编码为正确的 mimetype(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)
    }

该测试断言fire and forget调用的结果应该是一个异常,说明用户被“拒绝”访问(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社区中所有即将举行的活动。

查看全部