テクノロジー / セキュリティ

【Spring】Spring Securityでエンドポイント毎に異なる認証を行う

2025.10.03

株式会社GSI ITエンジニア募集【成長をカタチに】

初めに

認証方式に OpenID Connect を採用したWebアプリケーションでは、エンドポイントごとに異なるクライアントとして認証を行いたいという要件に遭遇することがあります。

この記事では、Spring Security を用いた、1つのWebアプリケーション内で複数のクライアントが同居する場合の OpenID Connect を用いた認証(以下、OIDC 認証)の設定方法を紹介します。

当記事の想定読者

  • Spring Security を導入したアプリケーションを開発される方
  • Spring Security を用いた OIDC 認証の基本的な設定方法を理解している方

環境

  • Spring Boot 3.5.3
  • Spring Security 6.5.1

求められる要件

OIDC 認証方式を利用する Webアプリケーション上で、エンドポイントごとに異なるクライアントとして認証を行う。

結論

以下の内容を適用することで実現が可能。

  • HttpSecurity::securityMatcher を使用して、エンドポイントごとに異なるセキュリティ設定を適用する。
  • HttpSessionSecurityContextRepository::setSpringSecurityContextKey を使用して、セッションに保存される認証情報のキーをエンドポイント毎に分離する。
  • AuthenticationEntryPoint を実装し、未認証アクセス時に OIDC 認証を開始する。

※本記事で利用するプロジェクト全体のサンプルコードはこちらです。記事内で表示するファイル名やファイル構成についてはこちらからご確認ください。

実装してみる

実際に

  • /foo に対して、クライアントAとして OIDC 認証する
  • /bar に対して、クライアントBとして OIDC 認証する

という実装が求められるアプリケーションを例に、具体的な実装を見ていきましょう。

以下のように「/foo」「/bar」に同一クライアントとして OIDC 認証を行っている SecurityConfig に対して、変更を加えていきます。

SecurityConfig.java

@Configuration
public class SecurityConfig {

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    // /fooと/barに対してOIDC認証を行うが、クライアントは全て同一になってしまう
    http.authorizeHttpRequests(a -> a
      .requestMatchers("/foo", "/bar").authenticated()
      .anyRequest().permitAll())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }

}

HttpSecurity::securityMatcher を使う

Spring Securityの公式リファレンスには、以下の通り記されています。

To effectively manage security in an application where certain areas need different protection, we can employ multiple filter chains alongside the securityMatcher DSL method. This approach allows us to define distinct security configurations tailored to specific parts of the application, enhancing overall application security and control.

要約すると、複数の SecurityFilterChain を作成し、かつ securityMatcher メソッドを利用することで、特定のエンドポイントに特定のセキュリティ設定を適用することが出来るようです。  

この機能を利用して /foo と /bar にそれぞれ異なる OIDC 認証の設定を適用していきます。

※今回はIDプロバイダーとして Keycloak を利用します。Keycloakの設定方法等については割愛します。

SecurityConfig.java

  @Bean
  @Order(1)
  SecurityFilterChain fooSecurityFilterChain(HttpSecurity http) throws Exception {

    // /foo、/login/oauth2/code/foo、/oauth2/authorization/fooに対する設定
    http.securityMatcher("/foo", "/login/oauth2/code/foo", "/oauth2/authorization/foo")
        .authorizeHttpRequests(
            a -> a.requestMatchers("/foo").authenticated().anyRequest().permitAll())
        .oauth2Login(Customizer.withDefaults());
    return http.build();
  }

  @Bean
  @Order(2)
  SecurityFilterChain barSecurityFilterChain(HttpSecurity http) throws Exception {

    // /bar、/login/oauth2/code/bar、/oauth2/authorization/barに対する設定
    http.securityMatcher("/bar", "/login/oauth2/code/bar", "/oauth2/authorization/bar")
        .authorizeHttpRequests(
            a -> a.requestMatchers("/bar").authenticated().anyRequest().permitAll())
        .oauth2Login(Customizer.withDefaults());
    return http.build();
  }


application.yml

spring:
  application:
    name: demo
  security:
    oauth2:
      client:
        registration:
          foo:
            client-id: foo-client-id
            client-secret: client-secret
            scope:
              - openid
              - profile
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
          bar:
            client-id: bar-client-id
            client-secret: client-secret
            scope:
              - openid
              - profile
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          foo:
            issuer-uri: http://localhost:9000/realms/sample
          bar:
            issuer-uri: http://localhost:9000/realms/sample


Spring Securityは、application.yml に記載した OIDC クライアントの登録ID(ここでは "foo" と "bar" )に基づき

  • 認可要求を開始するエンドポイント /oauth2/authorization/{登録ID}
  • 認可コードを受け取るエンドポイント /login/oauth2/code/{登録ID}

を自動的に作成します。  

これらのエンドポイントも各 securityMatcher に含めておきます。

Tips:
@Order アノテーションを使用することで、各 SecurityFilterChain の優先順を指定することが出来ます。

HttpSessionSecurityContextRepository::setSpringSecurityContextKey を使う

認証情報はセッションに保存されますが、このままでは /foo と /bar の認証情報が同じキーで保存されてしまい、後から認証した情報で上書きされてしまいます。

SecurityContext のセッションへの永続化を担当する HttpSessionSecurityContextRepository クラスの setSpringSecurityContextKey メソッドでセッションに保存する際のキーを指定できるため、このメソッドを利用して各エンドポイントの認証情報を個別にセッションに保持出来るように変更します。

