Keycloak 4.0.0.Finalがリリースされました。Release Notesを見ていると、その中にSpring Boot 2へのサポートが追加されたと
書かれていたので、ちょっと試してみようかと。
ところが、ドキュメントにはSpring Boot 2についての記載がありません。Spring Boot 1向けのAdapterの記述は
あるのですが…。
なので、ここはソースコードから存在を確認することに。
https://github.com/keycloak/keycloak/blob/4.0.0.Final/boms/adapter/pom.xml#L139
「keycloak-spring-boot-2-starter」を使えばよいみたいです。
最初の誤解
予備知識なしでやろうとして、KeycloakのSpring Boot 1/2 AdapterはてっきりSpring Securityと関連するものかと思って
いたのですが、どうやらそうではないようです。
KeycloakのSpring Boot Adapterと、Spring Security Adapterは別物です。
最初、てっきりSpring Boot Adapterの方がSpring Security Adapterの延長線上にいるものかと思っていたら、全然違うことに
途中で気付きました…。
参考)
Easily secure your Spring Boot applications with Keycloak - RHD Blog
今回は、Spring Boot 2 Adapterを使うことにします。
お題
今回は、以前に書いたこちらのエントリと同様、REST APIに対してログイン必須のものとそうでないものを作成して、
認証必須の場合の挙動の確認や、Keycloakから取得できる情報の確認、ログアウトなどをやってみたいと思います。
KeycloakのJava Servlet Filter Adapterを使ってOpenID Connect - CLOVER
利用するのは、前回同様OpenID Connectです。
環境
今回の環境は、こちら。
$ java -version openjdk version "1.8.0_171" OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.18.04.1-b11) OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode) $ mvn -version Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_171, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-23-generic", arch: "amd64", family: "unix"
Keycloakは、4.0.0.Finalです。
準備
今回の、Maven依存関係はこちら。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.0.3.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.keycloak.bom</groupId> <artifactId>keycloak-adapter-bom</artifactId> <version>4.0.0.Final</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-2-starter</artifactId> </dependency> </dependencies>
keycloak-adapter-bomのimportと、keycloak-spring-boot-2-starterの依存関係の追加だけではダメで、Spring Boot自体も
dependencyManagementに入れておかないと、実行時にライブラリのバージョン不整合が起こってエラーになります…。
Keycloakは起動済みとして、Keycloakおよび母体のWildFlyの管理アカウントを作成します。
$ bin/add-user-keycloak.sh -u keycloak-admin -p password $ bin/add-user.sh -u admin -p password $ bin/jboss-cli.sh -c -u=admin -p=password --command=reload
REST APIで使うユーザーは、あとで作成しましょう。
サンプルコードの作成
では、サンプルコードを作っていきます。
まずは、ログインしなくてもアクセスできる@RestController。
src/main/java/org/littlewings/keycloak/spring/controller/PublicController.java
package org.littlewings.keycloak.spring.controller; import java.security.Principal; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.keycloak.KeycloakSecurityContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("public") public class PublicController { @GetMapping("hello") public String hello() { return "Hello Application!!"; } @GetMapping("keycloak-context") public Map<String, Object> keycloakContext(HttpServletRequest request) { Map<String, Object> results = new LinkedHashMap<>(); Principal principal = request.getUserPrincipal(); if (principal != null) { results.put("principal-type", principal.getClass().getName()); results.put("principal-name", principal.getName()); } KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); if (context != null) { results.put("id-token-from-request", context.getIdToken()); results.put("roles-from-request", context.getToken().getRealmAccess().getRoles()); } KeycloakSecurityContext contextFromSession = (KeycloakSecurityContext) request.getSession().getAttribute(KeycloakSecurityContext.class.getName()); if (contextFromSession != null) { results.put("id-token-from-session", contextFromSession.getIdToken()); results.put("roles-from-session", context.getToken().getRealmAccess().getRoles()); } return results; } }
2つ目のメソッドでは、Keycloakの情報にHttpServletRequestまたはHttpSessionからアクセスしようとしています。
@GetMapping("keycloak-context") public Map<String, Object> keycloakContext(HttpServletRequest request) { Map<String, Object> results = new LinkedHashMap<>(); Principal principal = request.getUserPrincipal(); ... KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); ... KeycloakSecurityContext contextFromSession = (KeycloakSecurityContext) request.getSession().getAttribute(KeycloakSecurityContext.class.getName()); ...
続いて、アクセスするのにログインが必要な@RestController。
src/main/java/org/littlewings/keycloak/spring/controller/SecureController.java
package org.littlewings.keycloak.spring.controller; import java.net.URI; import java.security.Principal; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.keycloak.KeycloakSecurityContext; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("secure") public class SecureController { @GetMapping("hello") public String hello() { return "Hello Secure Application!!"; } @GetMapping("keycloak-context") public Map<String, Object> keycloakContext(HttpServletRequest request) { Map<String, Object> results = new LinkedHashMap<>(); Principal principal = request.getUserPrincipal(); if (principal != null) { results.put("principal-type", principal.getClass().getName()); results.put("principal-name", principal.getName()); } KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); if (context != null) { results.put("id-token-from-request", context.getIdToken()); results.put("roles-from-request", context.getToken().getRealmAccess().getRoles()); } KeycloakSecurityContext contextFromSession = (KeycloakSecurityContext) request.getSession().getAttribute(KeycloakSecurityContext.class.getName()); if (contextFromSession != null) { results.put("id-token-from-session", contextFromSession.getIdToken()); results.put("roles-from-session", context.getToken().getRealmAccess().getRoles()); } return results; } @GetMapping("logout") public ResponseEntity<String> logout(HttpServletRequest request) throws ServletException { request.logout(); return ResponseEntity.status(HttpStatus.SEE_OTHER).location(URI.create("/public/hello")).build(); } }
@GetMapping("keycloak-context") public Map<String, Object> keycloakContext(HttpServletRequest request) { Map<String, Object> results = new LinkedHashMap<>(); Principal principal = request.getUserPrincipal(); ... KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); ... KeycloakSecurityContext contextFromSession = (KeycloakSecurityContext) request.getSession().getAttribute(KeycloakSecurityContext.class.getName()); ... return results; } @GetMapping("logout") public ResponseEntity<String> logout(HttpServletRequest request) throws ServletException { request.logout(); return ResponseEntity.status(HttpStatus.SEE_OTHER).location(URI.create("/public/hello")).build(); }
ところで、今回使っているKeycloakのAPI、特にSpring Boot Adapterには説明がありません。
で、ドキュメントのどのAPIを見ればよいかというと、Java Servlet Filter Adapterなどと同じく、Servlet APIで記載されたドキュメントを見ます。
Spring Boot Adapterのドキュメントとしても、サポートしているEmbedded Containerは、
- Tomcat
- Undertow
- Jetty
ということなので、やっぱりServletベースなのですね。
また、Spring Bootを使ったアプリケーションの起動クラスを作成します。
src/main/java/org/littlewings/keycloak/spring/App.java
package org.littlewings.keycloak.spring; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
設定
Spring Boot 1/2 Adapterを使うと、keycloak.jsonではなく、application.propertiesにKeycloakの設定を書くことができます。
今回作成したapplication.propertiesは、こちら。
src/main/resources/application.properties
keycloak.realm = demo-api keycloak.auth-server-url = http://172.17.0.2:8080/auth keycloak.resource = sample-rest-api keycloak.credentials.secret = 72235f7b-06b3-4efd-96be-d8f4f8fada74 keycloak.security-constraints[0].auth-roles[0] = users keycloak.security-constraints[0].security-collections[0].name = secure area keycloak.security-constraints[0].security-collections[0].patterns[0] = /secure/* spring.jackson.serialization.indent_output=true
realmは「demo-api」、KeycloakへのアクセスURLは「http://172.17.0.2:8080/auth」、Keycloak側へ登録するClientは「sample-rest-api」、
Clientを作成した時のCredintialを設定します。
※Credentialは、KeycloakでClientを作成してから判明します
アクセス設定は、「/secure/*」配下のURLに、「users」ロールを持ったアカウントのみアクセスできるようにしています。
Keycloak側での設定
OpenID Connectでアクセスする、Keycloak側の設定をします。まあ、順番的には先にKeycloakの設定をしてから、クライアントアプリケーションの設定の順番
なのですが、今回はこういう感じで。
Realmの作成。名前は、「demo-api」。
Clientの作成。名前は、「sample-rest-api」。
「Access Type」を「confidential」に変更して「Save」すると、Credentialsを確認することができるようになります。
これを、application.propertiesに設定します。
keycloak.credentials.secret = 72235f7b-06b3-4efd-96be-d8f4f8fada74
続いて、ロールを作成。「users」と、「others」の2つのロールを作成します。
※「others」は省略
最後に、ユーザー作成。ロール「users」に紐付ける「api-user」と、「others」に紐付ける「other-user」を作成します。
※パスワードの設定は、省略しています
※「other-user」は省略
ユーザーのロールの割り当てとKeycloakのクライアント側の設定によって、ログイン後にどの範囲にアクセスできるかどうかが決まります。
確認してみる
それでは、確認してみましょう。
パッケージング。
$ mvn package
起動。
$ java -jar target/keycloak-spring-boot-adapter-0.0.1-SNAPSHOT.jar
まずは、ログインが必要ない「/public/hello」へ。そのまま表示できます。
Keycloakの情報を表示する「/public/keycloak-context」ページは、ログインしないままだと中身が空っぽになります。
次に、ログインを要求する「/secure/hello」にアクセスしてみます。すると、Keycloakへのログインを求められるので、「api-user」でログインします。
「/secure/keycloak-context」にアクセスすると、Keycloakから取得した情報を参照することができます。
この状態で、「/public/keycloak-context」へアクセスすると、今度はログインが必須のページではなくてもKeycloakの情報を表示することができます。
「/secure/logout」でログアウト。
ログアウトすると、ログインが必要ない「/public/hello」に戻り、ログイン状態も解除されます(「/secure配下にアクセスすると、Keycloakからログインを
求められる)。
ログアウト後、今度は「others」ロールを持つ「other-user」でアクセスしてみます。すると、403エラーになりました。
ここの設定どおり、「users」ロールに属するユーザーでなければ、アクセスできません、と。
keycloak.security-constraints[0].auth-roles[0] = users keycloak.security-constraints[0].security-collections[0].name = secure area keycloak.security-constraints[0].security-collections[0].patterns[0] = /secure/*
もう少し中身を
KeycloakのSpring Boot Adapterは、サポートしているコンテナ、Tomcat、Undertow、JettyにKeycloakの機能を組み込んでいくものみたいです。
https://github.com/keycloak/keycloak/blob/4.0.0.Final/adapters/oidc/spring-boot-adapter-core/src/main/java/org/keycloak/adapters/springboot/KeycloakBaseSpringBootConfiguration.java
Servlet Filterなどではありません。
通常のWebアプリケーションであれば、keycloak.jsonとweb.xmlで設定するところをapplication.propertiesで設定できるということでしょう。
この部分は、まさしくweb.xmlのsecurity-constraintに該当しますね。
keycloak.security-constraints[0].auth-roles[0] = users keycloak.security-constraints[0].security-collections[0].name = secure area keycloak.security-constraints[0].security-collections[0].patterns[0] = /secure/*
まとめ
Keycloak 4.0.0.Finalで追加された、Spring Boot 2 Adapterを試してみました。
そもそもKeycloakでSpring Bootを使うこと自体が初めてでしたし、Spring Securityとは関係ない世界にいるんだなーということを最初認識していなくて
戸惑ったりしましたが、そのあたりも把握できてよかったです。