マイクロサービスアーキテクチャの普及に伴い、サービス間の認証・認可を一元管理する認可サーバーの重要性が増しています。本記事では、Spring Authorization Serverを使用して、自組織向けの認可サーバーを構築する方法を解説します。外部サービス(Auth0やKeycloakなど)に依存せず、完全に自前で認可基盤を構築できるようになります。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Authorization Server 1.4.x
Spring Security 6.4.x
ビルドツール Maven または Gradle
IDE VS Code または IntelliJ IDEA

事前に以下の知識があると理解がスムーズです。

  • Spring Securityの基本概念(認証・認可の違い)
  • OAuth2の基礎知識(認可コードフロー、クライアントクレデンシャルフロー)
  • JWTの構造と検証の仕組み

Spring Authorization Serverとは

Spring Authorization Serverは、OAuth 2.1およびOpenID Connect 1.0の仕様に準拠した認可サーバーを構築するためのSpring公式プロジェクトです。2020年にSpring Securityプロジェクトから分離され、独立したプロジェクトとして開発が進められています。

主な機能

Spring Authorization Serverが提供する主要な機能は以下のとおりです。

機能 説明
OAuth2認可エンドポイント 認可コード発行、PKCE対応
トークンエンドポイント アクセストークン、リフレッシュトークンの発行
JWK Setエンドポイント 公開鍵の配布
トークンイントロスペクション トークンの有効性検証
トークン失効 トークンの明示的な無効化
OpenID Connect 1.0 ID Token発行、UserInfoエンドポイント
Dynamic Client Registration クライアントの動的登録

アーキテクチャ概要

Spring Authorization Serverを中心としたマイクロサービスアーキテクチャの全体像は以下のとおりです。

flowchart TB
    subgraph "OAuth2 エコシステム"
        U[ユーザー] --> C[クライアントアプリ<br/>SPA/モバイル/BFF]
        C --> AS[Spring Authorization Server<br/>認可サーバー]
        AS --> C
        C --> RS1[リソースサーバーA<br/>ユーザーAPI]
        C --> RS2[リソースサーバーB<br/>注文API]
        RS1 --> AS
        RS2 --> AS
    end
    
    AS -->|"JWK Set公開"| RS1
    AS -->|"JWK Set公開"| RS2

プロジェクトのセットアップ

依存関係の追加

Spring Authorization Serverを使用するには、以下の依存関係を追加します。

Mavenの場合(pom.xml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    </dependency>
</dependencies>

Gradleの場合(build.gradle):

1
2
3
4
5
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
}

spring-boot-starter-oauth2-authorization-serverは、Spring Authorization Serverの全機能を含んでいます。

プロジェクト構成

本記事で作成するプロジェクトの構成は以下のとおりです。

src/main/java/com/example/authserver/
├── AuthServerApplication.java
├── config/
│   ├── AuthorizationServerConfig.java
│   └── DefaultSecurityConfig.java
└── entity/
    └── User.java

最小構成での認可サーバー

application.ymlによる設定

Spring Boot 3.4以降では、application.ymlのみで基本的な認可サーバーを構成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server:
  port: 9000

logging:
  level:
    org.springframework.security: DEBUG

spring:
  security:
    user:
      name: user
      password: password
    oauth2:
      authorizationserver:
        client:
          web-client:
            registration:
              client-id: "web-client"
              client-secret: "{noop}web-secret"
              client-authentication-methods:
                - "client_secret_basic"
              authorization-grant-types:
                - "authorization_code"
                - "refresh_token"
              redirect-uris:
                - "http://127.0.0.1:8080/login/oauth2/code/web-client"
              post-logout-redirect-uris:
                - "http://127.0.0.1:8080/"
              scopes:
                - "openid"
                - "profile"
                - "email"
            require-authorization-consent: true

この設定により、以下のエンドポイントが自動的に構成されます。

エンドポイント パス 説明
認可エンドポイント /oauth2/authorize 認可コード発行
トークンエンドポイント /oauth2/token トークン発行
JWK Setエンドポイント /oauth2/jwks 公開鍵配布
トークン失効 /oauth2/revoke トークン無効化
トークンイントロスペクション /oauth2/introspect トークン検証
OpenID構成 /.well-known/openid-configuration ディスカバリー

カスタム設定による認可サーバー構築

本番環境向けには、Javaコードで明示的に設定を行う方法が推奨されます。以下では、詳細なカスタマイズが可能な構成を解説します。

SecurityFilterChainの設定

