RscoketとSpring Secutiryの組み合わせを試す。
はじめに
Rsocketとはアプリケーションレイヤーのプロトコルです。テキストベースのプロトコルであるHTTPとは違いバイナリベースでもデータの送受信を行います。また、RsocketはUDP、TCP、WebSocketなど複数のプロトコル上での実装があります。
その他にも以下の様な特徴がありまうす。
Springでは5.2からRsoketをサポートしており、Bootも2.2からRsocketをサポートしています。 Spring Securityも同様に5.2からRsokcetの対応がされているようです。
基本的には以下のブログの内容を個人的に試してみて、気になったところを深堀していくような感じになります。 * Spring Tips: RSocket and Spring Security
また、実装はBootを用います。言語はJavaです。
Rsocketのアプリを作成する。
Spring Initializarをでブランクプロジェクトを作成します。その際にBootのバージョンは2.3以上を選択肢、依存にはRsocketとSecurityを追加してください。
@SpringBootApplication public class RsocketSecuritySampleApplication { public static void main(String[] args) { SpringApplication.run(RsocketSecuritySampleApplication.class, args); } } @Data @AllArgsConstructor @NoArgsConstructor class GreetingResponse { private String message; } @Controller class GreetingController { @MessageMapping("greeting") 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))) .delayElements(Duration.ofSeconds(1)); }
まずは、さくっと、レスポンス用のメッセージクラスと、ハンドラーを実装します。コントローラーなどは特にJavaのファイルとしてはわけずに作成してしまいました。
通常のSecutiryと同様に@AuthenticationPrincipal
でUserDetailを取れるみたいです。ただその受け取り型がMonoでラップされています。
しれっと、もとのブログの方でしれっとLombokが追加されていたので依存に加えました。
SecutiryCofigを作成する
では、主題の方セキュリティの設定をしていきます。がその前にちょっとだけRsocketのSpring Securityについてまとめます。 Rsocketベースのサービスに対して、Spring Secutiryでは以下の2つのタイプの認証プロトコルをサポートします。(これもとのブログに3っつって書かれてたけど、実際ドキュメント見ても2つしかなかったので、多分誤字っぽい...)
今回はブログに従って、BASIC認証の方を試してみたいと思います。 以下のようにセキュリティのコンフィグを書きます。
@Configuration @EnableRSocketSecurity public class SecurityConfig { @Bean RSocketMessageHandler messageHandler(RSocketStrategies strategies) { RSocketMessageHandler mh = new RSocketMessageHandler(); mh.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver()); mh.setRSocketStrategies(strategies); は return mh; } @Bean MapReactiveUserDetailsService authentication() { UserDetails heno = User.withDefaultPasswordEncoder().username("heno").password("pw").roles("USER").build(); UserDetails mohezi = User.withDefaultPasswordEncoder().username("mohezi").password("pw").roles("ADMIN", "USER").build(); return new MapReactiveUserDetailsService(heno, mohezi); } @Bean PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) { return security.authorizePayload(spec -> spec.route("greetings").authenticated().anyExchange()) .simpleAuthentication(Customizer.withDefaults()) .build(); } }
JavaConfigでは複数の設定をしていきます。
まず、@EnableRSocketSecurity
で、Rsocketのセキュリティを有効にしています。
1つ目のBeanでは、AuthenticationPrincipalArgumentResolver
を登録し、ハンドラーメソッドの引数として@AuthenticatedPrincipal
を介してUserDetails
をうけとれるようにしています。先程Mono<UserDetails>
をハンドラーで受け取れていたのはこの辺の設定がかんでいたみたいですね。
2つめのBeanではユーザの登録を行っています。
ReactiveUserDetailsService
を実装してBean登録すればカスタムしたUserServiceを登録することも可能なようですが、今回はサンプルに従って、イン・メモリのプロバイダーである。MapReactiveUserDetailsService
を登録しています。
3つめのBeanではAPIエンドポイントに対する認可制御の設定を行っています。PayloadSocketAcceptorInterceptorはRsocket APIに対してペイロードの交換をインターセプトする役割を持っています。上記の設定ではgreetings
のリクエストに対して、認証済みであればすべての通信を許可する認可を行っています。
これで、サーバ側の実装は完了です。
クライアントの実装
Spring Initializarでクライアント側のブランクプロジェクトを作成します。その際にBootのバージョンは2.3以上を選択肢、依存にはRsocketとSecurityを追加してください。しれっとLombokも追加しておきます。 クライアントは下記のようなコードになります。
@Log4j2 @SpringBootApplication public class RsockerSecurityClientApplication { private final MimeType mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("mohezi", "pw"); @SneakyThrows public static void main(String[] args) { SpringApplication.run(RsockerSecurityClientApplication.class, args); System.in.read(); } @Bean RSocketStrategiesCustomizer rSocketStrategiesCustomizer() { return strategies -> strategies.encoder(new SimpleAuthenticationEncoder()); } @Bean RSocketRequester rSocketRequester(RSocketRequester.Builder builder) { return builder // Beanと同じライフサイクルでのメタデータの登録をしておけば // 毎回パスワードとユーザネームを設定し直す必要が無い //.setupMetadata(this.credentials, this.mimeType) .connectTcp("localhost", 8080) .block(); } @Bean ApplicationListener<ApplicationReadyEvent> ready(RSocketRequester requester) { return event -> { requester .route("greetings") .metadata(this.credentials, this.mimeType) .data(Mono.empty()) .retrieveFlux(GreetingResponse.class) .subscribe(gr -> log.info("secured response" + gr.getMessage())); }; } } @Data @AllArgsConstructor @NoArgsConstructor class GreetingResponse { private String message; }
まずは下記のコードの部分を見てみます。
private final MimeType mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("mohezi", "pw");
認証のためのメタデータを用意します。RSocketの認証を行なうことはMimeTypeとして定義するようです。また、ユーザネームとパスワードはメタデータとしてUsernamePasswordMetadata
とじて準備しています。ユーザネームとパスワードは${username-bytes-length}${username-bytes}${password-bytes}
の形式でエンコードされペイロードのメタデータとして送信されるようです。
準備メタデータはRsocketのリクエスターをBean登録する際にメタデータを設定していればBeanと同じライフサイクルでメタデータを登録することも可能ですし、リクエストごとにメタデータとして登録することも可能なようです。 ちなみに、直感的ではありますが、リクエストごととリクエスターごとの設定ではリクエストごとの設定が優先されます。
このアプリを起動してみると以下のようなログを吐きます。
2020-02-24 13:27:12.711 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:13.702 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:14.703 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:15.704 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:16.705 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:17.706 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi
きちんと通信が行えているようですね。
では、以下のようにわざと間違えたパスワードを送信して結果を見てみます。
private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("mohezi", "failedPw");
クライアント側で以下のようなエクセプションを吐き落ちるようになりました。
reactor.core.Exceptions$ErrorCallbackNotImplemented: io.rsocket.exceptions.RejectedSetupException: Invalid Credentials Caused by: io.rsocket.exceptions.RejectedSetupException: Invalid Credentials
きちんとセキュリティの制御がされているようですね。
まとめ
Spring SecurityのRScoketへの対応を少し触ってみました。認可の制御やUserDetailの設定などは今までのSpring Securityと同じように扱えそうなので、それに慣れている人は結構直感的にコードをかけるのでは無いでしょうか。
参考資料
- spring-security releases
- Spring Tips: RSocket and Spring Security
- Spring Update in 2019
RSocket Security その他にも以下の様な特徴がありまうす。
双方向通信
- 4つのインタラクションモデル
- バックプレッシャー(リクエスト側での流量制御)のサポート
Springでは5.2からRsoketをサポートしており、Bootも2.2からRsocketをサポートしています。 Spring Securityも同様に5.2からRsokcetの対応がされているようです。
基本的には以下のブログの内容を個人的に試してみて、気になったところを深堀していくような感じになります。 * Spring Tips: RSocket and Spring Security
また、実装はBootを用います。言語はJavaです。
Rsocketのアプリを作成する。
Spring Initializarをでブランクプロジェクトを作成します。その際にBootのバージョンは2.3以上を選択肢、依存にはRsocketとSecurityを追加してください。
@SpringBootApplication public class RsocketSecuritySampleApplication { public static void main(String[] args) { SpringApplication.run(RsocketSecuritySampleApplication.class, args); } } @Data @AllArgsConstructor @NoArgsConstructor class GreetingResponse { private String message; } @Controller class GreetingController { @MessageMapping("greeting") 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))) .delayElements(Duration.ofSeconds(1)); }
まずは、さくっと、レスポンス用のメッセージクラスと、ハンドラーを実装します。コントローラーなどは特にJavaのファイルとしてはわけずに作成してしまいました。
通常のSecutiryと同様に@AuthenticationPrincipal
でUserDetailを取れるみたいです。ただその受け取り型がMonoでラップされています。
しれっと、もとのブログの方でしれっとLombokが追加されていたので依存に加えました。
SecutiryCofigを作成する
では、主題の方セキュリティの設定をしていきます。がその前にちょっとだけRsocketのSpring Securityについてまとめます。 Rsocketベースのサービスに対して、Spring Secutiryでは以下の2つのタイプの認証プロトコルをサポートします。(これもとのブログに3っつって書かれてたけど、実際ドキュメント見ても2つしかなかったので、多分誤字っぽい...)
今回はブログに従って、BASIC認証の方を試してみたいと思います。 以下のようにセキュリティのコンフィグを書きます。
@Configuration @EnableRSocketSecurity public class SecurityConfig { @Bean RSocketMessageHandler messageHandler(RSocketStrategies strategies) { RSocketMessageHandler mh = new RSocketMessageHandler(); mh.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver()); mh.setRSocketStrategies(strategies); は return mh; } @Bean MapReactiveUserDetailsService authentication() { UserDetails heno = User.withDefaultPasswordEncoder().username("heno").password("pw").roles("USER").build(); UserDetails mohezi = User.withDefaultPasswordEncoder().username("mohezi").password("pw").roles("ADMIN", "USER").build(); return new MapReactiveUserDetailsService(heno, mohezi); } @Bean PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) { return security.authorizePayload(spec -> spec.route("greetings").authenticated().anyExchange()) .simpleAuthentication(Customizer.withDefaults()) .build(); } }
JavaConfigでは複数の設定をしていきます。
まず、@EnableRSocketSecurity
で、Rsocketのセキュリティを有効にしています。
1つ目のBeanでは、AuthenticationPrincipalArgumentResolver
を登録し、ハンドラーメソッドの引数として@AuthenticatedPrincipal
を介してUserDetails
をうけとれるようにしています。先程Mono<UserDetails>
をハンドラーで受け取れていたのはこの辺の設定がかんでいたみたいですね。
2つめのBeanではユーザの登録を行っています。
ReactiveUserDetailsService
を実装してBean登録すればカスタムしたUserServiceを登録することも可能なようですが、今回はサンプルに従って、イン・メモリのプロバイダーである。MapReactiveUserDetailsService
を登録しています。
3つめのBeanではAPIエンドポイントに対する認可制御の設定を行っています。PayloadSocketAcceptorInterceptorはRsocket APIに対してペイロードの交換をインターセプトする役割を持っています。上記の設定ではgreetings
のリクエストに対して、認証済みであればすべての通信を許可する認可を行っています。
これで、サーバ側の実装は完了です。
クライアントの実装
Spring Initializarでクライアント側のブランクプロジェクトを作成します。その際にBootのバージョンは2.3以上を選択肢、依存にはRsocketとSecurityを追加してください。しれっとLombokも追加しておきます。 クライアントは下記のようなコードになります。
@Log4j2 @SpringBootApplication public class RsockerSecurityClientApplication { private final MimeType mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("mohezi", "pw"); @SneakyThrows public static void main(String[] args) { SpringApplication.run(RsockerSecurityClientApplication.class, args); System.in.read(); } @Bean RSocketStrategiesCustomizer rSocketStrategiesCustomizer() { return strategies -> strategies.encoder(new SimpleAuthenticationEncoder()); } @Bean RSocketRequester rSocketRequester(RSocketRequester.Builder builder) { return builder // Beanと同じライフらいクルでのメタデータの登録をしておけば // 毎回パスワードとユーザネームを設定し直す必要が無い //.setupMetadata(this.credentials, this.mimeType) .connectTcp("localhost", 8080) .block(); } @Bean ApplicationListener<ApplicationReadyEvent> ready(RSocketRequester requester) { return event -> { requester .route("greetings") .metadata(this.credentials, this.mimeType) .data(Mono.empty()) .retrieveFlux(GreetingResponse.class) .subscribe(gr -> log.info("secured response" + gr.getMessage())); }; } } @Data @AllArgsConstructor @NoArgsConstructor class GreetingResponse { private String message; }
まずは下記のコードの部分を見てみます。
private final MimeType mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("mohezi", "pw");
認証のためのメタデータを用意します。RSocketの認証を行なうことはMimeTypeとして定義するようです。また、ユーザネームとパスワードはメタデータとしてUsernamePasswordMetadata
とじて準備しています。ユーザネームとパスワードは${username-bytes-length}${username-bytes}${password-bytes}
の形式でエンコードされペイロードのメタデータとして送信されるようです。
準備メタデータはRsocketのリクエスターをBean登録する際にメタデータを設定していればBeanと同じライフサイクルでメタデータを登録することも可能ですし、リクエストごとにメタデータとして登録することも可能なようです。 ちなみに、直感的ではありますが、リクエストごととリクエスターごとの設定ではリクエストごとの設定が優先されます。
このアプリを起動してみると以下のようなログを吐きます。
2020-02-24 13:27:12.711 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:13.702 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:14.703 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:15.704 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:16.705 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi 2020-02-24 13:27:17.706 INFO 7501 --- [tor-tcp-epoll-1] r.s.c.r.RsockerSecurityClientApplication : secured responseHello, mohezi
きちんと通信が行えているようですね。
では、以下のようにわざと間違えたパスワードを送信して結果を見てみます。
private final UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("mohezi", "failedPw");
クライアント側で以下のようなエクセプションを吐き落ちるようになりました。
reactor.core.Exceptions$ErrorCallbackNotImplemented: io.rsocket.exceptions.RejectedSetupException: Invalid Credentials Caused by: io.rsocket.exceptions.RejectedSetupException: Invalid Credentials
きちんとセキュリティの制御がされているようですね。
まとめ
Spring SecurityのRScoketへの対応を少し触ってみました。認可の制御やUserDetailの設定などは今までのSpring Securityと同じように扱えそうなので、それに慣れている人は結構直感的にコードをかけるのでは無いでしょうか。