2013年5月22日水曜日

Spring Security で Active Directory にログインする

はじめに

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 で保存します。

  • C:\studying-spring-security-ad
    • src
      • main
        • java
          • com
            • mydomain
              • MyGrantedAuthoritiesMapper.java
        • resources
          • META-INF
            • spring
              • applicationContext-locale.xml
              • applicationContext-security.xml
          • log4j.properties
        • webapp
          • resources
            • style.css
          • WEB-INF
            • i18n
              • messages_ja.properties
            • web.xml
          • admin.jsp
          • denied.jsp
          • index.jsp
          • login.jsp
    • pom.xml


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 権限を付与しています。

MyGrantedAuthoritiesMapper.java
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 クラスを設定します。

applicationContext-security.xml
<?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>
<http> タグの use-expressions 属性について

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 について

なぜかわたしの環境では最初、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