はじめに
今回やること
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との連携を確認します。
環境
インストールバージョン
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
に修正しています。
<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の設定
-
spring-security-oauth2クライアントをopenid-connectプロトコルで作成しておきます。
Authorization Enabled
を「ON」にします。自動的にServiceAccount Enabled
が「ON」に、Access Type
が「confidential」になります。
-
レルムのユーザーを作成しておきます。
- Username : test001
- Password : password
-
ServiceAccount Enabled
が「ON」の状態で、Springアプリケーション(クライアント)がClient IDとClient Secretを利用してKeycloakへ接続できるようになります。Credentials
タブを参照してsecret
を確認します。
SpringWebアプリの作成
Eclipseプロジェクトの作成
- Eclipseで動的Webプロジェクトを作成
[ファイル]-[新規]-[プロジェクト]
メニューから、web/動的Webプロジェクト
を選択し、次へ。
プロジェクト名を入力して、次へをクリックします。(ここではsampleweb
としました。)
再度次へをクリックし、最後にweb.xmlデプロイメント記述子の生成
にチェックをつけ、完了ボタンをクリックします。 - プロジェクトをmavenプロジェクトへ変更
作成したプロジェクトを右クリックし、[構成]-[Mavenプロジェクトへ変換]
をクリックします。
グループIDやプロジェクトIDなどプロジェクト情報を入力(デフォルトでOK)、完了ボタンをクリックします。 - 依存関係の記述(pom.xml)
プロジェクトディレクトリ直下にpom.xmlファイルが作成されているので、dependencies
ディレクティブ内に以下を追記します。
また、ビルドされるwarファイルの名前をbuild
タグ中のfinalName
に記述します。
<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が登録されるため一致する必要があります。
<?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に関する定義です。
<?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
の値を記述します。
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を利用しています。
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へのリンクとログアウトボタンのみのシンプルなページです。
<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です。
<%@ 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ボタンをクリックします。
クライアントWebアプリにログインしました。次にGet user information
リンクをクリックします。
遷移先では、認可が必要なリソース(Keycloakのユーザー情報エンドポイント)へアクセスします。
SSL証明書が自己証明書の場合、証明書エラー画面が表示されますが、閲覧を継続するまたは、例外に追加するなどで閲覧を継続します。
まず認可コードを取得するため、Keycloakの認証画面へリダイレクトされます。
Username or email
に「test001」、Password
に「password」を入力してLoginボタンをクリックします。
認証情報が正しければ、クライアントのリダイレクトURLへ認可コードレスポンスが返されます。
クライアントは、その認可コードを使用してアクセストークンを取得します。
取得したアクセストークンをAuthorizationヘッダに付与して、リソース(Keycloakのユーザー情報エンドポイントからユーザー情報)を取得し、画面に表示します。
※アプリケーションの動作確認時に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