SecurityConfig.java

  @Bean
  @Order(1)
  SecurityFilterChain fooSecurityFilterChain(HttpSecurity http) throws Exception {

    http.securityMatcher("/foo", "/login/oauth2/code/foo", "/oauth2/authorization/foo")
        .authorizeHttpRequests(
            a -> a.requestMatchers("/foo").authenticated().anyRequest().permitAll())
        .oauth2Login(Customizer.withDefaults())
        // SecurityContextの設定を追加する
        .securityContext(sec -> sec.securityContextRepository(this.fooSecurityContextRepository()));
    return http.build();
  }

  @Bean
  @Order(2)
  SecurityFilterChain barSecurityFilterChain(HttpSecurity http) throws Exception {

    http.securityMatcher("/bar", "/login/oauth2/code/bar", "/oauth2/authorization/bar")
        .authorizeHttpRequests(
            a -> a.requestMatchers("/bar").authenticated().anyRequest().permitAll())
        .oauth2Login(Customizer.withDefaults())
        // SecurityContextの設定を追加する
        .securityContext(sec -> sec.securityContextRepository(this.barSecurityContextRepository()));
    return http.build();
  }

  HttpSessionSecurityContextRepository fooSecurityContextRepository() {
    var securityContextRepository = new HttpSessionSecurityContextRepository();
    securityContextRepository.setSpringSecurityContextKey("SPRING_SECURITY_CONTEXT_FOO");
    return securityContextRepository;
  }

  HttpSessionSecurityContextRepository barSecurityContextRepository() {
    var securityContextRepository = new HttpSessionSecurityContextRepository();
    securityContextRepository.setSpringSecurityContextKey("SPRING_SECURITY_CONTEXT_BAR");
    return securityContextRepository;
  }


SecurityFilterChain ごとに異なるキーを使用する HttpSessionSecurityContextRepository を設定します。

securityMatcher に記載していないエンドポイントでは、ここで設定した SecurityContextRepository から取得出来る SecurityContextSecurityContextHolder を経由してアクセスすることが出来ないため、SecurityContext へアクセスする可能性のあるエンドポイントは全て securityMatcher に記載する必要があります。

AuthenticationEntryPoint を実装する

AuthenticationEntryPoint を実装し、認証が必要なエンドポイントへアクセスした際に自動的に認可要求を送信します。

Spring Security が認可要求を開始するエンドポイントを自動で実装してくれるため、AuthenticationEntryPoint ではそのエンドポイントへリダイレクトする処理のみを行います。

FooAuthenticationEntryPoint.java

public class FooAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException, ServletException {
    response.sendRedirect("/oauth2/authorization/foo");
  }

}


次に、各エンドポイントごとに利用する AutheticationEntryPoint を設定します。

SecurityConfig.java

  @Bean
  @Order(1)
  SecurityFilterChain fooSecurityFilterChain(HttpSecurity http) throws Exception {

    http.securityMatcher("/foo", "/login/oauth2/code/foo", "/oauth2/authorization/foo")
        .authorizeHttpRequests(
            a -> a.requestMatchers("/foo").authenticated().anyRequest().permitAll())
        .oauth2Login(Customizer.withDefaults())
        .securityContext(sec -> sec.securityContextRepository(this.fooSecurityContextRepository()))
        // 作成したAuthenticationEntryPointを登録する
        .exceptionHandling(ex -> ex.authenticationEntryPoint(new FooAuthenticationEntryPoint()));
    return http.build();
  }

  @Bean
  @Order(2)
  SecurityFilterChain barSecurityFilterChain(HttpSecurity http) throws Exception {

    http.securityMatcher("/bar", "/login/oauth2/code/bar", "/oauth2/authorization/bar")
        .authorizeHttpRequests(
            a -> a.requestMatchers("/bar").authenticated().anyRequest().permitAll())
        .oauth2Login(Customizer.withDefaults())
        .securityContext(sec -> sec.securityContextRepository(this.barSecurityContextRepository()))
        // 作成したAuthenticationEntryPointを登録する
        .exceptionHandling(ex -> ex.authenticationEntryPoint(new BarAuthenticationEntryPoint()));
    return http.build();
  }

  HttpSessionSecurityContextRepository fooSecurityContextRepository() {
    var securityContextRepository = new HttpSessionSecurityContextRepository();
    securityContextRepository.setSpringSecurityContextKey("SPRING_SECURITY_CONTEXT_FOO");
    return securityContextRepository;
  }

  HttpSessionSecurityContextRepository barSecurityContextRepository() {
    var securityContextRepository = new HttpSessionSecurityContextRepository();
    securityContextRepository.setSpringSecurityContextKey("SPRING_SECURITY_CONTEXT_BAR");
    return securityContextRepository;
  }

完成

以上で設定は完了です。

最後に、発行されたIDトークンのクレームを確認できるページを用意し、各エンドポイントで正しく認証が行われているかを確認します。

/foo エンドポイント

ID及びパスワードによる認証の後、IDトークンが持つクレームを確認できます。Keycloak のログイン画面からログインして確認してみましょう。

Keycloak のログイン画面

確認ページ

/bar エンドポイント

Keycloak 側のセッションが残っているためログイン画面が省略されていますが、/foo エンドポイントにアクセスした時とは at_hash の値が異なることから、異なるIDトークンが発行されていることが分かります。

確認ページ

おわりに

本記事では、各エンドポイントごとに異なるクライアントの OIDC 認証を設定する方法について紹介しました。

Spring Security の公式リファレンスには、他にも認証処理の実装にあたって役立つ情報が記載されていますので、実装に際しては是非ともご一読ください。

参考資料

Spring Security OAuth 2.0 ログイン

https://spring.pleiades.io/spring-security/reference/reactive/oauth2/login

Spring Security Servlet Authentication Architecture

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

Spring Security Java Configuration

https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-httpsecurity

株式会社GSI 採用サイト

新しいこと、始めよう

あなたとともに歩を進めるWEBメディア