Keycloak・リソースサーバー・リソースクライアントの連携例

概要

Keycloak による SSO 基盤構築検証のメモ。 SSO サーバー (Keycloak) のセットアップと、SSO クライアント (リソースサーバー、リソースクライアント) の開発を順に見ていきます。

この手順で使用したコードは、以下に公開しているので、こちらも参考にしてください。
https://github.com/yo1000/kc-resource

要件

環境

今回の作業環境は以下のとおりです。

  • Java 1.8.0_131
  • Spring Boot 1.5.9.RELEASE
  • Keycloak 3.4.1.Final
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.12.5
BuildVersion:	16F2073

$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
1
2
3
4
5
6
7
8
9

認証認可フロー

構築しようとしている認証認可フローの概要図は、以下のとおりです。

img/2017-12-18_kc-resource-flow.svg

各アクターの役割は、以下のとおりです。

  • User
    • Browser を操作して、リソースを表示するサイトを要求します
    • 認証を必要とした場合に、ログインします
  • Keycloak
    • SSO 基盤です
    • ユーザーを認証認可します
    • クライアント (認証認可情報を要求してきたアプリケーション) を認証認可します
  • Resource Server
    • Resource Store からリソースを取得して、クライアントへ提供します
      • 今回のサンプルでは、Resource Store からのリソース取得については実装しません
      • 今回のサンプルでは、ロールに応じた静的な値をリソースとして返却させます
  • Resource Client
    • Resource Server からリソースを取得して、画面に結果を表示します

備考

以降、一連の流れを実施するにあたり、ディレクトリ移動が数回発生するため、 便宜上、${BASE_DIR} をディレクトリの基点として使用します。

$ BASE_DIR=`pwd`
1

Keycloak のセットアップ (SSO Server)

ダウンロード・展開

執筆時点での最新は、3.4.1.Final
Download URL: https://downloads.jboss.org/keycloak/3.4.1.Final/keycloak-3.4.1.Final.tar.gz

$ cd ${BASE_DIR}
$ curl https://downloads.jboss.org/keycloak/3.4.1.Final/keycloak-3.4.1.Final.tar.gz \
  | tar -zxvf -
$ cd keycloak-3.4.1.Final
1
2
3
4

Keycloak の初期設定・起動

管理ユーザーは、以下いずれかの方法で追加します。

  • ホスト内の add-user-keycloak.sh による登録 (今回はこちらを使用)
  • 同一ホストからのアクセスによる Admin Console での登録

今回はすべてローカルホストに構築しているため、気にする必要はありませんが、 セットアップ後、初めて作成する管理ユーザーは、 リモートホストから、直接追加することができない ため、 セットアップ時に一緒に作成しておく必要があります。

$ # Add admin user for WildFly Management Console
$ bin/add-user.sh \
  -u wildfly \
  -p wildfly1234
Added user 'wildfly' to file '/workDir/keycloak-3.4.1.Final/standalone/configuration/mgmt-users.properties'
Added user 'wildfly' to file '/workDir/keycloak-3.4.1.Final/domain/configuration/mgmt-users.properties'

$ # Add admin user for Keycloak Admin Console
$ bin/add-user-keycloak.sh \
  -r master \
  -u keycloak \
  -p keycloak1234
Added 'admin' to '/workDir/keycloak-3.4.1.Final/standalone/configuration/keycloak-add-user.json', restart server to load user

$ # Run Keycloak
$ bin/standalone.sh \
  -b 0.0.0.0 &
..
18:38:14,161 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 3.4.1.Final (WildFly Core 3.0.8.Final) started in 61427ms - Started 545 of 881 services (604 services are lazy, passive or on-demand)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Keycloak ヘログイン

以降、kcadm.sh を使用する上で、ログイン状態が必要になるため、ログインします。 kcadm.sh 実行時に、以下のようなメッセージが出力された場合は、 ログインセッションが期限切れとなっているため、改めてログインします。

Session has expired. Login again with 'kcadm.sh config credentials'

