Spring 小技巧:RSocket 與 Spring Security

工程 | Josh Long | 2020 年 2 月 20 日 | ...

嗨,Spring 愛好者們! 在 Spring 小技巧 第七季 的第一集中,我們將探討如何使用 Spring Security 來鎖定 RSocket 服務。

作者:Josh Long (@starbuxman)

嗨,Spring 愛好者們! 在這一集中,我們將探討如何一起使用 Spring Security 和 RSocket。 RSocket 是 Netflix 和 Facebook 工程師開發的一種與 payload 和平台無關的線路協定,它支援線路上的 Reactive Streams 概念。 該協定是一種以有狀態連線為中心的協定:請求者節點連線並保持連線到另一個回應者節點。 連線後,任何一方都可以隨時傳輸資訊。 連線是多路複用的,這意味著一個連線可以處理多個請求。 RSocket 從一開始就被設計為支援傳播帶外資訊,例如標頭和服務健康資訊,以及 payload 本身。 因此,一個使用者可以使用與一個服務的連線,或者多個使用者可以使用相同的連線。

在這個影片中,我們以 Spring Framework 5.2 的核心 RSocket 支援(以及非常方便的 @MessageMapping 元件模型)為基礎,建立一個 RSocket 用戶端,然後以安全的方式連線到 RSocket 服務。

讓我們介紹一個基本的 RSocket 服務。 您需要前往 Spring Initializr 並使用選定的 RSocket 和 Security 產生一個新專案,並且 - 重要的是 - Spring Boot 2.3 或更高版本。

package com.example.greetingsservice;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Supplier;
import java.util.stream.Stream;

@SpringBootApplication
public class GreetingsServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(GreetingsServiceApplication.class, args);
	}
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class GreetingResponse {
	private String message;
}

@Controller
class GreetingController {

	@MessageMapping("greetings")
	Flux<GreetingResponse> greet(@AuthenticationPrincipal Mono<UserDetails> user) {
		return user.map(UserDetails::getUsername).flatMapMany(GreetingController::greet);
	}

	private static Flux<GreetingResponse> greet(String name) {
		return Flux.fromStream(
			Stream
				.generate(() -> new GreetingResponse("Hello " + name + " @ " + Instant.now().toString())))
			.delayElements(Duration.ofSeconds(1));
	}
}

我還製作了另外兩個關於 RSocketSpring 對 RSocket 的支援 的影片,您可以在觀看這個影片之前參考它們。 第一個介紹了原始 RSocket API,第二個介紹了 Spring 中的元件模型。 請參考這些內容以了解控制器中發生的事情。

Spring Security 提供了三種機制來保護基於 RSocket 的服務。 BASIC 驗證有點像 HTTP BASIC - 它支援使用者名稱和密碼。 它現在也被棄用了。 因此,我們將在本影片中重點介紹簡單驗證。 簡單驗證也是基於使用者名稱和密碼的。 RSocket 也支援基於 JWT 的驗證。 JWT 支援基於 Token 的驗證,對於複雜的安全用例來說,它可能更有趣。 (基於 RSocket 的 JWT 驗證可能將是另一個影片的主題。)

由於 RSocket 連線可以是有狀態且共享的,因此我們需要決定:我們是在建立連線時進行驗證,還是針對透過連線傳送的每條訊息進行驗證? 如果它是共享的,我們將希望每個使用者為每個請求提供自己的驗證。

Spring Security 解決了兩個問題:驗證和授權。 這些是相關但正交的問題。 驗證回答了問題:誰在向系統發出請求? 授權回答了問題:一旦他們進入系統,他們被允許做什麼?

讓我們介紹應用程式的 Spring Security 配置。


@Configuration
@EnableRSocketSecurity
class RSocketSecurityConfiguration {

	@Bean
	RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
		var mh = new RSocketMessageHandler();
		mh.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver());
		mh.setRSocketStrategies(strategies);
		return mh;
	}

	@Bean
	MapReactiveUserDetailsService authentication() {
		var jlong = User.withDefaultPasswordEncoder().username("jlong").password("pw").roles("USER").build();
		var rwinch = User.withDefaultPasswordEncoder().username("rwinch").password("pw").roles("ADMIN", "USER").build();
		return new MapReactiveUserDetailsService(jlong, rwinch);
    }
    
    @Bean
	PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) {
		return security
			.authorizePayload(spec ->
				spec
					.route("greetings").authenticated()
					.anyExchange().permitAll()
			)
			.simpleAuthentication(Customizer.withDefaults())
			.build();
	}
}

安全配置包含三個 bean。 第一個是 messageHandler,它啟動 Spring Security 元件模型的部分,讓我們將經過驗證的使用者(使用 @AuthenticatedPrincipal 註解)注入到我們的處理程式方法(那些使用 @MessageMapping 註解的方法)中。

第二個 bean,authentication,安裝了一個簡單的使用者名稱和密碼字典。 您可以與任意數量的不同身分提供者進行通訊,但為了便於示範,我配置了一個記憶體中的 MapReactiveUserDetailsService

第三個 bean,authorization,至少在我看來是最有趣的。 這個 bean 的目標是告訴框架哪些 RSocket 路由(在本例中是 greetings)可以被請求存取。 希望這是自我描述的:對 greetings 的所有請求都應該經過驗證。 否則,任何其他請求都允許未經檢查地通過。

現在我們已經啟動並運行了它,讓我們看看客戶端。

package com.example.greetingsclient;

import io.rsocket.metadata.WellKnownMimeType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.rsocket.metadata.SimpleAuthenticationEncoder;
import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import reactor.core.publisher.Mono;

@Log4j2
@SpringBootApplication
public class GreetingsClientApplication {

	private final MimeType mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
	private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("jlong", "pw");

	@SneakyThrows
	public static void main(String[] args) {
		SpringApplication.run(GreetingsClientApplication.class, args);
		System.in.read();
	}

	@Bean
	RSocketStrategiesCustomizer rSocketStrategiesCustomizer() {
		return strategies -> strategies.encoder(new SimpleAuthenticationEncoder());
	}

	@Bean
	RSocketRequester rSocketRequester(RSocketRequester.Builder builder) {
		return builder
//			.setupMetadata(this.credentials , this.mimeType)
			.connectTcp("localhost", 8888)
			.block();
	}

	@Bean
	ApplicationListener<ApplicationReadyEvent> ready(RSocketRequester greetings) {
		return event ->
			greetings
				.route("greetings")
				.metadata(this.credentials, this.mimeType)
				.data(Mono.empty())
				.retrieveFlux(GreetingResponse.class)
				.subscribe(gr -> log.info("secured response: " + gr.toString()));
	}
}


@Data
@AllArgsConstructor
@NoArgsConstructor
class GreetingResponse {
	private String message;
}

我們將向服務傳送元數據。 我們有兩個選擇。 如果與 RSocket 連線的連線是共享的,那麼我們希望為每個請求傳送元數據。 這就是我們在這個範例中所做的,因為這是更可能發生的情況。 另一方面,如果您只需要驗證一次,那麼您可以在 rSocketRequester bean 中建立連線時傳送元數據。

我們在事件監聽器中使用 RSocketRequester 客戶端,在其中我們呼叫服務上的 greetings 路由。 它基本上和以前一樣,略有不同的是我們正在請求中編碼元數據以進行驗證。

我們才剛開始觸及這個部落格中的服務表面 - 請觀看影片以了解更多詳細資訊! :D

取得 Spring 電子報

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

訂閱

取得領先

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

了解更多

取得支援

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

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

檢視全部