Spring Security 5.xでOAuth 2.0のリソースサーバーを作る（JWT編）

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

> 2018-11-06 改訂: Spring Boot 2.1が正式リリースされましたので改訂しました！
> 2019-12-03 改訂: Spring Boot 2.2が正式リリースされましたので改訂しました！

# 依存ライブラリ

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

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
    ...

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

    <properties>
        <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-oauth2-resource-server</artifactId>
        </dependency>
    </dependencies>

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

</project>
```

特に重要なのがこれです。Spring Boot 2.2から、リソースサーバー用のStarterライブラリが追加されました。

```xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
```

> クライアント用のStarterもあります（spring-boot-starter-oauth2-client）

# application.propertiesの設定

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

```properties: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
```

手元の認可サーバーは、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とスコープのマッピングの部分です。

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

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

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

# 実行

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

```java:HelloController.java
@RestController
public class HelloController {

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

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

```bash:アクセストークン無しで実行
$ 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!」が返ってきます。

```bash:アクセストークン(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!
```

# サンプルコード
https://github.com/MasatoshiTada/oauth2-with-spring-security-51/tree/master/resource-server

# 参考資料

- [公式のサンプルコード](https://github.com/spring-projects/spring-security/tree/5.1.x/samples/boot/oauth2resourceserver)
- [JWTによるJSONに対する電子署名と、そのユースケース](https://dev.classmethod.jp/server-side/json-signing-jws-jwt-usecase/)
- [KeycloakのJava Servlet Filter Adapterを使ってOpenID Connect](http://d.hatena.ne.jp/Kazuhira/20180325/1521979482)