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 類中保護“即發即棄”訊息對映的示例

    @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() 方法傳遞使用者物件和在 (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)
    }

該測試斷言即發即棄呼叫的結果應該是一個異常,表明使用者被“拒絕”訪問 (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 社群所有即將舉行的活動。

檢視所有