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 社群的所有近期活動。

檢視全部