認可サーバー用とデフォルト認証用の2つのSecurityFilterChainを定義します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.example.authserver.config;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        
        // 認可サーバーのデフォルト設定を適用
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
                OAuth2AuthorizationServerConfigurer.authorizationServer();

        http
            .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
            .with(authorizationServerConfigurer, authorizationServer ->
                authorizationServer
                    // OpenID Connect 1.0を有効化
                    .oidc(Customizer.withDefaults())
            )
            .authorizeHttpRequests(authorize ->
                authorize.anyRequest().authenticated()
            )
            // 未認証時はログインページへリダイレクト
            .exceptionHandling(exceptions -> exceptions
                .defaultAuthenticationEntryPointFor(
                    new LoginUrlAuthenticationEntryPoint("/login"),
                    new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                )
            );

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            // フォームログインを有効化
            .formLogin(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .issuer("http://localhost:9000")
                .build();
    }
}

各コンポーネントの役割は以下のとおりです。

コンポーネント 役割
authorizationServerSecurityFilterChain OAuth2/OIDCプロトコルエンドポイント用のフィルターチェーン
defaultSecurityFilterChain ログインページなど通常の認証用フィルターチェーン
jwkSource JWTの署名・検証に使用するRSA鍵ペアを提供
jwtDecoder 自己発行したJWTの検証用デコーダー
authorizationServerSettings 発行者(issuer)URLなどの設定

UserDetailsServiceの設定

認証に使用するユーザー情報を提供するUserDetailsServiceを設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.authserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class DefaultSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        
        UserDetails admin = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("admin")
                .roles("USER", "ADMIN")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

本番環境では、InMemoryUserDetailsManagerではなく、データベースと連携したUserDetailsServiceを実装してください。

クライアント登録と複数クライアント対応

RegisteredClientRepositoryの設定

OAuth2クライアントの登録と管理を行うRegisteredClientRepositoryを設定します。以下では、異なるユースケースに対応した複数のクライアントを登録する例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.example.authserver.config;

import java.util.UUID;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;

import java.time.Duration;

@Configuration
public class ClientConfig {

    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        
        // Webアプリケーション用クライアント(認可コードフロー)
        RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("web-client")
                .clientSecret(passwordEncoder.encode("web-secret"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/web-client")
                .postLogoutRedirectUri("http://127.0.0.1:8080/")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope(OidcScopes.EMAIL)
                .scope("read")
                .scope("write")
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .requireProofKey(false)
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofMinutes(30))
                        .refreshTokenTimeToLive(Duration.ofDays(7))
                        .reuseRefreshTokens(false)
                        .build())
                .build();

        // SPAアプリケーション用クライアント(PKCE必須)
        RegisteredClient spaClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("spa-client")
                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:3000/callback")
                .postLogoutRedirectUri("http://127.0.0.1:3000/")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope("read")
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .requireProofKey(true)  // PKCE必須
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofMinutes(15))
                        .refreshTokenTimeToLive(Duration.ofHours(8))
                        .reuseRefreshTokens(false)
                        .build())
                .build();

        // マイクロサービス間通信用クライアント(クライアントクレデンシャルフロー)
        RegisteredClient serviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("service-client")
                .clientSecret(passwordEncoder.encode("service-secret"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .scope("internal:read")
                .scope("internal:write")
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofMinutes(60))
                        .build())
                .build();

        return new InMemoryRegisteredClientRepository(webClient, spaClient, serviceClient);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return org.springframework.security.crypto.factory.PasswordEncoderFactories
                .createDelegatingPasswordEncoder();
    }
}

クライアント種別と設定の対応

各クライアント種別に応じた設定のポイントを以下にまとめます。

クライアント種別 認証方式 グラントタイプ PKCE 主なユースケース
Webアプリ(BFF) CLIENT_SECRET_BASIC authorization_code 任意 サーバーサイドレンダリング
SPA NONE authorization_code 必須 フロントエンドアプリ
マイクロサービス CLIENT_SECRET_BASIC client_credentials 不要 サービス間通信
モバイルアプリ NONE authorization_code 必須 ネイティブアプリ

JWKSetエンドポイントの公開

JWKSet(JSON Web Key Set)エンドポイントは、リソースサーバーがJWTの署名を検証するための公開鍵を提供します。Spring Authorization Serverでは、JWKSource Beanを登録すると自動的に/oauth2/jwksエンドポイントが有効になります。

本番環境向けの鍵管理

本番環境では、起動ごとに鍵を生成するのではなく、永続化された鍵を使用する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Bean
public JWKSource<SecurityContext> jwkSource() throws Exception {
    // PEM形式の秘密鍵を読み込む場合
    String privateKeyPem = Files.readString(Path.of("/etc/secrets/private-key.pem"));
    String publicKeyPem = Files.readString(Path.of("/etc/secrets/public-key.pem"));
    
    RSAPrivateKey privateKey = parsePrivateKey(privateKeyPem);
    RSAPublicKey publicKey = parsePublicKey(publicKeyPem);
    
    RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID("prod-key-2026")
            .build();
    
    JWKSet jwkSet = new JWKSet(rsaKey);
    return new ImmutableJWKSet<>(jwkSet);
}