$ # Login to Keycloak
$ bin/kcadm.sh config credentials \
  --server http://127.0.0.1:8080/auth \
  --realm master \
  --user keycloak \
  --password keycloak1234
Logging into http://127.0.0.1:8080/auth as user admin of realm master
1
2
3
4
5
6
7

Realm の作成

レルムは 領域・範囲・部門 といった単語に略されるもので、 そのログインが、どのような種類のリソースにアクセスするためのものかを区別、管理するための単位です。 より端的には、レルムが分離されていると、ログイン画面の URL が分離されます。

使用例としては、顧客が使用するシステムと、システム管理者が使用するシステムで、 同じ SSO 基盤 (Keycloak) を使用しながらも、 ログインフォームや、ログイン後に使用されるロールや、グループの管理を、 完全に分離しておきたいような場合に活用できます。

$ # Create realm
$ bin/kcadm.sh create realms \
  -s realm=kc-resource \
  -s enabled=true
Created new realm with id 'kc-resource'

$ # Create realm roles
$ bin/kcadm.sh create roles \
  -r kc-resource \
  -s name=admin
Created new role with id 'admin'
$ bin/kcadm.sh create roles \
  -r kc-resource \
  -s name=user
Created new role with id 'user'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

ユーザーの登録

kc-resource レルムにユーザーを追加していきます。 管理ユーザーとして alice を、一般ユーザーとして bob を、それぞれ作成します。

$ # Create realm users
$ bin/kcadm.sh create users \
  -r kc-resource \
  -s username=alice \
  -s enabled=true
Created new user with id '26c64aeb-6d09-4d58-afac-fd7550d4ff7b'
$ bin/kcadm.sh create users \
  -r kc-resource \
  -s username=bob \
  -s enabled=true
Created new user with id '8ab8768a-a2b1-479d-be11-3d7dd1b3d3db'

$ # Update password
$ bin/kcadm.sh set-password \
  -r kc-resource \
  --username alice \
  -p alice1234
$ bin/kcadm.sh set-password \
  -r kc-resource \
  --username bob \
  -p bob1234

$ # Add realm roles to users
$ bin/kcadm.sh add-roles \
  -r kc-resource \
  --uusername alice \
  --rolename admin \
  --rolename user
$ bin/kcadm.sh add-roles \
  -r kc-resource \
  --uusername bob \
  --rolename user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

SSO クライアントの登録

SSO 基盤を使用するアプリケーション (SSO サーバーに対する、クライアント) を登録します。 リソースサーバーとして kc-resource-server を、 リソースクライアントとして kc-resource-client をそれぞれ作成します。

リソースサーバー、リソースクライアントという、 リソースに対して、サーバー・クライアントの関係にある2つのアプリケーション を作成しますが、 これらアプリケーションは、 いずれも SSO サーバーから見れば、等しく SSO クライアント となります。

$ # Add realm client for Resource server
$ RES_SRV_ID=`bin/kcadm.sh create clients -r kc-resource -s clientId=kc-resource-server -s bearerOnly=true -i`; \
  echo $RES_SRV_ID
58f8a1ad-a409-4f22-9bb8-de10f9ca5365

$ # Add realm client for Resource client
$ RES_CLI_ID=`bin/kcadm.sh create clients -r kc-resource -s clientId=kc-resource-client -s 'redirectUris=["http://localhost:28080/*"]' -i`; \
  echo $RES_CLI_ID
373d1ce7-19c2-4a40-b1a3-deb3e4a02c83
1
2
3
4
5
6
7
8
9

リソースサーバーの実装 (SSO Client - A)

プロジェクトの作成

Spring Initializr で、リソースサーバー用のプロジェクトを作成します。

