はじめに
Active Directory のユーザで Spring Security の From 認証ができるか試してみたいと思います。
前提としては、Active Directory を構築済みということになります。
Active Directoryは、無料で楽しめる Samba 4 AD を使用しました。
Samba 4 の PDC の構築手順は、わたしも をまとめてみたりしています。
(
『Samba4 で Active Directory / インストール編』
『Samba4 で Active Directory / PDC編』
)
それから、もう一点、Spring MVC Framework など特定の MVC Framework、ライブラリーを使用しなくても
単独で Spring Security が動くことも確認したいと思います。
今回は、簡単な ログインアプリを作成して動作を検証してみたいと思います。
ではでは!Getting Started!!
環境
プリケーションサーバーアプリケーションサーバー | Tomcat 7.0.37 |
---|---|
Java | JDK 1.7.0_21 |
フレームワーク | Spring Framework 3.2.3.RELEASE |
Spring Security 3.1.4.RELEASE | |
ビルドツール | Maven 3.0.5 ※mvnコマンドが実行できるように設定しておきます。 |
OS | Windows XP |
Active Directory
Active Directory | Samba 4.0.5 |
---|---|
レルム | MYDOMAIN.LOCAL |
ドメイン | MYDOMAIN |
Host | pdc |
プロジェクト構成
Cドライブ直下に「studying-spring-security-ad」フォルダを作成し各ファイルを以下のように配置します。
ファイルは、すべて UTF-8 で保存します。
ServletとLoggerの設定
web.xml ファイルを以下の内容で作成する。web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xml="http://www.w3.org/XML/1998/namespace" 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"> <display-name>studying-spring-security-ad</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:META-INF/spring/applicationContext*.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-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> <session-config> <session-timeout>10</session-timeout> <tracking-mode>COOKIE</tracking-mode> </session-config> </web-app>
ログ設定ファイルを以下の内容で作成する。
Active Directory 認証処理の Debug ログがコンソールに出力されるようにしました。
log4j.properties
log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n log4j.rootLogger=error, stdout log4j.logger.org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider=debug, stdout log4j.logger.org.springframework.security.ldap.authentication=debug, stdout
エラーメッセージの日本語化設定
Spring 設定 ファイルを以下の内容で作成する。applicationContext-locale.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource" p:basenames="WEB-INF/i18n/messages" p:fallbackToSystemLocale="false" p:fileEncodings="UTF-8" p:defaultEncoding="UTF-8" /> </beans>
エラーメッセージファイルを以下の内容で作成する。
日本語化については雰囲気です。(笑
messages_ja.properties
LdapAuthenticationProvider.badCredentials=ログインユーザ、パスワードが違います。 LdapAuthenticationProvider.credentialsExpired=パスワードをもう一度入力してください。 LdapAuthenticationProvider.disabled=無効なユーザです。 LdapAuthenticationProvider.expired=期限切れユーザです。 LdapAuthenticationProvider.locked=ロックされています。 LdapAuthenticationProvider.emptyUsername=ログインユーザを入力してください。
AbstractUserDetailsAuthenticationProvider.XXX で始まるものを使用していたため
エラーメッセージをなかなか日本語表示できず結構ハマっていました。
Active Directory (というか Ldap ?)の場合、LdapAuthenticationProvider.XXX のものを使用するようです。
元々のメッセージプロパティファイルは、spring-security-core のバイナリの方の jar ファイルに含まれています。
/org/springframework/security フォルダあたりにあると思います。
割と他の言語のものは、揃っているのに日本語はありませんでした。
権限 Mapping クラスの作成
Active Directory の Group と Spring Security の Role の紐付けを行います。
Active Directory の memberOf 属性を元に紐付けを行うのですが
わたしの環境では、なぜか Primary Group に設定された Group が memberOf 属性に追加されないみたいでした。
となると Domain Users Group にしか所属しない AD ユーザの場合、memberOf 属性がなしになり Role を割り当てられません。
本来なら Domain Users あたりに所属する AD ユーザに ROLE_USER 権限を付与したいところなのですが
とりあえず、認証OK の場合は、無条件に ROLE_USER 権限を付与することにしました。
あとは、memberOf 属性から Domain Admins または、Administrators に所属する AD ユーザに ROLE_ADMIN 権限を付与しています。
package com.mydomain; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; public class MyGrantedAuthoritiesMapper implements GrantedAuthoritiesMapper { public Collection<? extends GrantedAuthority> mapAuthorities( Collection<? extends GrantedAuthority> authorities) { Set<SimpleGrantedAuthority> roles = new HashSet<SimpleGrantedAuthority>(); roles.add(new SimpleGrantedAuthority("ROLE_USER")); for (GrantedAuthority authority : authorities) { switch (authority.toString()) { case "Domain Admins" : case "Administrators" : roles.add(new SimpleGrantedAuthority("ROLE_ADMIN")); break; } } return roles; } }
Spring Securityの設定
「/resources」以下は、認証を設定せず、それ以外の URL に認証を設定しています。
「/admin.jsp」へのアクセスは、管理者権限を持つユーザに制限しています。
ログイン後にログインページが表示されるのが微妙なので
「/login.jsp」へのアクセスは、ログインしていないユーザに制限しています。
権限エラーページを使い回ししていますが、別に作った方がいいかも?
ログイン Top ページにリダイレクトするのもありかも??
ActiveDirectoryLdapAuthenticationProvider を定義して
AuthoritiesMapper に先ほど作成した MyGrantedAuthoritiesMapper クラスを設定します。
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" 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.2.xsd"> <http pattern="/resources/**" security="none"/> <http pattern="/login.jsp*" auto-config='true' access-denied-page="/denied.jsp"> <intercept-url pattern="/**" access="ROLE_ANONYMOUS" /> </http> <http pattern="/**" auto-config='true' access-denied-page="/denied.jsp" use-expressions="true"> <intercept-url pattern="/admin.jsp" access="hasRole('ROLE_ADMIN')" /> <intercept-url pattern="/**" access="isFullyAuthenticated()" /> <form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=t" /> </http> <authentication-manager> <authentication-provider ref="ldapActiveDirectoryAuthProvider" /> </authentication-manager> <beans:bean id="ldapActiveDirectoryAuthProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider" c:domain="mydomain.local" c:url="ldap://pdc:389/" p:authoritiesMapper-ref="myAuthoritiesMapper" p:useAuthenticationRequestCredentials="true" p:convertSubErrorCodesToExceptions="true" /> <beans:bean id="myAuthoritiesMapper" class="com.mydomain.MyGrantedAuthoritiesMapper"/> </beans:beans>
true にすると <intercept-url> タグの access 属性は、Spring EL の Security Expressions での設定になるようです。
access="ROLE_ADMIN"、access="IS_AUTHENTICATED_FULLY" は
access="hasRole('ROLE_ADMIN')"、access="isFullyAuthenticated()" となるようです。
結果が ture / false になるような EL 式 をaccess 属性値に設定する感じになるみたいです。
また、View ファイル中で Spring Security の Taglib の属性値に EL 式を使用する場合も
use-expressions="true" としておく必要があるようです。
View ファイルの作成
ログインページ、Top ページ、管理者ページ、権限エラーページの4つを作成します。なんとなく、ヘッダーとフッターをテンプレート化できそうですが
今回は、他のライブラリーなしでも Spring Security が動くこと確認したかったので使用しませんでした。
ページ遷移についても MVC フレームワークなどを使用していないので直リンクになっています。
login.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <c:url value="/resources/style.css" var="style"/> <link rel="stylesheet" type="text/css" href="${style}" /> <title>ログイン</title> </head> <body> <div id="page"> <h3>ログイン</h3> <c:if test="${not empty param.login_error}"> <div class="errors"> <p><c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" /></p> </div> </c:if> <spring:url value="/j_spring_security_check" var="form_url" /> <form name="f" action="${fn:escapeXml(form_url)}" method="POST"> <table> <tbody> <tr> <th><label for="j_username">ログインユーザ</label></th> <td><input id="j_username" type="text" name="j_username"/></td> </tr> <tr> <th><label for="j_password">パスワード</label></th> <td> <input id="j_password" type="password" name="j_password" /> <input id="proceed" type="submit" value="ログイン" /> </td> </tr> </tbody> </table> </form> </div> </body> </html>index.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <c:url value="/resources/style.css" var="style" /> <link rel="stylesheet" type="text/css" href="${style}" /> <title>Top ページ</title> </head> <body> <div id="page"> <h3>Top ページ</h3> ようこそ<sec:authentication property="principal.username" />さん<br /> <sec:authentication property="principal.dn" /><br /> <br /> 以下の権限が設定されています。 <sec:authentication property="authorities" var="roles" scope="page" /> <ul> <c:forEach var="role" items="${roles}"> <li>${role}</li> </c:forEach> </ul> <div class="footer"> <a href="./admin.jsp">Admin ページへ</a> <sec:authorize access="hasRole('ROLE_ADMIN') == false"> ※管理者権限は設定されていません。 </sec:authorize> <br /> <a href="./j_spring_security_logout">ログアウト</a> </div> </div> </body> </html>admin.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <c:url value="/resources/style.css" var="style" /> <link rel="stylesheet" type="text/css" href="${style}" /> <title>管理者ページ</title> </head> <body> <div id="page"> <h3>管理者ページ</h3> <div class="footer"> <a href="./">Top ページへ</a><br /> <a href="./j_spring_security_logout">ログアウト</a> </div> </div> </body> </html>denied.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <c:url value="/resources/style.css" var="style" /> <link rel="stylesheet" type="text/css" href="${style}" /> <title>権限エラー</title> </head> <body> <div id="page"> <div class="errors"> <h3>権限エラー</h3> <p>このページにアクセスする権限がありません。</p> </div> <div class="footer"> <a href="./">Top ページへ</a><br /> <a href="./j_spring_security_logout">ログアウト</a> </div> </div> </body> </html>style.css
body { margin: 0px; padding: 0px; font: 13px Arial, Helvetica, sans-serif; color: #212121; } h1 { margin-top: 0px; font-size: 2.4em; } p { margin-bottom: 1.8em; line-height: 160%; } table { margin: 0px auto; } th, td { text-align: left; } div.errors { background-color: #FFEBE8; border: solid 1px #DD3C10; width: 300px; margin: 3px auto; padding: 3px; } div.footer { border-top: solid 1px #777; margin-top: 7px; padding-top: 3px; text-align: left; } #page { width: 320px; margin: 0px auto; padding: 30px 0px; text-align: center; } #j_username { width: 98%; } #j_password { width: 100px; }
POM ファイルの作成
POM ファイルを以下の内容で作成する。pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mydomain</groupId> <artifactId>studying-spring-security-ad</artifactId> <packaging>war</packaging> <version>1.0</version> <name>studying-spring-security-ad</name> <properties> <java.version>1.7</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>3.2.3.RELEASE</spring.version> <spring-security.version>3.1.4.RELEASE</spring-security.version> </properties> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-ldap</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies> <build> <finalName>studying-spring-security-ad</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.1</version> </plugin> </plugins> </build> </project>
ビルドと実行
コマンドプロンプトを開きCドライブ直下の「studying-spring-security-ad」フォルダに移動後、以下の mvn コマンドを実行します。
cd c:\studying-spring-security-ad mvn clean package mvn tomcat7:run
ブラウザから「http://localhost:8080/studying-spring-security-ad/」を開いて AD ユーザでログインできるか試してみてください。
また、管理者ページを表示できるユーザとできないユーザも確認できると思います。
なぜかわたしの環境では最初、Administrator でログインできませんでした。
ログイン処理の際に Active Directory の userPrincipalName 属性を元にユーザーが検索されるようなのですが
これの登録がありませんでした。
Samba 4 で生成された Administrator は、自動的に userPrincipalName 属性が登録されないのでしょか?
Windows の Active Directory 管理から Administrator の「プロパティ」を開いて
「アカウント」タブにある「ユーザログオン名」を入力することでログインできるようになりました。
おわりに
割と簡単に AD ユーザでの Form認証ができたのでは思う。(Primary Group など微妙なところがあったにせよ…
この感じだとアプリ側をあまり変更せず、DB を使う認証プロバイダーにも変更できそうだ。
よくできているなぁと思った。
とはいっても、実際の業務アプリなどでお客さんに提案するのには勇気がいる。
Spring Security のものをベースにしつつも自分で MyActiveDirectoryLdapAuthenticationProvider 的なものを
作る覚悟は必要になるだろう。
Spring MVC Framework を使わなくても認証処理が動くことも確認できたと思う。
認証処理が Servlet Filter として実装されているからなのだろうけど。
Spring MVC Framework 以外の MVC Framework を選択したとしても
認証処理には、Spring Security を使うということも可能だと思う。
- 参考URL
- http://static.springsource.org/spring-security/site/docs/3.1.x/reference/ldap.html
- http://comdynamics.net/blog/544/spring-security-3-integration-with-active-directory-ldap/
- http://raymondhlee.wordpress.com/2012/03/17/active-directory-authentication-with-spring-security-3-1/
- http://static.springsource.org/spring-security/site/docs/3.1.x/reference/taglibs.html
- http://doanduyhai.wordpress.com/2012/02/26/spring-security-part-v-security-tags/
- http://support.microsoft.com/kb/275523/ja