JWKSetレスポンスの例

/oauth2/jwksエンドポイントにアクセスすると、以下のようなJWK Setが返却されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "kid": "prod-key-2026",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4..."
    }
  ]
}

リソースサーバーは、この公開鍵を使用してJWTの署名を検証します。

トークンのカスタマイズ

アクセストークンへのカスタムクレーム追加

アクセストークンにユーザー固有の情報を追加する場合は、OAuth2TokenCustomizerを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.authserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;

import java.util.stream.Collectors;

@Configuration
public class TokenCustomizerConfig {

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                Authentication principal = context.getPrincipal();
                
                // ロール情報をクレームに追加
                context.getClaims().claim("roles", 
                    principal.getAuthorities().stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toSet()));
                
                // カスタムクレームの追加
                context.getClaims().claim("tenant_id", "default");
                context.getClaims().claim("user_type", "internal");
            }
        };
    }
}

カスタマイズ後のアクセストークンのペイロード例は以下のとおりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "sub": "user",
  "aud": "web-client",
  "nbf": 1736726400,
  "scope": "openid profile email read",
  "roles": ["ROLE_USER"],
  "tenant_id": "default",
  "user_type": "internal",
  "iss": "http://localhost:9000",
  "exp": 1736728200,
  "iat": 1736726400
}

認可フローの動作確認

認可コードフローのテスト

認可コードフローを手動でテストする手順を示します。

1. 認可リクエストの送信:

ブラウザで以下のURLにアクセスします。

http://localhost:9000/oauth2/authorize?
  response_type=code&
  client_id=web-client&
  scope=openid%20profile%20email%20read&
  redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-client&
  state=abc123

2. ユーザー認証:

ログインページが表示されるので、設定したユーザー(user/password)でログインします。

3. 同意画面:

requireAuthorizationConsenttrueに設定している場合、スコープの同意画面が表示されます。

4. 認可コードの取得:

同意後、リダイレクトURIに認可コードが付与されて戻ります。

http://127.0.0.1:8080/login/oauth2/code/web-client?
  code=AUTHORIZATION_CODE&
  state=abc123

5. トークン取得リクエスト:

認可コードを使用してトークンを取得します。

1
2
3
4
5
6
curl -X POST http://localhost:9000/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "web-client:web-secret" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=http://127.0.0.1:8080/login/oauth2/code/web-client"

クライアントクレデンシャルフローのテスト

マイクロサービス間通信用のクライアントクレデンシャルフローをテストします。

1
2
3
4
5
curl -X POST http://localhost:9000/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "service-client:service-secret" \
  -d "grant_type=client_credentials" \
  -d "scope=internal:read internal:write"

成功すると以下のようなレスポンスが返却されます。

1
2
3
4
5
6
{
  "access_token": "eyJraWQiOiJwcm9kLWtleS0yMDI2IiwiYWxnIjoiUlMyNTYifQ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "internal:read internal:write"
}

データベース永続化の設定

本番環境では、クライアント情報や認可情報をデータベースに永続化する必要があります。

JDBC実装の設定

Spring Authorization Serverは、JDBC実装を標準で提供しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.authserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;

