1. 概要

このチュートリアルでは、SpringセキュリティOAuth2実装を取得してJSONWebトークンを利用する方法について説明します。

また、このOAuthシリーズの Spring REST API + OAuth2 +Angularの記事に基づいて構築を続けています。

2. OAuth2認証サーバー

以前は、Spring Security OAuthスタックは、承認サーバーをSpringアプリケーションとしてセットアップする可能性を提供していました。 次に、 JwtTokenStore を使用するように構成して、JWTトークンを使用できるようにする必要がありました。

ただし、OAuthスタックはSpringによって非推奨になり、認証サーバーとしてKeycloakを使用するようになります。

今回は、Spring Bootアプリに組み込みKeycloakサーバーとして認証サーバーを設定します。 デフォルトでJWTトークンを発行するため、この点に関して他の構成は必要ありません。

3. リソースサーバー

次に、JWTを使用するようにリソースサーバーを構成する方法を見てみましょう。

これはapplication.ymlファイルで行います。

server: 
  port: 8081
  servlet: 
    context-path: /resource-server

spring:
  jpa:
    defer-datasource-initialization: true
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung
          jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

JWTにはすべての情報が含まれています トークン内であるため、リソースサーバーは次のことを行う必要があります トークンの署名を確認する データが変更されていないことを確認します。 jwk-set-uriプロパティ公開鍵が含まれています サーバーがこの目的で使用できる

issuer-uri プロパティは、基本のAuthorization Server URIを指します。これは、追加のセキュリティ対策としてissクレームを検証するためにも使用できます。

さらに、 jwk-set-uri プロパティが設定されていない場合、リソースサーバーは issuer-uri を使用して、Authorizationからこのキーの場所を特定しようとします。サーバーメタデータエンドポイント

issuer-uri プロパティを追加すると、リソースサーバーアプリケーションを起動する前に承認サーバーを実行する必要があることに注意してください。

ここで、h を見てみましょう。Java構成を使用してJWTサポートを構成できます:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
              .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
                  .hasAuthority("SCOPE_read")
                .antMatchers(HttpMethod.POST, "/api/foos")
                  .hasAuthority("SCOPE_write")
                .anyRequest()
                  .authenticated()
            .and()
              .oauth2ResourceServer()
                .jwt();
    }
}

ここでは、デフォルトのHttpセキュリティ構成をオーバーライドしています。 これをリソースサーバーとして動作させ、それぞれメソッドoauth2ResourceServer()およびjwt()を使用してJWT形式のアクセストークンを使用することを明示的に指定する必要があります。

上記のJWT構成は、デフォルトのSpringBootインスタンスが提供するものです。 これは、後で説明するようにカスタマイズすることもできます。

4. トークンのカスタムクレーム

次に、承認サーバーによって返されるアクセストークンにいくつかのカスタムクレームを追加できるように、いくつかのインフラストラクチャをセットアップしましょう。 フレームワークによって提供される標準的な主張はすべてうまくいっていますが、ほとんどの場合、クライアント側で利用するためにトークンにいくつかの追加情報が必要になります。

特定のユーザーの組織の名前を含むカスタムクレームorganizationの例を見てみましょう。

4.1. 承認サーバーの構成

このために、レルム定義ファイルbaeldung-realm.jsonにいくつかの構成を追加する必要があります。

  • 属性organizationをユーザー[email protected]に追加します。
    "attributes" : {
      "organization" : "baeldung"
    },
  • 組織というprotocolMapperjwtClient構成に追加します。
    "protocolMappers": [{
      "id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1",
      "name": "organization",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-usermodel-attribute-mapper",
      "consentRequired": false,
      "config": {
        "userinfo.token.claim": "true",
        "user.attribute": "organization",
        "id.token.claim": "true",
        "access.token.claim": "true",
        "claim.name": "organization",
        "jsonType.label": "String"
      }
    }],

スタンドアロンのKeycloakセットアップの場合、これは管理コンソールを使用して行うこともできます。 

上記のJSON構成はKeycloakに固有であり、他のOAuthサーバーでは異なる可能性があることを覚えておくことが重要です。

この新しい構成が稼働すると、[email protected]のトークンペイロードに追加の属性organization=baeldungが取得されます。

{
  jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e"
  exp: 1585242462
  nbf: 0
  iat: 1585242162
  iss: "http://localhost:8083/auth/realms/baeldung"
  sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f"
  typ: "Bearer"
  azp: "jwtClient"
  auth_time: 1585242162
  session_state: "384ca5cc-8342-429a-879c-c15329820006"
  acr: "1"
  scope: "profile write read"
  organization: "baeldung"
  preferred_username: "[email protected]"
}

4.2. Angularクライアントでアクセストークンを使用する

次に、Angularクライアントアプリケーションでトークン情報を利用します。 そのためにangular2-jwtライブラリを使用します。

AppServiceorganizationクレームを利用し、関数getOrganizationを追加します。

getOrganization(){
  var token = Cookie.get("access_token");
  var payload = this.jwtHelper.decodeToken(token);
  this.organization = payload.organization; 
  return this.organization;
}

この関数は、angle2-jwtライブラリのJwtHelperServiceを利用して、アクセストークンをデコードし、カスタムクレームを取得します。 これで、AppComponentに表示するだけで済みます。

@Component({
  selector: 'app-root',
  template: `<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a>
    </div>
  </div>
  <div class="navbar-brand">
    <p>{{organization}}</p>
  </div>
</nav>
<router-outlet></router-outlet>`
})

