Edited at

Spring Security 5.1でOAuth 2.0のリソースサーバーを作る

Spring Security 5.1から、OAuth 2.0のリソースサーバー作成機能が追加されましたので、紹介します。


2018-11-06 改訂: Spring Boot 2.1が正式リリースされましたので改訂しました!



依存ライブラリ

今回はSpring Bootで作っていきます。


pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project ...>
...

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
</properties>

<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.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

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

</project>


特に重要なのがこの2つです。

        <dependency>

<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

spring-security-oauth2-resource-server がリソースサーバー機能を持つライブラリです。

spring-security-oauth2-jose は、JWTに関する機能を持っています。

どうもリソースサーバー機能はJWTが必須っぽいです(自信なし)。


application.propertiesの設定

必要な設定は2つだけです。


application.properties

# 認可サーバーのIssuer Identifier

spring.security.oauth2.resourceserver.jwt.issuer-uri: http://localhost:9000/auth/realms/todo-api
# 認可サーバーのJWK Setが返ってくるURL
spring.security.oauth2.resourceserver.jwt.jwk-set-uri: http://localhost:9000/auth/realms/todo-api/protocol/openid-connect/certs

手元の認可サーバーは、DockerでKeycloakサーバーをポート番号9000で立てています。

これらのプロパティは、どちらか一方でOKです。 issuer-uri が使える認可サーバー(OpenID Connectに対応したサーバー)を使うと、こちらだけ設定すれば jwk-set-uri は自動で設定されます。

(リソースサーバー起動時に認可サーバーにアクセスして、 jwk-set-uri をもらいます。)


Java Configの記述


SecurityConfig.java

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers(HttpMethod.GET, "/hello").hasAuthority("SCOPE_hello:read")
.anyRequest().authenticated();
http.oauth2ResourceServer()
.jwt();
}
}


特に重要なポイントは、URLとスコープのマッピングの部分です。

.mvcMatchers(HttpMethod.GET, "/hello").hasAuthority("SCOPE_hello:read")

hasAuthority() メソッドでスコープを指定します。

スコープは SCOPE_スコープ名 と指定します。


実行

「Hello!」を返すだけの簡単なRestControllerを作っておきます。


HelloController.java

@RestController

public class HelloController {

@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}


アクセストークン無しで実行すると401が返ってきます。


アクセストークン無しで実行

$ curl -v -X GET http://localhost:8090/hello

> GET /hello HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401
< WWW-Authenticate: Bearer
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Sat, 08 Sep 2018 07:27:50 GMT
<

アクセストークンを指定して実行すると、200で「Hello!」が返ってきます。


アクセストークン(JWT)を指定して実行

$ curl -v -X GET -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzbGcyMG9uOVg2czZFOExmNDZfQmRuaExHQy1xZnIyMVlvWE9nQVFKRlIwIn0.eyJqdGkiOiJlNzhkYTg0Yy1iZDYxLTQyY2YtOGJlYi05MGFhYTQ0NzQ5YzUiLCJleHAiOjE1MzYzOTE3NDEsIm5iZiI6MCwiaWF0IjoxNTM2MzkxNDQxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAvYXV0aC9yZWFsbXMvaGVsbG8tYXBpIiwiYXVkIjoidHJhaW5pbmc2LWZyb250LXNlcnZpY2UiLCJzdWIiOiIxYWI5Yjg4Ny0yNDRhLTRjZTktYTBjMy1iZTc2ZGE4NzZiMTQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0cmFpbmluZzYtZnJvbnQtc2VydmljZSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImJlMTQ4MmFkLTc0YjAtNGY3OS1iNjkwLWEzOTFmOTliYzkxZiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4MDgwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJoZWxsbzpyZWFkIHByb2ZpbGUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyIn0.IfSf25zltXHu550TMrnp8O1We-vLx4O8b74ooLFUC5CLsWSHQ8rqG4JwqUX_LYDhVaameSy5ix3eRLNTAkjXc24WqsS856zGy2ULxxh7YItQ0CZX3qa7GxZ2Acv7nAkJeAE6eG_6B68o7H4MqdSywDA4qy4tNL4UF7wKQ5IJcMggYPQYUh45GchsDiF1h27ePDeUaUPLMHW04sqxhBHzsOaaVubglYWIG4BCkDwEk4JLNmd0mYBS4mXmYgmKx9_gW1NQasXfd4vLkYVksJIyncY7xk2QPDImmu6ip0_QXH5pIYr7K13DVe_Tl8Xc2ob_9OWODIYOBqYJzDU5XI9ClA" http://localhost:8090/hello

> GET /hello HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJzbGcyMG9uOVg2czZFOExmNDZfQmRuaExHQy1xZnIyMVlvWE9nQVFKRlIwIn0.eyJqdGkiOiJlNzhkYTg0Yy1iZDYxLTQyY2YtOGJlYi05MGFhYTQ0NzQ5YzUiLCJleHAiOjE1MzYzOTE3NDEsIm5iZiI6MCwiaWF0IjoxNTM2MzkxNDQxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAvYXV0aC9yZWFsbXMvaGVsbG8tYXBpIiwiYXVkIjoidHJhaW5pbmc2LWZyb250LXNlcnZpY2UiLCJzdWIiOiIxYWI5Yjg4Ny0yNDRhLTRjZTktYTBjMy1iZTc2ZGE4NzZiMTQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0cmFpbmluZzYtZnJvbnQtc2VydmljZSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImJlMTQ4MmFkLTc0YjAtNGY3OS1iNjkwLWEzOTFmOTliYzkxZiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4MDgwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJoZWxsbzpyZWFkIHByb2ZpbGUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyIn0.IfSf25zltXHu550TMrnp8O1We-vLx4O8b74ooLFUC5CLsWSHQ8rqG4JwqUX_LYDhVaameSy5ix3eRLNTAkjXc24WqsS856zGy2ULxxh7YItQ0CZX3qa7GxZ2Acv7nAkJeAE6eG_6B68o7H4MqdSywDA4qy4tNL4UF7wKQ5IJcMggYPQYUh45GchsDiF1h27ePDeUaUPLMHW04sqxhBHzsOaaVubglYWIG4BCkDwEk4JLNmd0mYBS4mXmYgmKx9_gW1NQasXfd4vLkYVksJIyncY7xk2QPDImmu6ip0_QXH5pIYr7K13DVe_Tl8Xc2ob_9OWODIYOBqYJzDU5XI9ClA
>
< HTTP/1.1 200
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 6
< Date: Sat, 08 Sep 2018 07:26:02 GMT
<
Hello!


参考資料