LoginSignup
8
9

More than 3 years have passed since last update.

KeycloakとSpringBoot/SecurityでOpenID Connect(クライアント編)

Last updated at Posted at 2020-04-28

前回の続きです。

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の場合)などで調べるといいかもしれませんね。

application.properties
server.port=9000

これで実行すれば、localhost:9000でアクセスできます。

2.ページを作成

htmlを表示するのに、なんかおすすめらしいThymeleaf(タイムリーフ)を使ってみます

pom.xml
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>

(1)indexページ

indexページはnon-secureでいいでしょう。このページは固定なのでThymeleafは使っていません。

resource/templates/index.html
<!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/ にアクセスすると、以下のようなページが開きます。

index.png

(2)productsページ

後でセキュアなページにする予定のページを作ります。

ProductController.java
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";
    }
}
ProductService.java
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");
    }
}
resource/templates/products.html
<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>

実行するとこうなります。

products.png

3.Keycloak関係の設定を追加

(1)依存関係の追加

Keycloakと、SpringSecurityを追加します。

pom.xml
    <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)セキュリティ設定クラスを追加

SecurityConfig.java
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

application.properties
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_noclient.png

Keycloakにクライアントを追加する

前回やったようにクライアントを追加します。

create_client1.png
  • AccessType
    • publicにします
  • Valid Redirect URIs
    • http://localhost:9000*とします

上記以外はデフォルトでいいかと思います。
ポート番号は自分で設定したのと合わせてください。

detail_client1.png

リダイレクトURLに、localhostパターンとローカルIP指定パターンの両方登録しておきました。

[Save]を忘れずに。

これで接続してみます。
(127.0.0.1:9000ではなく、localhost:9000でアクセスしてください。リダイレクトがうまくかかりません)
クライアントアプリを再起動して/productsページにアクセス!

keycloak_login.png

ログイン画面が出ました!
作成済みの任意のユーザー名とパスワードでログインしてみましょう!

403.png

あ・・・あれ、Forbidden(403)・・・

当たり前です。ユーザーにロールを設定していませんでした(テヘペロ)

ユーザーにロールを割り当てる

1.ロールを追加

Keycloakの管理画面で、Rolesから[Add Role]をクリックします。

add_role.png

Role Nameuserと入力し、[Save]。

user_role.png

2.ユーザーにロールを割り当てる

  • Users-[View all users]で、任意のユーザーを選ぶ
  • [Role Mappings]タブをクリック
  • Available Rolesにあるuserを選択して、[Add selefcted]とする
user_role_mapping.png

アクセス!

products.png

きました!

ユーザー情報を取得する

といってもユーザー情報APIを叩くのではなくて、アクセストークンを解析して取ってみようと思います。

1.ユーザー情報閲覧ページを追加

ProductController.java
    @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:/";
    }

ついでにログアウトボタンも付けましょう。

userinfo.html
<!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ページにユーザー情報ページへのリンクを追加

index.html
<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ページ

link_userinfo.png

ユーザー情報ページ

userinfo.png

体裁は、まあ^^;

scopeですが、KeycloakのClient設定で、[Client Scopes]の[Default Client scopes]から、helloをセットしておいたので、scopesにいますね。

ログアウトをクリックすると、indexに戻ります。で、またログインから実行できます。

4.このアクセストークンをリソースサーバーに投げてみる

これで、前回作成したリソースサーバーと同じレルムで動くクライアントが出来ました。
SSOでリソースサーバーにもアクセスできるはずです。
とりあえず手動で投げます。CurlとかPostmanで。

以下はPostmanの例です。

hello.png

scopehelloがいるので、/helloはアクセスできます。
/listはアクセスできません。
[Default scopes]にuserも追加すれば、見られるようになります。

感想

次は、このクライアントから、リソースサーバーのAPIを叩いて表示出来るようにしたいですね。いわゆるSSO(シングルサインオン)の実現です。
トークンのやり取りどうなるかしら?

それと、サーバー(tomcat)のアクセスログが全然見えなくて困ったので、以下のような設定を追加しました。

applicatin.properties
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

8
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9