Spring Security OAuth2を使ってKeycloakとつないでみる（Spring Framework 3編）

# はじめに

## 今回やること

Spring Framework 3という古い環境で、Spring Security 3とSpring Security OAuth 1.0.5を利用したKeycloakとの連携方法について簡単に説明します。
「5分でわかる」をタイトルにつけようとしましたが、さすがに5分では無理でしたのであきらめました。。。
Spring Framework 5やSpring Bootを利用したSpring Security OAuth2とKeycloakの接続はここでは言及しませんのでご了承ください。

## Spring Boot－Keycloakの紹介

Keycloakでは、Spring Boot用のKeycloakアダプターが開発されています。
そのため、簡単に連携することが可能です。
[こちらの記事](https://qiita.com/tamura__246/items/4290a1035e1adcab733b)で紹介されていますので、Spring BootでKeycloakと連携したい方はご参照ください。

## 背景
なぜ古いバージョンのSpring FrameworkでKeycloakとの連携について記述するかというと、既存のJava7で稼動しているSpring Frameworkで作成されたWebアプリにSpring Security OAuth2による認可を追加したいという要望があがり、調査する必要があったためです。

# 連携確認
この記事では、認可コードフローを利用してKeycloakのユーザー情報を取得するSpring Frameworkクライアントアプリケーションを作成し、Keycloakとの連携を確認します。

## 環境

今回検証を行う環境は以下の通りです。
![os-middle.png](https://qiita-image-store.s3.amazonaws.com/0/208407/72e83768-086b-1ef5-3ea5-0fa3ed0b2724.png)

### インストールバージョン

|FQDN|役割|構成|
|:---|:---|:---|
|webapp.example.com|SpringWebアプリケーションサーバー|CentOS 7.4<br>OpenJDK 1.7.0_181<br>Apache Tomcat 7.0.57|
|keycloak.example.com|Keycloakサーバー|CentOS 7.4<br>OpenJDK 1.8.0_191<br>Apache Httpd 2.4.6<br>Keycloak 4.5.0|


#### その他設定
**Tomcat-SpringWebアプリケーションサーバー**
TomcatのConnecter Portを`80`に修正しています。

``` tomcat-7.0.57/conf/server.xml
    <Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
```

**Keycloakサーバー**
Httpdへmod_ssl, mod_proxy_httpを導入し、Keycloakと接続しています。
`UserAgent → https → httpd(mod_ssl,mod_proxy_http) → http → Keycloak`

SSL用の自己署名証明書の作成については、[私の過去の記事](https://qiita.com/katakura__pro/items/07c02f691e673eb50cee#ssl%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88)にも記載しています。

#### 開発環境
- Windows7 Professional SP1
- Eclipse Oxygen 
- Maven EclipseEmbedded 3.3.9


## Keycloakの設定

1. demoレルムを作成します
![keycloak-realm1.png](https://qiita-image-store.s3.amazonaws.com/0/208407/b5682204-1c3a-50a1-3a40-7f23948aaba5.png)

1. spring-security-oauth2クライアントをopenid-connectプロトコルで作成しておきます。`Authorization Enabled`を「ON」にします。自動的に`ServiceAccount Enabled`が「ON」に、`Access Type`が「confidential」になります。
![keycloak-client1.png](https://qiita-image-store.s3.amazonaws.com/0/208407/97285b2b-1a9e-c276-6cd2-f3bffc8f1a97.png)

1. レルムのユーザーを作成しておきます。
  * Username : test001
  * Password : password

1.  `ServiceAccount Enabled`が「ON」の状態で、Springアプリケーション(クライアント)がClient IDとClient Secretを利用してKeycloakへ接続できるようになります。`Credentials`タブを参照して`secret`を確認します。
![keycloak-client2.png](https://qiita-image-store.s3.amazonaws.com/0/208407/dbfd3967-141a-385b-ef77-b6aa2f1f612a.png)


## SpringWebアプリの作成
### Eclipseプロジェクトの作成
1. Eclipseで動的Webプロジェクトを作成  
  `[ファイル]-[新規]-[プロジェクト]`メニューから、`web／動的Webプロジェクト`を選択し、次へ。  
  プロジェクト名を入力して、次へをクリックします。(ここでは`sampleweb`としました。)  
  再度次へをクリックし、最後に`web.xmlデプロイメント記述子の生成`にチェックをつけ、完了ボタンをクリックします。
1. プロジェクトをmavenプロジェクトへ変更  
  作成したプロジェクトを右クリックし、`[構成]-[Mavenプロジェクトへ変換]`をクリックします。  
  グループIDやプロジェクトIDなどプロジェクト情報を入力（デフォルトでOK）、完了ボタンをクリックします。
1. 依存関係の記述(pom.xml)  
  プロジェクトディレクトリ直下にpom.xmlファイルが作成されているので、`dependencies`ディレクティブ内に以下を追記します。  
  また、ビルドされるwarファイルの名前を`build`タグ中の`finalName`に記述します。

  ``` xml:pom.xml
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>3.1.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security.oauth</groupId>
      <artifactId>spring-security-oauth2</artifactId>
      <version>1.0.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>sampleweb</finalName>
  </build>
  ```

### アプリの設定
#### web.xml 定義
  Webアプリ用のサーブレットの定義とWebアプリを利用するためのSpring Securityフィルターの定義を行います。
  Spring Securityを有効にするには"`springSecurityFilterChain`"という`filter-name`でフィルタークラス`org.springframework.web.filter.DelegatingFilterProxy`を定義します。
Spring Securityでは、"`springSecurityFilterChain`"というBean名でSpring SecurityのFilterChainProxyが登録されるため一致する必要があります。

``` WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	version="3.0">
	
	<display-name>Client Application</display-name>  
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<servlet>
		<servlet-name>dispatcherServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
				/WEB-INF/applicationContext.xml
			</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>dispatcherServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	<filter>
		<filter-name>springSecurityFilterChain</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>springSecurityFilterChain</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
</web-app>
```

#### applicationContext.xml
クライアントアプリケーションの定義を行います。
xmlスキーマでネームスペースが`oauth2`の定義が認可に関する定義です。
xmlスキーマでネームスペースが`sec`の定義がSpring Securityに関する定義です。


``` WEB-INF/applicationContext.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:sec="http://www.springframework.org/schema/security"
	xmlns:oauth="http://www.springframework.org/schema/security/oauth2" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-1.0.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

	<!-- アノテーションによる定義を有効にする -->
	<mvc:annotation-driven />
	<!-- OAuth2クライアントとしてのコンテキストフィルター定義 -->
	<oauth:client id="oauth2ClientContextFilter" />
	<!--リソース認可の設定 -->
	<oauth:resource id="sampleResource" type="authorization_code" client-id="spring-security-oauth2" client-secret="67c796c4-5605-4970-831a-d9b56be21cb9"
		access-token-uri="${accessTokenUri}" user-authorization-uri="${userAuthorizationUri}" />
	<!-- Spring Security HTTPセキュリティ設定 -->
	<sec:http>
		<sec:intercept-url pattern="/userinfo**" access="ROLE_USER" />
		<sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" />

		<sec:form-login default-target-url="/index" />
		<sec:logout logout-success-url="/" logout-url="/logout.do"  invalidate-session="true" />
		<!-- OAuth2クライアントコンテキストフィルターを追加します -->
		<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER" />
	</sec:http>
	<!-- Spring Security認証情報 -->
	<sec:authentication-manager>
		<sec:authentication-provider>
			<sec:user-service>
				<sec:user name="user" password="user" authorities="ROLE_USER" />
			</sec:user-service>
		</sec:authentication-provider>
	</sec:authentication-manager>
	<!-- パッケージ名を定義。ここではexampleとした -->
	<context:component-scan base-package="example" />
	<!-- プロパティファイルの読み込み -->
	<context:property-placeholder location="classpath:/oauth2.properties" />
	<mvc:default-servlet-handler />
	<bean id="sampleController" class="example.SampleController">
		<property name="restTemplate">
			<oauth:rest-template resource="sampleResource" />
		</property>
	</bean>
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/jsp/" />
	</bean>
</beans>
```

**oauthネームスペースタグについて**

| タグ | 説明 |
| :----- | :----- |
| oauth:client | OAuth2クライアントの定義です。 |
| oauth:resource | OAuth2リソースの定義です。どのような認可が必要なリソースであるかを定義します。 |
| oauth:rest-template | 認可リクエストやレスポンスを取り扱うRestTemplateの定義です。 |

**secネームスペースタグについて**

| タグ | 説明 |
| :----- | :----- |
| sec:http | URLによるセキュリティ定義を行います。 |
| sec:intercept-url | 正規表現によるURLパスの動作を定義します。 |
| sec:form-login | Spring Securityのログインフォームの定義を行います。ここではSpring Securityのデフォルトログイン画面を使用します。 |
| sec:logout | ログアウトのパスやログアウト後遷移先などの定義を行います。 |
| sec:authentication-manager | Spring Securityの認証について定義します。 |
| sec:authentication-provider | Spring Securityの認証方法を定義します。ここでは簡易なユーザー認証を定義します。 |
| sec:user-service | ユーザーによる認証を定義します。 |
| sec:user | ユーザー情報の定義を行います。 |
 

### 実装
#### リソースファイル
src/main/resourcesにOAuth2の認可サーバーのエンドポイントなどの情報を定義したpropertiesファイルを作成します。
`client.id`は、Keycloakで作成したClientID、`client.secret`は、KeycloakのClient設定のCredentialsタブの`secret`の値を記述します。

``` oauth2.properties
accessTokenUri=https://keycloak.example.com/auth/realms/demo/protocol/openid-connect/token
userAuthorizationUri=https://keycloak.example.com/auth/realms/demo/protocol/openid-connect/auth
userInfoUri=https://keycloak.example.com/auth/realms/demo/protocol/openid-connect/userinfo
client.secret=67c796c4-5605-4970-831a-d9b56be21cb9
client.id=spring-security-oauth2
```

#### Controller
Keycloakのuserinfoエンドポイントからユーザー情報を取得するサンプルコントローラーです。
restTemplateのgetForObjectメソッドでリソースへアクセスすることで、認可を取得後リソースへアクセスします。
getForXXXメソッドは、リソースの形式により適切なメソッドを利用します。今回のリソースはJSON形式のためgetForObjectを利用しています。

``` src/main/java/example/SampleController.java
package example;

import static org.springframework.web.bind.annotation.RequestMethod.*;
import java.util.*;
import org.codehaus.jackson.node.ObjectNode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestOperations;

@Controller
@PropertySource(value="classpath:oauth2.properties")
public class SampleController {

	@Value("${userInfoUri}")
	public String USER_INFO_URL;

	private RestOperations restTemplate;

	@RequestMapping(value = "/index", method = GET)
	public String top() {

		return "index.jsp";
	}

	@RequestMapping(value="/userinfo", method = GET)
	public String index(Model model) {
		// RestTemplateでリソースの取得を行う
		ObjectNode result = restTemplate
				.getForObject(USER_INFO_URL, ObjectNode.class);

		Iterator<String> names = result.getFieldNames();
		HashMap<String, String> map = new HashMap<String, String>();
		while (names.hasNext()) {
			String name = names.next();
			map.put(name, result.get(name).asText());
		}
		model.addAttribute("informations", map);
		return "userinfo.jsp";
	}

	public void setRestTemplate(OAuth2RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}
}
```


#### JSP

ログイン後ページ(index.jsp)です。リソースを利用する/userinfoへのリンクとログアウトボタンのみのシンプルなページです。

```WEB-INF/jsp/index.jsp
<html>
<body>
<h2>Logged in!</h2>
<a href="/sampleweb/userinfo">Get user information.</a>
<form action="<c:url value="/logout.do"/>" method="post">
	<input name="login" value="Logout" type="submit"/>
</form>
</body>
</html>
```

Keycloakのuserinfoエンドポイントから取得した情報を表示するJSPです。

``` WEB-INF/jsp/userinfo.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>userinfo</title></head>
<body>
<div id="container">
	<div id="content">
		<h1>User info:</h1>
		<table style="border: 1px black solid;">
		<c:forEach var="info" items="${informations}">
			<tr><td><c:out value="${info.key}"/></td><td><c:out value="${info.value}"/></td></tr>
		</c:forEach>
		</table>
	</div>
	<form action="<c:url value="/logout.do"/>" method="post">
		<input name="login" value="Logout" type="submit"/>
	</form>
</div>
</body>
</html>
```

### ビルド
プロジェクトを右クリックし、「実行」-「Maven install」をクリックするとtargetディレクトリに`sampleweb.war`ファイルが作成されます。
Tomcatサーバーのwebappsディレクトリにwarファイルをデプロイします。

## 接続してみる

`http://webapp.example.com/sampleweb/`へアクセスします。
まず、クライアントアプリのログイン画面が出ます。これはクライアントアプリを利用するための認証を想定しています。
`user`、`password`ともに「user」を入力して、Loginボタンをクリックします。
![screen1.png](https://qiita-image-store.s3.amazonaws.com/0/208407/dc039e27-425d-8b65-f3d5-ff8c4a0236ec.png)

クライアントWebアプリにログインしました。次に`Get user information`リンクをクリックします。
遷移先では、認可が必要なリソース（Keycloakのユーザー情報エンドポイント）へアクセスします。

![screen2.png](https://qiita-image-store.s3.amazonaws.com/0/208407/26c6bae6-75d7-b1ad-91e2-45319f77386f.png)
:information_source: SSL証明書が自己証明書の場合、証明書エラー画面が表示されますが、閲覧を継続するまたは、例外に追加するなどで閲覧を継続します。

まず認可コードを取得するため、Keycloakの認証画面へリダイレクトされます。
`Username or email`に「test001」、`Password`に「password」を入力してLoginボタンをクリックします。
![screen3.png](https://qiita-image-store.s3.amazonaws.com/0/208407/39f783cb-60ef-127c-8461-c94ec32ae716.png)

認証情報が正しければ、クライアントのリダイレクトURLへ認可コードレスポンスが返されます。
クライアントは、その認可コードを使用してアクセストークンを取得します。
取得したアクセストークンをAuthorizationヘッダに付与して、リソース（Keycloakのユーザー情報エンドポイントからユーザー情報）を取得し、画面に表示します。
![screen4.png](https://qiita-image-store.s3.amazonaws.com/0/208407/358e73c2-5378-2ce2-68a1-8c51744b241e.png)


※アプリケーションの動作確認時にSSLHandshakeException が発生する場合は、Tomcat WebアプリサーバーのKeyStoreにKeycloakサーバーのSSL証明書のインポートを行ってください。

※認可のみを試す場合は、web.xmlのフィルター設定からSpring Securityフィルターを削除し、Spring Security OAuth2のフィルターID"`oauth2ClientContextFilter`"を記述することでクライアントアプリケーションへのログインが不要となります。その場合は、`/sampleweb/index`へアクセスしてください。

# 最後に

今回はアクセストークンを取得して、リソースへアクセスするところまでとします。
KeycloakのSpring Securityアダプターを使用したOpenIDConnectによるクライアントアプリケーションへのログインも試そうとしましたが、Spring Framework 3系のバージョンでは動作しませんでした。
今回は、古いバージョンのSpring FrameworkとSpring Security OAuth2であったため、情報収集に苦労しました。
新しいバージョンのSpring FrameworkやSpring Bootでは、より簡単かつ便利にKeycloakと連携可能なようですので試してみたいと思います。
また、認可に関して興味のある方は、この[アドベントカレンダー](https://qiita.com/advent-calendar/2018/nri-openstandia)の[3日目の記事](https://qiita.com/yagiaoskywalker/items/66defea2ccb618c5633d)に詳しく書いてありますのでご参照ください。

# 参考とした情報
[Tutorial · Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/)
[Spring Security OAuth](http://projects.spring.io/spring-security-oauth/1.x/docs/oauth2.html)
[GitHub - spring-projects/spring-security-oauth](https://github.com/spring-projects/spring-security-oauth)
[keycloak-documentation.openstandia.jp - securing_apps](https://keycloak-documentation.openstandia.jp/4.0.0.Final/ja_JP/securing_apps/index.html#openid-connect-3)