前回の続きです。
KeycloakとSpringBoot/SecurityでOpenID Connect(リソースサーバー編)
目標
KeycloakをID Providerとして、クライアントアプリケーションを作成する(Webアプリ)。
本当はSpring Framework(not Boot)で作りたかったのですが、スピード重視で一旦Bootでやります。
(検索するとBootの情報しかない気がします・・・)
環境など
ツールなど | バージョンなど |
---|---|
MacbookPro | macOS Mojave 10.14.5 |
IntelliJ IDEA | Ultimate 2019.3.3 |
Java | AdoptOpenJDK 11 |
apache maven | 3.6.3 |
JUnit | 5.6.0 |
Postman | 7.19.1 |
Spring Boot | 2.2.6.RELEASE |
Spring Security | 5.2.2.RELEASE |
Keycloak | 9.0.3 |
Docker | 19.03.8, build afacb8b |
Thymeleaf | 3.0.11.RELEASE |
クライアントアプリを作る
Spring Bootでちゃちゃっと作ります。
前回の雛形の作成までを参考にプロジェクトを作ってください。
1.ポート番号の設定など
このまま作ると8080
になってリソースサーバーとかぶるので、変更します。
Keycloakが8088
なので、どうしようかな。
9000
くらいにしときますか。空いてるか心配な方はPortscan
(Macの場合)などで調べるといいかもしれませんね。
server.port=9000
これで実行すれば、localhost:9000
でアクセスできます。
2.ページを作成
htmlを表示するのに、なんかおすすめらしいThymeleaf(タイムリーフ)を使ってみます
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
(1)indexページ
indexページはnon-secureでいいでしょう。このページは固定なのでThymeleafは使っていません。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<main role="main" class="inner cover">
<h1 class="cover-heading">Apache Keycloak SSO demonstration</h1>
<p class="lead">
</p>
<p class="lead">
<a href="/products" class="btn btn-lg btn-secondary">Search products</a>
</p>
</main>
</div>
</body>
これで実行して、http://127.0.0.1:9000/ にアクセスすると、以下のようなページが開きます。
![]() |
---|
(2)productsページ
後でセキュアなページにする予定のページを作ります。
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@Controller
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping(path = "/products")
public String getProducts(Model model){
model.addAttribute("products", productService.getProducts());
return "products";
}
}
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class ProductService{
public List getProducts(){
return Arrays.asList("Mazda","Toyota","Audi");
}
}
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Keycloak</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<main role="main" class="inner cover">
<h1 class="cover-heading">Keycloak SSO demonstration</h1>
<div style="width: 18rem; padding: 10px; display: block; margin-left: auto;margin-right: auto">
<ul class="list-group">
<li th:each="element : ${products}" class="list-group-item">[[${element}]]</li>
</ul>
</div>
</main>
</div>
</body>
</html>
実行するとこうなります。
![]() |
---|
3.Keycloak関係の設定を追加
(1)依存関係の追加
Keycloakと、SpringSecurityを追加します。
<properties>
<java.version>11</java.version>
<keycloak.version>9.0.3</keycloak.version>
</properties>
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
...
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${keycloak.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
(2)セキュリティ設定クラスを追加
package com.example.client_demo;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/products*").hasRole("user")
.anyRequest().permitAll();
}
}
(3)application.properites
keycloak.enabled=true
keycloak.auth-server-url=http://localhost:8088/auth
keycloak.realm=myrealm
keycloak.resource=client1
keycloak.public-client=true
ポート番号、レルム名は自分でつけたものに合わせてください。
keycloak.resource
は、これからKeycloakに作成するクライアントIDです。
この状態で起動すると、/products
ページは401
になる・・・
というか、Keycloakの画面に飛んで、「そんなクライアントいないよ」と言われますね。
![]() |
---|
Keycloakにクライアントを追加する
前回やったようにクライアントを追加します。
![]() |
---|
-
AccessType
- publicにします
-
Valid Redirect URIs
-
http://localhost:9000*
とします
-
上記以外はデフォルトでいいかと思います。
ポート番号は自分で設定したのと合わせてください。
![]() |
---|
リダイレクトURLに、localhost
パターンとローカルIP指定パターンの両方登録しておきました。
[Save]を忘れずに。
これで接続してみます。
(127.0.0.1:9000ではなく、localhost:9000でアクセスしてください。リダイレクトがうまくかかりません)
クライアントアプリを再起動して/products
ページにアクセス!
![]() |
---|
ログイン画面が出ました!
作成済みの任意のユーザー名とパスワードでログインしてみましょう!
![]() |
---|
あ・・・あれ、Forbidden(403)・・・
当たり前です。ユーザーにロールを設定していませんでした(テヘペロ)
ユーザーにロールを割り当てる
1.ロールを追加
Keycloakの管理画面で、Roles
から[Add Role]をクリックします。
![]() |
---|
Role Nameにuser
と入力し、[Save]。
![]() |
---|
2.ユーザーにロールを割り当てる
-
Users
-[View all users]で、任意のユーザーを選ぶ - [Role Mappings]タブをクリック
-
Available Rolesにある
user
を選択して、[Add selefcted]とする
![]() |
---|
アクセス!
![]() |
---|
きました!
ユーザー情報を取得する
といってもユーザー情報APIを叩くのではなくて、アクセストークンを解析して取ってみようと思います。
1.ユーザー情報閲覧ページを追加
@GetMapping(path = "userinfo")
public String userInfo(HttpServletRequest request, Model model){
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();
KeycloakPrincipal principal=(KeycloakPrincipal)token.getPrincipal();
KeycloakSecurityContext session = principal.getKeycloakSecurityContext();
AccessToken accessToken = session.getToken();
UserInfo info = new UserInfo();
info.username = accessToken.getPreferredUsername();
info.emailID = accessToken.getEmail();
info.lastname = accessToken.getFamilyName();
info.firstname = accessToken.getGivenName();
info.realmName = accessToken.getIssuer();
AccessToken.Access realmAccess = accessToken.getRealmAccess();
info.roles = realmAccess.getRoles().toString();
info.scopes = accessToken.getScope();
model.addAttribute("userinfo", info);
return "userinfo";
}
public static class UserInfo {
public String username;
public String emailID;
public String lastname;
public String firstname;
public String realmName;
public String roles;
public String scopes;
}
@GetMapping(path = "/logout")
public String logout(HttpServletRequest request) throws ServletException {
request.logout();
return "redirect:/";
}
ついでにログアウトボタンも付けましょう。
<!doctype html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<title>UserInfo</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<main role="main" class="inner cover">
<h1 class="cover-heading">User Info</h1>
<p class="lead">
</p>
<p class="lead">
<div th:text="|user name = ${userinfo.username}|"></div>
<div th:text="|email = ${userinfo.emailID}|"></div>
<div th:text="|last name = ${userinfo.lastname}|"></div>
<div th:text="|first name = ${userinfo.firstname}|"></div>
<div th:text="|roles = ${userinfo.roles}|"></div>
<div th:text="|scopes = ${userinfo.scopes}|"></div>
<div th:text="|access token = ${userinfo.accessToken}|"></div>
<div th:text="|id token = ${userinfo.idToken}|"></div>
</p>
<p class="lead">
<a href="/logout" class="btn btn-lg btn-secondary">Logout</a>
</p>
</main>
</div>
</body>
デバッグ用にAccess TokenもID Tokenも晒しちゃいます。
2.indexページにユーザー情報ページへのリンクを追加
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<main role="main" class="inner cover">
<h1 class="cover-heading">Apache Keycloak SSO demonstration</h1>
<p class="lead">
</p>
<p class="lead">
<a href="/products" class="btn btn-lg btn-secondary">Search products</a>
</p>
<p class="lead">
</p>
<p class="lead">
<a href="/userinfo" class="btn btn-lg btn-secondary">User Information</a>
</p>
</main>
</div>
3.実行!
indexページ
![]() |
---|
ユーザー情報ページ
![]() |
---|
体裁は、まあ^^;
scopeですが、KeycloakのClient設定で、[Client Scopes]の[Default Client scopes]から、hello
をセットしておいたので、scopes
にいますね。
ログアウトをクリックすると、indexに戻ります。で、またログインから実行できます。
4.このアクセストークンをリソースサーバーに投げてみる
これで、前回作成したリソースサーバーと同じレルムで動くクライアントが出来ました。
SSOでリソースサーバーにもアクセスできるはずです。
とりあえず手動で投げます。CurlとかPostmanで。
以下はPostmanの例です。
![]() |
---|
scope
にhello
がいるので、/hello
はアクセスできます。
/list
はアクセスできません。
[Default scopes]にuser
も追加すれば、見られるようになります。
感想
次は、このクライアントから、リソースサーバーのAPIを叩いて表示出来るようにしたいですね。いわゆるSSO(シングルサインオン)の実現です。
トークンのやり取りどうなるかしら?
それと、サーバー(tomcat)のアクセスログが全然見えなくて困ったので、以下のような設定を追加しました。
server.tomcat.accesslog.enabled=true
server.tomcat.basedir=/dev
server.tomcat.accesslog.directory=stdout
server.tomcat.accesslog.suffix=
server.tomcat.accesslog.prefix=
server.tomcat.accesslog.file-date-format=
サンプルプロジェクト
ここまでのサンプルプロジェクトは、以下にアップしてあります。
https://github.com/le-kamba/SpringSecurityAndKeycloakSamples
参考ページ
このサンプルのベースコードはこちらです。
http://www.javafoundation.xyz/2018/07/integrate-keycloak-with-spring-security.html
Spring Boot で Thymeleaf 使い方メモ
https://qiita.com/opengl-8080/items/eb3bf3b5301bae398cc2
アクセストークンを解析する方法の参考になりました。
https://stackoverflow.com/questions/48432626/get-the-accesstoken-of-keycloak-in-spring-boot
ログアウト後にindexページへリダイレクトする方法の参考になりました。
https://teratail.com/questions/187906