RscoketとSpring Secutiryの組み合わせを試す。

はじめに

Rsocketとはアプリケーションレイヤーのプロトコルです。テキストベースのプロトコルであるHTTPとは違いバイナリベースでもデータの送受信を行います。また、RsocketはUDPTCP、WebSocketなど複数のプロトコル上での実装があります。

その他にも以下の様な特徴がありまうす。

  • 双方向通信
  • 4つのインタラクションモデル
    • Request Response(1つのリクエストに対して1つのレスポンス)
    • Request Stream(1つのリクエストメッセージに対してレスポンスメッセージのストリーム)
    • Request Channel(リクエストメッセージのストリームに対してレスポンスメッセージのストリームを受信)
    • Fire and Forget(1メッセージを送信に対して、レスポンスはなし)
  • バックプレッシャー(リクエスト側での流量制御)のサポート

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つのインタラクションモデル
    • Request Response(1つのリクエストに対して1つのレスポンス)
    • Request Stream(1つのリクエストメッセージに対してレスポンスメッセージのストリーム)
    • Request Channel(リクエストメッセージのストリームに対してレスポンスメッセージのストリームを受信)
    • Fire and Forget(1メッセージを送信に対して、レスポンスはなし)
  • バックプレッシャー(リクエスト側での流量制御)のサポート

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と同じように扱えそうなので、それに慣れている人は結構直感的にコードをかけるのでは無いでしょうか。

参考資料