1. 概要
このチュートリアルでは、セキュリティ5を使用してOAuth2.0リソースサーバーをセットアップする方法を学習します。
これは、JWTと、SpringSecurityでサポートされている2種類のベアラートークンである不透明なトークンを使用して行います。
実装とコードサンプルにジャンプする前に、いくつかの背景を確立します。
2. 少し背景
2.1. JWTと不透明トークンとは何ですか?
JWT、または JSON Web Token は、広く受け入れられているJSON形式で機密情報を安全に転送する方法です。 含まれる情報は、ユーザーに関するもの、またはトークン自体に関するもの(有効期限や発行者など)である可能性があります。
一方、不透明なトークンは、その名前が示すように、それが運ぶ情報に関しては不透明です。 トークンは、認証サーバーに保存されている情報を指す単なる識別子であり、サーバー側のイントロスペクションによって検証されます。
2.2. リソースサーバーとは何ですか?
OAuth 2.0のコンテキストでは、リソースサーバーは、OAuthトークンを介してリソースを保護するアプリケーションです。 これらのトークンは、承認サーバーによって、通常はクライアントアプリケーションに発行されます。 リソースサーバーの役割は、クライアントにリソースを提供する前にトークンを検証することです。
トークンの有効性は、いくつかのことによって決定されます。
- このトークンは、構成された認証サーバーからのものですか?
- 期限が切れていませんか?
- このリソースサーバーは対象読者ですか?
- トークンには、要求されたリソースにアクセスするために必要な権限がありますか?
視覚化するために、認証コードフローのシーケンス図を見て、動作中のすべてのアクターを見てみましょう。
手順8でわかるように、クライアントアプリケーションがリソースサーバーのAPIを呼び出して保護されたリソースにアクセスすると、最初に承認サーバーにアクセスして、リクエストの Authorization:Bearerヘッダーに含まれるトークンを検証します。次に、クライアントに応答します。
ステップ9は、このチュートリアルで焦点を当てているものです。
では、コード部分に飛び込みましょう。 Keycloak、JWTトークンを検証するリソースサーバー、不透明なトークンを検証する別のリソースサーバー、およびクライアントアプリをシミュレートして応答を検証するためのいくつかのJUnitテストを使用して、承認サーバーをセットアップします。
3. 承認サーバー
まず、認証サーバー、つまりトークンを発行するサーバーをセットアップします。
そのために、Spring Bootアプリケーションに埋め込まれたKeycloakを使用します。 Keycloakは、オープンソースのIDおよびアクセス管理ソリューションです。 このチュートリアルではリソースサーバーに焦点を当てているため、これ以上詳しくは説明しません。
組み込みのKeycloakサーバーには、2つのリソースサーバーアプリケーションに対応するfooClientとbarClient–の2つのクライアントが定義されています。
4. リソースサーバー–JWTの使用
リソースサーバーには、次の4つの主要コンポーネントがあります。
- モデル–保護するリソース
- API –リソースを公開するためのRESTコントローラー
- セキュリティ構成–APIが公開する保護されたリソースのアクセス制御を定義するクラス
- application.yml –認証サーバーに関する情報を含むプロパティを宣言するための構成ファイル
依存関係を確認した後、JWTトークンを処理するリソースサーバーについて、それらを1つずつ見ていきましょう。
4.1. Mavenの依存関係
主に、リソースサーバーをサポートするための spring -boot-starter-oauth2-resource-server 、Spring Bootのスターターが必要です。 このスターターにはデフォルトでSpringセキュリティが含まれているため、明示的に追加する必要はありません。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
それとは別に、Webサポートも追加しました。
デモンストレーションの目的で、Apacheの commons-lang3 ライブラリの助けを借りて、データベースからリソースを取得するのではなく、ランダムにリソースを生成します。
4.2. モデル
簡単にするために、保護されたリソースとして Foo 、POJOを使用します。
public class Foo {
private long id;
private String name;
// constructor, getters and setters
}
4.3. API
Fooを操作できるようにするためのRESTコントローラーは次のとおりです。
@RestController
@RequestMapping(value = "/foos")
public class FooController {
@GetMapping(value = "/{id}")
public Foo findOne(@PathVariable Long id) {
return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
}
@GetMapping
public List findAll() {
List fooList = new ArrayList();
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
return fooList;
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public void create(@RequestBody Foo newFoo) {
logger.info("Foo created");
}
}
明らかなように、すべての Foo を取得し、IDで Foo を取得し、FooをPOSTするためのプロビジョニングがあります。
4.4. セキュリティ構成
この構成クラスでは、リソースのアクセスレベルを定義します。
@Configuration
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authz -> authz
.antMatchers(HttpMethod.GET, "/foos/**").hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/foos").hasAuthority("SCOPE_write")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
}
}
read スコープを持つアクセストークンを持っている人は誰でも、Fooを取得できます。 新しいFooをPOSTするには、トークンにwriteスコープが必要です。
さらに、 oauth2ResourceServer()DSLを使用してjwt()への呼び出しを追加し、ここでサーバーがサポートするトークンのタイプを示します。
4.5. application.yml
アプリケーションのプロパティでは、通常のポート番号とコンテキストパスに加えて、リソースサーバーがプロバイダー構成を検出できるように、承認サーバーの発行者URIへのパスを定義する必要があります。
server:
port: 8081
servlet:
context-path: /resource-server-jwt
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8083/auth/realms/baeldung
シーケンス図のステップ9に従って、リソースサーバーはこの情報を使用して、クライアントアプリケーションから着信するJWTトークンを検証します。
issuer-uri プロパティを使用してこの検証を機能させるには、認証サーバーが稼働している必要があります。 そうしないと、リソースサーバーが起動しません。
個別に起動する必要がある場合は、代わりに jwk-set-uri プロパティを指定して、公開鍵を公開する承認サーバーのエンドポイントを指すことができます。
jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs
サーバーでJWTトークンを検証するために必要なのはこれだけです。
4.6. テスト
テストのために、JUnitをセットアップします。 このテストを実行するには、承認サーバーとリソースサーバーが稼働している必要があります。
テストでは、readスコープトークンを使用してresource-server-jwtからFooを取得できることを確認しましょう。
@Test
public void givenUserWithReadScope_whenGetFooResource_thenSuccess() {
String accessToken = obtainAccessToken("read");
Response response = RestAssured.given()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.get("http://localhost:8081/resource-server-jwt/foos");
assertThat(response.as(List.class)).hasSizeGreaterThan(0);
}
上記のコードでは、行#3で、認証サーバーから read スコープのアクセストークンを取得し、シーケンス図の1から7までのステップをカバーしています。
ステップ8は、 RestAssuredのget()呼び出しによって実行されます。 ステップ9は、私たちが見た構成でリソースサーバーによって実行され、ユーザーとしては透過的です。
5. リソースサーバー–不透明なトークンの使用
次に、不透明なトークンを処理するリソースサーバーの同じコンポーネントを見てみましょう。
5.1. Mavenの依存関係
不透明なトークンをサポートするには、さらにoauth2-oidc-sdk依存関係が必要です。
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>8.19</version>
<scope>runtime</scope>
</dependency>
5.2. モデルとコントローラー
これには、Barリソースを追加します。
public class Bar {
private long id;
private String name;
// constructor, getters and setters
}
また、 Bar をディッシュするために、以前のFooControllerと同様のエンドポイントを持つBarControllerを用意します。
5.3. application.yml
ここのapplication.ymlに、認証サーバーのイントロスペクションエンドポイントに対応するintrospection-uriを追加する必要があります。 前に述べたように、これは不透明なトークンが検証される方法です:
server:
port: 8082
servlet:
context-path: /resource-server-opaque
spring:
security:
oauth2:
resourceserver:
opaque:
introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
introspection-client-id: barClient
introspection-client-secret: barClientSecret
5.4. セキュリティ構成
BarリソースのFooと同様のアクセスレベルを維持しながら、この構成クラスもoauth2ResourceServer()DSLを使用してopaqueToken()を呼び出し、不透明なトークンタイプの使用:
@Configuration
public class OpaqueSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}")
String introspectionUri;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}")
String clientId;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}")
String clientSecret;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authz -> authz
.antMatchers(HttpMethod.GET, "/bars/**").hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/bars").hasAuthority("SCOPE_write")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(token -> token.introspectionUri(this.introspectionUri)
.introspectionClientCredentials(this.clientId, this.clientSecret)));
}
}
ここでは、使用する承認サーバーのクライアントに対応するクライアント資格情報も指定しています。 これらは、application.ymlで以前に定義しました。
5.5. テスト
JWTの場合と同様に、不透明なトークンベースのリソースサーバー用にJUnitをセットアップします。
この場合、writeスコープのアクセストークンがBarをresource-server-opaqueにPOSTできるかどうかを確認しましょう。
@Test
public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() {
String accessToken = obtainAccessToken("read write");
Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(newBar)
.log()
.all()
.post("http://localhost:8082/resource-server-opaque/bars");
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value());
}
CREATEDのステータスが返される場合は、リソースサーバーが不透明なトークンを正常に検証し、Barを作成したことを意味します。
6. 結論
このチュートリアルでは、JWTと不透明なトークンを検証するためにSpringSecurityベースのリソースサーバーアプリケーションを構成する方法を説明しました。
ご覧のとおり、最小限のセットアップで、Springは、発行者を使用してトークンをシームレスに検証し、要求側(この場合はJUnitテスト)にリソースを送信することを可能にしました。
いつものように、ソースコードはGitHubでから入手できます。