Spring Security 5.1から、OAuth 2.0のリソースサーバー作成機能が追加されましたので、紹介します。
2018-11-06 改訂: Spring Boot 2.1が正式リリースされましたので改訂しました!
2019-12-03 改訂: Spring Boot 2.2が正式リリースされましたので改訂しました!
依存ライブラリ
今回はSpring Bootで作っていきます。
<?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ライブラリが追加されました。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
クライアント用のStarterもあります(spring-boot-starter-oauth2-client)
application.propertiesの設定
必要な設定は2つだけです。
# 認可サーバーの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の記述
@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を作っておきます。
@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!」が返ってきます。
$ 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!
サンプルコード