@Configuration
public class JdbcConfig {

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    @Bean
    public OAuth2AuthorizationService authorizationService(
            JdbcTemplate jdbcTemplate, 
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(
            JdbcTemplate jdbcTemplate, 
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }
}

スキーマの作成

Spring Authorization Serverが必要とするテーブルのスキーマは、公式リポジトリで提供されています。以下はPostgreSQL用のスキーマ例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
CREATE TABLE oauth2_registered_client (
    id varchar(100) NOT NULL,
    client_id varchar(100) NOT NULL,
    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret varchar(200) DEFAULT NULL,
    client_secret_expires_at timestamp DEFAULT NULL,
    client_name varchar(200) NOT NULL,
    client_authentication_methods varchar(1000) NOT NULL,
    authorization_grant_types varchar(1000) NOT NULL,
    redirect_uris varchar(1000) DEFAULT NULL,
    post_logout_redirect_uris varchar(1000) DEFAULT NULL,
    scopes varchar(1000) NOT NULL,
    client_settings varchar(2000) NOT NULL,
    token_settings varchar(2000) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE oauth2_authorization (
    id varchar(100) NOT NULL,
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorization_grant_type varchar(100) NOT NULL,
    authorized_scopes varchar(1000) DEFAULT NULL,
    attributes text DEFAULT NULL,
    state varchar(500) DEFAULT NULL,
    authorization_code_value text DEFAULT NULL,
    authorization_code_issued_at timestamp DEFAULT NULL,
    authorization_code_expires_at timestamp DEFAULT NULL,
    authorization_code_metadata text DEFAULT NULL,
    access_token_value text DEFAULT NULL,
    access_token_issued_at timestamp DEFAULT NULL,
    access_token_expires_at timestamp DEFAULT NULL,
    access_token_metadata text DEFAULT NULL,
    access_token_type varchar(100) DEFAULT NULL,
    access_token_scopes varchar(1000) DEFAULT NULL,
    oidc_id_token_value text DEFAULT NULL,
    oidc_id_token_issued_at timestamp DEFAULT NULL,
    oidc_id_token_expires_at timestamp DEFAULT NULL,
    oidc_id_token_metadata text DEFAULT NULL,
    refresh_token_value text DEFAULT NULL,
    refresh_token_issued_at timestamp DEFAULT NULL,
    refresh_token_expires_at timestamp DEFAULT NULL,
    refresh_token_metadata text DEFAULT NULL,
    user_code_value text DEFAULT NULL,
    user_code_issued_at timestamp DEFAULT NULL,
    user_code_expires_at timestamp DEFAULT NULL,
    user_code_metadata text DEFAULT NULL,
    device_code_value text DEFAULT NULL,
    device_code_issued_at timestamp DEFAULT NULL,
    device_code_expires_at timestamp DEFAULT NULL,
    device_code_metadata text DEFAULT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE oauth2_authorization_consent (
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorities varchar(1000) NOT NULL,
    PRIMARY KEY (registered_client_id, principal_name)
);

リソースサーバーとの連携

作成した認可サーバーと連携するリソースサーバーの設定例を示します。

リソースサーバーの設定

リソースサーバー側のapplication.ymlは以下のように設定します。

1
2
3
4
5
6
7
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9000
          jwk-set-uri: http://localhost:9000/oauth2/jwks

リソースサーバーは、jwk-set-uriから公開鍵を取得してJWTを検証します。issuer-uriを指定することで、起動時に自動的に.well-known/openid-configurationを取得し、各種エンドポイントを解決します。

構成図

認可サーバーとリソースサーバーの連携を示す図は以下のとおりです。

sequenceDiagram
    participant Client as クライアント
    participant AS as 認可サーバー<br/>(localhost:9000)
    participant RS as リソースサーバー<br/>(localhost:8080)

    Client->>AS: 1. 認可リクエスト
    AS->>Client: 2. 認可コード
    Client->>AS: 3. トークンリクエスト
    AS->>Client: 4. アクセストークン (JWT)
    Client->>RS: 5. APIリクエスト + Bearer Token
    RS->>AS: 6. JWK Set取得 (初回のみ)
    AS->>RS: 7. 公開鍵
    RS->>RS: 8. JWT署名検証
    RS->>Client: 9. リソース返却

本番環境に向けた考慮事項

Spring Authorization Serverを本番環境で運用する際には、以下の点を考慮してください。

項目 推奨事項
鍵の管理 HSM(Hardware Security Module)やAWS KMS、HashiCorp Vaultなどを使用
HTTPS 本番環境では必ずHTTPSを使用
クライアントシークレット 環境変数やSecrets Managerで管理
セッション管理 Redisなどの分散キャッシュでセッション共有
ログ監査 認可イベントのログ記録と監視
レート制限 トークンエンドポイントへのアクセス制限
高可用性 複数インスタンスでのクラスタ構成

まとめ

本記事では、Spring Authorization Serverを使用して自組織向けの認可サーバーを構築する方法を解説しました。主なポイントを以下にまとめます。

  • Spring Authorization Serverは、OAuth 2.1およびOpenID Connect 1.0に準拠した認可サーバーを構築するための公式プロジェクトです
  • 最小構成ではapplication.ymlのみで動作しますが、本番環境ではJavaコードによる明示的な設定が推奨されます
  • 複数のクライアント(Webアプリ、SPA、マイクロサービス)に対応した設定が可能です
  • JWK Setエンドポイントにより、リソースサーバーへの公開鍵配布が自動化されます
  • データベース永続化により、本番環境での運用に対応できます

Spring Authorization Serverを活用することで、KeycloakやAuth0などの外部サービスに依存せず、完全に自前の認可基盤を構築できます。マイクロサービスアーキテクチャにおける認証・認可の一元管理に、ぜひ活用してください。

参考リンク