Edited at

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アダプターが開発されています。

そのため、簡単に連携することが可能です。

こちらの記事で紹介されていますので、Spring BootでKeycloakと連携したい方はご参照ください。


背景

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


連携確認

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


環境

今回検証を行う環境は以下の通りです。

os-middle.png


インストールバージョン

FQDN
役割
構成

webapp.example.com
SpringWebアプリケーションサーバー
CentOS 7.4
OpenJDK 1.7.0_181
Apache Tomcat 7.0.57

keycloak.example.com
Keycloakサーバー
CentOS 7.4
OpenJDK 1.8.0_191
Apache Httpd 2.4.6
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用の自己署名証明書の作成については、私の過去の記事にも記載しています。


開発環境


  • Windows7 Professional SP1

  • Eclipse Oxygen

  • Maven EclipseEmbedded 3.3.9


Keycloakの設定


  1. demoレルムを作成します

    keycloak-realm1.png


  2. spring-security-oauth2クライアントをopenid-connectプロトコルで作成しておきます。Authorization Enabledを「ON」にします。自動的にServiceAccount Enabledが「ON」に、Access Typeが「confidential」になります。

    keycloak-client1.png



  3. レルムのユーザーを作成しておきます。


    • Username : test001

    • Password : password



  4. ServiceAccount Enabledが「ON」の状態で、Springアプリケーション(クライアント)がClient IDとClient Secretを利用してKeycloakへ接続できるようになります。Credentialsタブを参照してsecretを確認します。

    keycloak-client2.png



SpringWebアプリの作成


Eclipseプロジェクトの作成


  1. Eclipseで動的Webプロジェクトを作成

    [ファイル]-[新規]-[プロジェクト]メニューから、web/動的Webプロジェクトを選択し、次へ。

    プロジェクト名を入力して、次へをクリックします。(ここではsamplewebとしました。)

    再度次へをクリックし、最後にweb.xmlデプロイメント記述子の生成にチェックをつけ、完了ボタンをクリックします。

  2. プロジェクトをmavenプロジェクトへ変更

    作成したプロジェクトを右クリックし、[構成]-[Mavenプロジェクトへ変換]をクリックします。

    グループIDやプロジェクトIDなどプロジェクト情報を入力(デフォルトでOK)、完了ボタンをクリックします。

  3. 依存関係の記述(pom.xml)

    プロジェクトディレクトリ直下にpom.xmlファイルが作成されているので、dependenciesディレクティブ内に以下を追記します。

    また、ビルドされるwarファイルの名前をbuildタグ中のfinalNameに記述します。


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/へアクセスします。

まず、クライアントアプリのログイン画面が出ます。これはクライアントアプリを利用するための認証を想定しています。

userpasswordともに「user」を入力して、Loginボタンをクリックします。

screen1.png

クライアントWebアプリにログインしました。次にGet user informationリンクをクリックします。

遷移先では、認可が必要なリソース(Keycloakのユーザー情報エンドポイント)へアクセスします。

screen2.png

:information_source: SSL証明書が自己証明書の場合、証明書エラー画面が表示されますが、閲覧を継続するまたは、例外に追加するなどで閲覧を継続します。

まず認可コードを取得するため、Keycloakの認証画面へリダイレクトされます。

Username or emailに「test001」、Passwordに「password」を入力してLoginボタンをクリックします。

screen3.png

認証情報が正しければ、クライアントのリダイレクトURLへ認可コードレスポンスが返されます。

クライアントは、その認可コードを使用してアクセストークンを取得します。

取得したアクセストークンをAuthorizationヘッダに付与して、リソース(Keycloakのユーザー情報エンドポイントからユーザー情報)を取得し、画面に表示します。

screen4.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と連携可能なようですので試してみたいと思います。

また、認可に関して興味のある方は、このアドベントカレンダー3日目の記事に詳しく書いてありますのでご参照ください。


参考とした情報

Tutorial · Spring Boot and OAuth2

Spring Security OAuth

GitHub - spring-projects/spring-security-oauth

keycloak-documentation.openstandia.jp - securing_apps