export class AppComponent implements OnInit {
  public organization = "";
  constructor(private service: AppService) { }  
   
  ngOnInit() {  
    this.organization = this.service.getOrganization();
  }  
}

5. リソースサーバーで追加のクレームにアクセスする

しかし、リソースサーバー側でその情報にアクセスするにはどうすればよいでしょうか。

5.1. 認証サーバーの主張にアクセスする

これは本当に簡単です。UserInfoControllerの他の属性の場合と同じように、org.springframework.security.oauth2.jwt.Jwtの AuthenticationPrincipal、から抽出する必要があります。

@GetMapping("/user/info")
public Map<String, Object> getUserInfo(@AuthenticationPrincipal Jwt principal) {
    Map<String, String> map = new Hashtable<String, String>();
    map.put("user_name", principal.getClaimAsString("preferred_username"));
    map.put("organization", principal.getClaimAsString("organization"));
    return Collections.unmodifiableMap(map);
}

5.2. クレームを追加/削除/名前変更するための構成

リソースサーバー側でクレームを追加したい場合はどうすればよいでしょうか。 または、一部を削除または名前変更しますか?

認証サーバーからのorganizationクレームを変更して、大文字の値を取得するとします。 ただし、クレームがユーザーに存在しない場合は、その値をunknownとして設定する必要があります。

これを実現するには、Converterインターフェイスを実装し、MappedJwtClaimSetConverterを使用してクレームを変換するクラスを追加する必要があります

public class OrganizationSubClaimAdapter implements 
  Converter<Map<String, Object>, Map<String, Object>> {
    
    private final MappedJwtClaimSetConverter delegate = 
      MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);
        String organization = convertedClaims.get("organization") != null ? 
          (String) convertedClaims.get("organization") : "unknown";
        
        convertedClaims.put("organization", organization.toUpperCase());

        return convertedClaims;
    }
}

次に、 SecurityConfig クラスで、独自のJwtDecoderインスタンスを追加して、Spring Boot によって提供されるインスタンスをオーバーライドし、OrganizationSubClaimAdapterをクレームコンバーターとして設定する必要があります。

@Bean
public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
      properties.getJwt().getJwkSetUri()).build();
    
    jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter());
    return jwtDecoder;
}

これで、ユーザー[email protected]/user/infoAPIにアクセスすると、組織UNKNOWNとして取得されます。 ]。

SpringBootによって構成されたデフォルトのJwtDecoder Beanのオーバーライドは、必要なすべての構成が引き続き含まれていることを確認するために慎重に行う必要があることに注意してください。

6. Javaキーストアからのキーのロード

以前の構成では、承認サーバーのデフォルトの公開鍵を使用して、トークンの整合性を検証しました。

また、Javaキーストアファイルに保存されているキーペアと証明書を使用して、署名プロセスを実行することもできます。

6.1. JKSJavaKeyStoreファイルを生成します

まず、コマンドラインツール keytool:を使用して、キー、より具体的には.jksファイルを生成しましょう。

keytool -genkeypair -alias mytest 
                    -keyalg RSA 
                    -keypass mypass 
                    -keystore mytest.jks 
                    -storepass mypass

このコマンドは、 mytest.jks というファイルを生成します。このファイルには、公開キーと秘密キーが含まれています。

また、keypassstorepassが同じであることを確認してください。

6.2. 公開鍵のエクスポート

次に、生成されたJKSから公開鍵をエクスポートする必要があります。 次のコマンドを使用してこれを行うことができます。

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

サンプル応答は次のようになります。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----

6.3. Maven構成

JKSファイルがMavenフィルタリングプロセスによって取得されないようにするため、pom.xmlで必ず除外します。

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <excludes>
                <exclude>*.jks</exclude>
            </excludes>
        </resource>
    </resources>
</build>

Spring Bootを使用している場合は、JKSファイルがSpring BootMavenプラグインaddResourcesを介してアプリケーションクラスパスに追加されていることを確認する必要があります。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <addResources>true</addResources>
            </configuration>
        </plugin>
    </plugins>
</build>

6.4. 承認サーバー

次に、 mytest.jksのキーペアをレルム定義JSONファイルのKeyProvider セクションに追加して、Keycloakを使用するように構成します。

{
  "id": "59412b8d-aad8-4ab8-84ec-e546900fc124",
  "name": "java-keystore",
  "providerId": "java-keystore",
  "subComponents": {},
  "config": {
    "keystorePassword": [ "mypass" ],
    "keyAlias": [ "mytest" ],
    "keyPassword": [ "mypass" ],
    "active": [ "true" ],
    "keystore": [
            "src/main/resources/mytest.jks"
          ],
    "priority": [ "101" ],
    "enabled": [ "true" ],
    "algorithm": [ "RS256" ]
  }
},

ここでは、priority101に設定しました。これは、承認サーバーの他のどのキーペアよりも大きく、activetrueに設定します。 これは、リソースサーバーが前に指定したjwk-set-uriプロパティからこの特定のキーペアを確実に選択するために行われます。

この場合も、この構成はKeycloakに固有であり、他のOAuthサーバーの実装では異なる場合があります。

7. 結論

この短い記事では、JSONWebトークンを使用するようにSpringSecurityOAuth2プロジェクトを設定することに焦点を当てました。

この記事の完全な実装は、GitHubにあります。