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 的新類別,其中包含以下程式碼。

注意:import 陳述式遺失了。請在出現提示時要求您的 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 類別中的「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 的導覽。我希望您覺得它很有用。您也可以觀看 Josh Long 在此 Spring Tips 影片中如何處理相同主題。一如既往,請隨時按讚、分享並在下方留言。如需未來的新聞和更新,何不在 Twitter 上追蹤我

取得 Spring 電子報

隨時掌握 Spring 電子報的最新資訊

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部