Spring Cloud Gateway / KeyCloakを試してみた

以下の記事の内容を試してみました。

https://piotrminkowski.com/2020/10/09/spring-cloud-gateway-oauth2-with-keycloak/

# 試す内容
以下のフローの⑦だけスキップしてそれ以外を実装しました。
言語はkotlinで実装しております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/8931c3e8-5883-4871-0fe6-17deb09a78e6.png)

# Keycloakの設定
## DockerでKeycloakを起動
以下のコマンドで自動でDockerイメージがダウンロードされる

```bash
docker run -d --name keycloak -p 8888:8080 \
   -e KEYCLOAK_USER=spring \
   -e KEYCLOAK_PASSWORD=spring123 \
   jboss/keycloak
```
## Clientの作成
以下のURLにアクセスする
http://localhost:8888/auth/admin/
![Keycloak_Admin_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/f458f851-631c-fce5-db11-bc7928dae643.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/145bfdbd-c0c8-5dc2-6619-2efe4239d89a.png)

## Client Secretを取得
![Keycloak_Admin_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/6ca5d008-85b5-4fbd-d9fa-8e647e39d886.png)

keycloak-without-test-scopeも同様に作成する。

# Spring Bootのダウンロード
Spring InitializrからSpring Bootをダウンロードする
https://start.spring.io/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/ba71caf2-b9a4-dcb1-e237-b35513aa09ca.png)

# build.gradle.ktsの設定
```kotlin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id("org.springframework.boot") version "2.5.2"
	id("io.spring.dependency-management") version "1.0.11.RELEASE"
	kotlin("jvm") version "1.5.20"
	kotlin("plugin.spring") version "1.5.20"
}

group = "phoneappli.people"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {
	mavenCentral()
}

dependencyManagement {
	imports {
		mavenBom("org.springframework.cloud:spring-cloud-dependencies:2020.0.3")
	}
}

dependencies {
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	implementation("org.springframework.cloud:spring-cloud-starter-gateway")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.springframework.boot:spring-boot-starter-security")
	implementation("org.springframework.security:spring-security-test")
	implementation("org.springframework.security:spring-security-oauth2-resource-server")
	implementation("org.springframework.security:spring-security-oauth2-jose")
	implementation("org.springframework.security:spring-security-oauth2-client")

	testImplementation("org.springframework.boot:spring-boot-starter-test")

}

tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs = listOf("-Xjsr305=strict")
		jvmTarget = "1.8"
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}
```

# application.ymlの設定
```yaml
server.port: 8090

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            token-uri: http://127.0.0.1:8888/auth/realms/master/protocol/openid-connect/token
            authorization-uri: http://127.0.0.1:8888/auth/realms/master/protocol/openid-connect/auth
            userinfo-uri: http://127.0.0.1:8888/auth/realms/master/protocol/openid-connect/userinfo
            user-name-attribute: preferred_username
        registration:
          keycloak-with-test-scope:
            provider: keycloak
            client-id: spring-with-test-scope
            client-secret: f7fab54d-b766-4fc3-b3a2-fbcaf826e816
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
  application:
    name: callme
    security:
      oauth2:
        resourceserver:
          jwt:
            issuer-uri: http://127.0.0.1:8888/auth/realms/master
  cloud:
    gateway:
      default-filters: TokenRelay
      routes:
        - id: callme-service
          uri: http://127.0.0.1:5000 #callmeサービスのURL
          predicates:
            - Path=/callme/**
          filters:
            - RemoveRequestHeader=Cookie #callmeサービスへ転送する時にCookieを削除
  main:
    web-application-type: reactive
    allow-bean-definition-overriding: true

logging:
  level:
    org.springframework.security: DEBUG
```

# Spring Securityの設定
```kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig: WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.authorizeHttpRequests {
            authorize -> authorize.anyRequest().authenticated()
        }.oauth2ResourceServer().jwt()
    }
}
```

# 動作確認
callmeサービスはPython Flaskで文字列を返すだけのサービスを実装。

ブラウザで以下のURLにアクセス。
http://127.0.0.1:8090/callme/ping
以下の画面に自動でリダイレクトする。
![Please_sign_in.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/6ef79a10-5978-e207-0a11-5bc7265f0084.png)

「keycloak-with-test-scope」「keycloak-without-test-scope」のどちらかを選択するとログイン画面が表示されるので、ログインする。
認証URLはこのようになっている。(http://127.0.0.1:8888/auth/realms/master/protocol/openid-connect/auth?response_type=code&client_id=spring-with-test-scope&state=AnB-Pc1Mi6Uv3caMM59g5Sfs6ASz7wu4oMPN6aXpsvM%3D&redirect_uri=http://127.0.0.1:8090/login/oauth2/code/keycloak)
![Sign_in_to_Keycloak.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/2e97ef13-7bba-8b28-3f34-07d6ac8b52d3.png)

認証に成功するとcallmeサービスが返した内容が表示される。
http://127.0.0.1:8090/callme/ping
![127_0_0_1_8090_callme_ping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/6138c6e1-7725-95fe-cff4-8b7f379ac6fd.png)

callmeサービスへ転送されたリクエストではcookieは削除されており、また以下の内容がheaderに付与されています。

```
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJaSlZqWmlHUzgybmVETVlpVUZaRW1QQjBJM0ItOXJSTmhzQWJMWnhnbmtjIn0.eyJleHAiOjE2MjU3MjQ4NDgsImlhdCI6MTYyNTcyNDc4OCwiYXV0aF90aW1lIjoxNjI1NzIzODUxLCJqdGkiOiI5MTU3YWE4Zi0xZGQ1LTQxYTMtYmJkYi0wMTIxNTgwNmZlM2YiLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjg4ODgvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiY2M1MWIxNTQtMGQ2Mi00YjRhLWI5YWYtMWZkN2FjMmFmNDdhIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5nLXdpdGgtdGVzdC1zY29wZSIsInNlc3Npb25fc3RhdGUiOiJjMGQxMWNmMi0wODllLTQzZWItYmYzMi1kNDVkYzM1N2EzNzQiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwiU0NPUEVfVEVTVCIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im1hc3Rlci1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBURVNUIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNwcmluZyJ9.CRKt9rGPsv-et1bWlLcVdgj46PSGRKSMmZmxzBusHxORjJK2OeyzSYRqFNlGWW_hZDtTp3zB4DU3VYnFSDo_zxAaBJnjA4efr3SnKhUKjdSa6Oiv95PVkU_xzStKKOeqC7VUB8WZpXWgh4GfcC7VFMnUhWJyjCUsE3kfUZA3z7WxuAhrU_9nz3elDIgtQt6Rj9CePk_a4HDfMrjhWLqYi1qjlnyWsMatHav44Iz0ygTpOQyd4k0awd-f7FEp916zfAxLOKFIo7tqsS8DEf46II_GBThTkTtmCNlOJoBHHTqL9VHaN1kWHUbm6BxTfoaqooLaIxTc7BIbkJPVp8Hhag
```
こちらはjwtになっており分解するとroleなどの情報が入っております。