前回はJavaScriptを用いてSPAでアクセストークンを取得してみましたが、今回はSpring Securityを用いてサーバーによるアクセストークン取得をやってみたいと思います。
といいましても、Spring Securityが細かい実装の部分をサポートしてくれているので、やることはそんなに多くないです。が、中身の理解が大切になってくるかなと思いますのでまずはそちらから。
Spring Security OAuth2 Client の中身を理解したい
参考にしたのは公式リファレンスのこのあたりです。
ざーっくりと主要なクラス、インターフェイスをまとめてみるとこんな感じかなと。それぞれの関係性は追えていない部分もあり、イメージで引いているものなので参考程度に。
この中で特に中心的な役割になるのが次のクラス、インターフェイスになるかと思います。
- ClientRegistration:クライアントの登録情報。クライアントIDやクライアントシークレットの値、また認可URIやトークンURIの値を持つ。
- OAuth2AuthorizedClient:リソースオーナーから認可を受けたことを示す。
ClientRegistration
と アクセストークン、リフレッシュトークンを結びつける。 - OAuth2AuthorizedClientProvider:特定の認可フローの実装。結果として
OAuth2AuthorizedClient
を返す。
ClientRegistration
と OAuth2AuthorizedClientProvider
はSpring Bootのapplicationファイルから設定する感じになります。 OAuth2AuthorizedClient
はアクセストークンを受け取った後に生まれるもので、リソースサーバーにアクセスする処理においてアクセストークンを取り出すときなどに使用するイメージかなと思っています。
とりあえず動かしてどんな感じなのかを調べていきたいと思います。
Spring Security でクライアントを作成する
Spring Boot を使って行きます。 build.gradle.kts
はこちらなんですが、マルチプロジェクトの構成にしているのフルで依存関係を知りたければ以下のリポジトリを見てもらえればと思います。OAuth2 クライアントにするには spring-boot-starter-oauth2-client
が必要になります。
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation(platform("org.junit:junit-bom:5.7.0")) testImplementation("org.junit.jupiter:junit-jupiter") } tasks.test { useJUnitPlatform() }
Config クラスはこのようになりました。ユーザー管理を行うよう設定します。というのも、アクセストークンやリフレッシュトークンはユーザーごとにセッションに保存されるみたいなので、設定が必要になるようです。
configure
メソッドで http.oauth2Client()
によりクライアントとして設定します。
package com.example.spring.security.oauth2.client.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); userDetailsManager.createUser(User.withUsername("bob").password("12345").roles("user").build()); return userDetailsManager; } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin(); http.oauth2Client(); http.authorizeRequests() .anyRequest().authenticated(); } }
applicationファイルはこうなりました。クライアントシークレットが載ってますが、これは学習用で作成した値なので載せてます。本来この値は公開してはいけないクレデンシャル値なので、本番環境とかの値とかであれば絶対公開しないようにしましょう。
spring: security: oauth2: client: registration: spring-oauth2-client-demo: client-id: spring-oauth2-client client-secret: 2f7b1965-44d3-46df-9e5b-1655af84d0f2 provider: "local-docker-keycloak" authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/home" scope: spring-oauth2-client provider: local-docker-keycloak: authorization-uri: http://localhost:8081/auth/realms/spring-oauth2/protocol/openid-connect/auth token-uri: http://localhost:8081/auth/realms/spring-oauth2/protocol/openid-connect/token
値は大きく2つのカテゴリに分けられていて、 registration
で設定している値が ClientRegistration
クラスに対応しています。provider
で設定している値が OAuth2AuthorizedClientProvider
クラスに対応しています。
ここで設定している spring-oauth2-client-demo
という値が registration id と言われるものですが、この値は認可フローを開始するURLの一部となっていて、この場合だと /oauth2/authorization/spring-oauth2-client-demo
になります。URLの末尾が registration id になるというわけです。このURLにアクセスすると設定している値に基づいて認可フローが開始されます。 なので、リンクをこのURLに指定して認可フローを開始させるといった使い方が予想できますね。
spring-oauth2-client-demo.provider
の箇所では使用する provider
を指定しています。イメージとしては ClientRegistration
クラスごとに、使用する認可サーバーの情報を OAuth2AuthorizedClientProvider
クラスとして指定するような感じでしょうか。今回は一つの registration
と provider
を設定していますが、複数設定することもできるというわけです。このあたりは柔軟な構造になっている印象を受けますね。
一つコントローラーを作成しエンドポイントを作成します。
package com.example.spring.security.oauth2.client.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/home") public class HomeController { private final Logger logger = LoggerFactory.getLogger(HomeController.class.getName()); private final OAuth2AuthorizedClientService authorizedClientService; public HomeController(OAuth2AuthorizedClientService authorizedClientService) { this.authorizedClientService = authorizedClientService; } @GetMapping public String home(Authentication authentication, Model model) { OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient( "spring-oauth2-client-demo", authentication.getName() ); model.addAttribute("authorized", authorizedClient != null); if (authorizedClient != null) { logger.info("access token value [{}]", authorizedClient.getAccessToken().getTokenValue()); } return "home"; } }
authorizedClientService
に対する呼び出しを見てもらえればわかりますが、 Authentication#getName()
の値から OAuth2AuthorizedClient
を取得できます。そしてこのオブジェクトを経由して OAuth2AccessToken
オブジェクトを取得できます。
このエンドポイントで返しているHTMLはこうなっています。認可しているかどうかで表示内容を変更するようにしています。認可していない場合、認可フローを開始するリンクをアクティブにします。
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Home</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> </head> <body> <div class="container"> <div class="row"> <div class="col"> <h1>OAuth2 Client Demo</h1> </div> </div> <div class="row"> <div class="col"> <p th:if="${authorized}">本アプリを認可しました。</p> </div> </div> <div class="row"> <div class="col"> <a class="btn btn-primary" th:classappend="${authorized ? 'disabled' : ''}" th:href="@{/oauth2/authorization/spring-oauth2-client-demo}" href="#">Keycloakにより本アプリを認可する</a> </div> </div> </div> </body> </html>
実装はこんな感じになりました。
動作としてはこうなります。
まず /home
にアクセスします。
認可フローを開始するとKeycloakにリダイレクトされログインを求められます。
ログインに成功すると /home
に戻されます。この実装だとコンソールログにアクセストークンを出力してますがあくまでも検証ですので本番ではNGです。
2021-03-03 21:02:17.954 INFO 10823 --- [nio-8080-exec-7] c.e.s.s.o.c.controller.HomeController : access token value [eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJEeDVVV1p....