$ cd ${BASE_DIR}
$ curl https://start.spring.io/starter.tgz \
  -d dependencies="web,security,keycloak" \
  -d language="kotlin" \
  -d javaVersion="1.8" \
  -d packaging="jar" \
  -d bootVersion="1.5.9.RELEASE" \
  -d type="maven-project" \
  -d groupId="com.yo1000" \
  -d artifactId="kc-resource-server" \
  -d version="1.0.0-SNAPSHOT" \
  -d name="kc-resource-server" \
  -d description="Keycloak Client Demo - Resource Server" \
  -d packageName="com.yo1000.keycloak.resource.server" \
  -d baseDir="kc-resource-server" \
  -d applicationName="KcResourceServerApplication" \
  | tar -xzvf -

$ ls kc-resource-server
mvnw		mvnw.cmd	pom.xml		src

$ cd kc-resource-server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

設定ファイルの配置

リソースサーバー用の、構成ファイルをセットアップします。

$ sed -i '' \
  's/<keycloak.version>3.4.0.Final<\/keycloak.version>/<keycloak.version>3.4.1.Final<\/keycloak.version>/g' \
  pom.xml

$ mv \
  src/main/resources/application.properties \
  src/main/resources/application.yml

$ echo 'server.port: 18080

keycloak:
  realm: kc-resource
  resource: kc-resource-server
  bearer-only: true
  auth-server-url: http://127.0.0.1:8080/auth
  ssl-required: external
' > src/main/resources/application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

セキュリティ構成の実装

コード例の後に、要点をまとめます。

$ echo 'package com.yo1000.keycloak.resource.server

import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
import org.keycloak.adapters.KeycloakConfigResolver

@Configuration
@EnableWebSecurity
class KcSecurityConfigurer: KeycloakWebSecurityConfigurerAdapter() {
    @Bean
    fun grantedAuthoritiesMapper(): GrantedAuthoritiesMapper {
        val mapper = SimpleAuthorityMapper()
        mapper.setConvertToUpperCase(true)
        return mapper
    }

    @Bean
    fun keycloakConfigResolver(): KeycloakConfigResolver {
        return KeycloakSpringBootConfigResolver()
    }

    @Bean
    fun keycloakAuthenticationProcessingFilterRegistrationBean(
            filter: KeycloakAuthenticationProcessingFilter): FilterRegistrationBean {
        val registrationBean = FilterRegistrationBean(filter)
        registrationBean.isEnabled = false
        return registrationBean
    }

    @Bean
    fun keycloakPreAuthActionsFilterRegistrationBean(
            filter: KeycloakPreAuthActionsFilter): FilterRegistrationBean {
        val registrationBean = FilterRegistrationBean(filter)
        registrationBean.isEnabled = false
        return registrationBean
    }

    override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
        return NullAuthenticatedSessionStrategy()
    }

    override fun keycloakAuthenticationProvider(): KeycloakAuthenticationProvider {
        val provider = super.keycloakAuthenticationProvider()
        provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper())
        return provider
    }

    override fun configure(auth: AuthenticationManagerBuilder?) {
        auth!!.authenticationProvider(keycloakAuthenticationProvider())
    }

    override fun configure(http: HttpSecurity) {
        super.configure(http)
        http
                .authorizeRequests()
                .antMatchers("/kc/resource/server/admin").hasRole("ADMIN")
                .antMatchers("/kc/resource/server/user").hasRole("USER")
                .anyRequest().permitAll()
    }
}
' > src/main/kotlin/com/yo1000/keycloak/resource/server/KcSecurityConfigurer.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

configure(http: HttpSecurity)

認証で保護したい URL のパターンと、許可するロールの組み合わせを正しく設定します。 この設定に誤りがあると、SSO 基盤へのリダイレクトに失敗します。

grantedAuthoritiesMapper(): GrantedAuthoritiesMapper

認証基盤でロール名を小文字や、大文字小文字混在で設定しても、 mapper.setConvertToUpperCase(true) を設定することで、 プログラムから扱う場合に、すべて大文字で統一することができます。

コントローラーの実装

リソースを返却するエンドポイントとなる、API 用コントローラーを実装します。

$ echo 'package com.yo1000.keycloak.resource.server

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/kc/resource/server")
class KcResourceServerController {
    @GetMapping("/admin")
    fun getAdminResource(): String {
        return "ADMIN Resource!!"
    }

    @GetMapping("/user")
    fun getUserResource(): String {
        return "USER Resource."
    }
}
' > src/main/kotlin/com/yo1000/keycloak/resource/server/KcResourceServerController.kt 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ビルド・起動

リソースサーバー用アプリケーションを起動します。

$ ./mvnw clean spring-boot:run &
1

リソースクライアントの実装 (SSO Client - B)

プロジェクトの作成

Spring Initializr で、リソースクライアント用のプロジェクトを作成します。

$ cd ${BASE_DIR}
$ curl https://start.spring.io/starter.tgz \
  -d dependencies="web,security,keycloak" \
  -d language="kotlin" \
  -d javaVersion="1.8" \
  -d packaging="jar" \
  -d bootVersion="1.5.9.RELEASE" \
  -d type="maven-project" \
  -d groupId="com.yo1000" \
  -d artifactId="kc-resource-client" \
  -d version="1.0.0-SNAPSHOT" \
  -d name="kc-resource-client" \
  -d description="Keycloak Client Demo - Resource Client" \
  -d packageName="com.yo1000.keycloak.resource.client" \
  -d baseDir="kc-resource-client" \
  -d applicationName="KcResourceClientApplication" \
  | tar -xzvf -

$ ls kc-resource-client
mvnw		mvnw.cmd	pom.xml		src

$ cd kc-resource-client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

設定ファイルの配置

リソースクライアント用の、構成ファイルをセットアップします。

こちらでは、リソースサーバーでは設定しなかった keycloak.json が必要になります。 Set up Clients$RES_CLI_ID 変数に取っておいた、 クライアント ID を使用して、keycloak.json を出力します。

$ # Update Keycloak dependency version
$ sed -i '' \
  's/<keycloak.version>3.4.0.Final<\/keycloak.version>/<keycloak.version>3.4.1.Final<\/keycloak.version>/g' \
  pom.xml

$ # Configure application.yml
$ mv \
  src/main/resources/application.properties \
  src/main/resources/application.yml
$ echo 'server.port: 28080

keycloak:
  realm: kc-resource
  resource: kc-resource-client
  auth-server-url: http://127.0.0.1:8080/auth
' > src/main/resources/application.yml

$ # Install credentials
$ ${BASE_DIR}/keycloak-3.4.1.Final/bin/kcadm.sh \
  get clients/${RES_CLI_ID}/installation/providers/keycloak-oidc-keycloak-json \
  -r kc-resource \
  > src/main/resources/keycloak.json 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

セキュリティ構成の実装

コード例の後に、要点をまとめます。

Resource Server 用の実装で触れたものと概ね同様ですが、 adapterDeploymentContext() の説明を追加しています。

$ echo 'package com.yo1000.keycloak.resource.client

import org.keycloak.adapters.AdapterDeploymentContext
import org.keycloak.adapters.springsecurity.AdapterDeploymentContextFactoryBean
import org.keycloak.adapters.springsecurity.KeycloakConfiguration
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory
import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Scope
import org.springframework.core.io.ClassPathResource
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.GrantedAuthoritiesMapper
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
class SecurityConfiguration : KeycloakWebSecurityConfigurerAdapter() {
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    fun keycloakRestTemplate(keycloakClientRequestFactory: KeycloakClientRequestFactory): KeycloakRestTemplate {
        return KeycloakRestTemplate(keycloakClientRequestFactory)
    }

    @Bean
    override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
        return RegisterSessionAuthenticationStrategy(SessionRegistryImpl())
    }

    @Bean
    override fun adapterDeploymentContext(): AdapterDeploymentContext {
        val factoryBean = AdapterDeploymentContextFactoryBean(ClassPathResource("keycloak.json"))
        factoryBean.afterPropertiesSet()
        return factoryBean.`object`
    }

    @Bean
    fun keycloakAuthenticationProcessingFilterRegistrationBean(
            filter: KeycloakAuthenticationProcessingFilter): FilterRegistrationBean {
        val registrationBean = FilterRegistrationBean(filter)
        registrationBean.isEnabled = false
        return registrationBean
    }

    @Bean
    @ConditionalOnClass(SpringBootApplication::class)
    fun keycloakPreAuthActionsFilterRegistrationBean(
            filter: KeycloakPreAuthActionsFilter): FilterRegistrationBean {
        val registrationBean = FilterRegistrationBean(filter)
        registrationBean.isEnabled = false
        return registrationBean
    }

    @Bean
    fun grantedAuthoritiesMapper(): GrantedAuthoritiesMapper {
        val mapper = SimpleAuthorityMapper()
        mapper.setConvertToUpperCase(true)
        return mapper
    }

    override fun keycloakAuthenticationProvider(): KeycloakAuthenticationProvider {
        val provider = super.keycloakAuthenticationProvider()
        provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper())
        return provider
    }

    override fun configure(auth: AuthenticationManagerBuilder?) {
        auth!!.authenticationProvider(keycloakAuthenticationProvider())
    }

    override fun configure(http: HttpSecurity) {
        super.configure(http)
        http
                .authorizeRequests()
                .antMatchers("/kc/resource/client/user").hasRole("USER")
                .antMatchers("/kc/resource/client/admin").hasRole("ADMIN")
                .anyRequest().permitAll()
    }
}
' > src/main/kotlin/com/yo1000/keycloak/resource/client/SecurityConfiguration.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

configure(http: HttpSecurity)

認証で保護したい URL のパターンと、許可するロールの組み合わせを正しく設定します。 この設定に誤りがあると、SSO 基盤へのリダイレクトに失敗します。

grantedAuthoritiesMapper(): GrantedAuthoritiesMapper

認証基盤でロール名を小文字や、大文字小文字混在で設定しても、 mapper.setConvertToUpperCase(true) を設定することで、 プログラムから扱う場合に、すべて大文字で統一することができます。

adapterDeploymentContext(): AdapterDeploymentContext

アプリケーションが読み込む、keycloak.json の位置を変更します。 デフォルトでは、WEB-INF/keycloak.json となっています。 Spring Boot で、実行可能 JAR を作成する場合、 WEB-INF にファイルを配置するのは一般的ではないため、 resources 直下に配置した、keycloak.json を読み込ませるようにします。

KeycloakRestTemplate を使用するコントローラーの実装

リソースサーバーにリソースを要求して、 結果を画面に表示するエンドポイントとなる、コントローラーを実装します。

$ echo 'package com.yo1000.keycloak.resource.client

import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseBody

@Controller
@RequestMapping("/kc/resource/client")
class KcResourceClientController(
        val template: KeycloakRestTemplate
) {
    companion object {
        const val ENDPOINT = "http://localhost:18080/kc/resource/server/{role}"
    }

    @GetMapping("/admin")
    @ResponseBody
    fun getAdmin(): String {
        val resp = template.getForObject(ENDPOINT, String::class.java, mapOf("role" to "admin"))
        return """
            This page is Resource Client side.
            Resource Server Response: [$resp]
            """.trimIndent()
    }

    @GetMapping("/user")
    @ResponseBody
    fun getUser(): String {
        val resp = template.getForObject(ENDPOINT, String::class.java, mapOf("role" to "user"))
        return """
            This page is Resource Client side.
            Resource Server Response: [$resp]
            """.trimIndent()
    }
}
' > src/main/kotlin/com/yo1000/keycloak/resource/client/KcResourceClientController.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

ビルド・起動

リソースクライアント用アプリケーションを起動します。

$ ./mvnw clean spring-boot:run &
1

デモ

参考までに、実際に動かした結果を、以下キャプチャに残しておきます。

以下 URL にアクセスしてみます。
http://localhost:28080/kc/resource/client/admin

Keycloak へリダイレクトされ、ログインを要求されます。
img/2017-12-18_kc-resource-demo-1.png

ログインすると、ロールに応じたメッセージが表示されます。
img/2017-12-18_kc-resource-demo-2.png

参考

ドキュメント

コード例