2013年5月28日火曜日

AspectJ の Pointcut - this と target の違いって?

はじめに

this と target についてそれぞれ thisは、実行中のオブジェクト、target は、対象オブジェクトであれば
A というクラスが B というクラスを呼出している場合、this は A クラス、target は B クラスになってほしいのですが
Spring の AspectJ では this と target が同じオブジェクトになってしまう。
調べてみると call では、上記のような動きになり、execution では同じオブジェクトになるようだ。
Spring の AspectJ は、execution のみで call に対応していないらしい。
そんな訳で本家の AspectJ を使って call と execution での this と target の違いを調べてみたいと思います。

今回は、簡単な実験クラスをいくつか作成して検証してみたいと思います。
ではでは!Getting Started!!


環境

Java JDK 1.7.0_21
フレームワーク aspectjrt 1.7.2
ビルドツール Maven 3.0.5
※mvnコマンドが実行できるように設定しておきます。
OS Windows XP


プロジェクト構成

Cドライブ直下に「studying-aspectj-pointcut」フォルダを作成し各ファイルを以下のように配置します。
ファイルは、すべて UTF-8 で保存します。

  • C:\studying-aspectj-pointcut
    • src
      • main
        • java
          • com
            • mydomain
              • App.java
              • Casino.java
              • Gambler.java
              • Money.java
              • MyAspect.java
    • pom.xml


POM ファイルの作成

AspectJ のコンパイルに aspectj-maven-plugin を使用しています。
また jar の生成には、maven-shade-plugin を使用しています。
このプラグインは、依存する jar を一旦バラして 今回作成したクラスと一緒に1つの jar ファイルにまとめてくれます。
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/xsd/maven-4.0.0.xsd">
    
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.mydomain</groupId>
  <artifactId>studying-aspectj</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>

  <name>studying-aspectj</name>

  <properties>
    <java.version>1.7</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.7.2</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>studying-aspectj</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.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.4</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>test-compile</goal>
            </goals>
            <configuration>
              <complianceLevel>1.6</complianceLevel>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>com.mydomain.App</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>


Aspect 実験クラス作成

概要としては、次のような感じです。
App クラスのメインメソッドで Money クラスを作成して Gambler クラスの paySalary() メソッドに渡します。
Gambler クラスは、Casino クラスの gambling() メソッドに 受け取った Money クラスを渡します。
そして、持ち金の 20% が無くなります。
簡単に言うと貰った給料で即パチンコ打ってまぁまぁ負けるというような感じです。(笑

App.java
package com.mydomain;

public class App {

  public static void main(String[] args) {

    Money money = new Money();
    money.setValue(100);

    Gambler gambler = new Gambler();
    gambler.paySalary(money);
  }
}
Money.java
package com.mydomain;

public class Money {

  private int value;

  public int getValue() {
    return value;
  }

  public void setValue(int value) {
    this.value = value;
  }
}
Gambler.java
package com.mydomain;

public class Gambler {

  public void paySalary(Money money) {

    System.out.printf("Gambler paySalary %s %n", money.getValue());

      Casino casino = new Casino();
      casino.gambling(money);
  }
}
Casino.java
package com.mydomain;

public class Casino {

  public void gambling(Money money) {

    money.setValue((int)(money.getValue() * 0.8));
    System.out.printf("Casino gambling %s %n", money.getValue());
  }
}


Aspect クラスを作成して Pointcut で遊ぶ

Casino クラスの gambling() メソッドに着目して this と target を調べてみたいと思います。
this と target が call と execution ではどう違うかを Around アドバイスを使って見てみます。
MyAspect クラスを以下の内容で作成します。

MyAspect.java
package com.mydomain;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class MyAspect {

  @Around("call(void com.mydomain.Casino.gambling(Money))")
  public Object aroundCall(ProceedingJoinPoint pjp) throws Throwable {
    
    System.out.printf("AroundCall [this:%s] [target:%s] %n", pjp.getThis(), pjp.getTarget());
    
    Object retVal = pjp.proceed();
    return retVal;
  }

  @Around("execution(void com.mydomain.Casino.gambling(Money))")
  public Object aroundExec(ProceedingJoinPoint pjp) throws Throwable {
    
    System.out.printf("AroundExec [this:%s] [target:%s] %n", pjp.getThis(), pjp.getTarget());
    
    Object retVal = pjp.proceed();
    return retVal;
  }
}

コマンドプロンプトを開きCドライブ直下の「studying-aspectj-pointcut」フォルダに移動後、 mvn コマンドでビルドします。
そして生成された jar ファイルを実行します。

cd c:\studying-aspectj-pointcut
mvn clean package
java -jar C:\studying-aspectj-pointcut\target\studying-aspectj.jar

実行結果は以下のようになると思います。

Gambler paySalary 100
AroundCall [this:com.mydomain.Gambler@1c7e2da] [target:com.mydomain.Casino@1fe571f]
AroundExec [this:com.mydomain.Casino@1fe571f] [target:com.mydomain.Casino@1fe571f]
Casino gambling 80

なるほど call では、this が呼出元クラス、target は、呼出先クラスになり execution では、this、target 両方対象クラスになる。
納得だ!
今度は、Before アドバイスでも試してみる。
MyAspect クラスのメソッドを以下の内容に差し替えます。

MyAspect.java
...
  @Before("call(void com.mydomain.Casino.gambling(Money)) && this(Gambler) && target(Casino)")
  public void beforeCall(){ 
    System.out.printf("Before Call %n");
  }

  @Before("execution(void com.mydomain.Casino.gambling(Money)) && this(Casino) && target(Casino)")
  public void beforeExecution(){ 
    System.out.printf("Before Execution %n");
  }
...

再ビルド後、実行すると以下のようになると思います。

Gambler paySalary 100
Before Call
Before Execution
Casino gambling 80

Aspect は呼出されているようだけど、なんだかよく分からないので this と target をキャプチャして print してみる。
MyAspect クラスのメソッドを以下の内容に差し替えて再ビルド後、実行する。

MyAspect.java
...
  @Before("call(void com.mydomain.Casino.gambling(Money)) && this(obj01) && target(obj02)")
  public void beforeCall(Gambler obj01, Casino obj02){ 
    System.out.printf("Before Call [this:%s] [target:%s] %n", obj01, obj02);
  }

  @Before("execution(void com.mydomain.Casino.gambling(Money)) && this(obj01) && target(obj02)")
  public void beforeExecution(Casino obj01, Casino obj02){ 
    System.out.printf("Before Execution [this:%s] [target:%s] %n", obj01, obj02);
  }
...
Gambler paySalary 100
Before Call [this:com.mydomain.Gambler@7eb366] [target:com.mydomain.Casino@33f0de]
Before Execution [this:com.mydomain.Casino@33f0de] [target:com.mydomain.Casino@33f0de]
Casino gambling 80

ふむふむ、Before アドバイスでも call と execution で this と target が異なることが分かる。
試しに execution の this を Gambler クラス(call での呼出元クラス)変更してみる。
MyAspect クラスの beforeExecution() メソッドを以下の内容に差し替えて再ビルド後、実行する。

MyAspect.java
...
  @Before("execution(void com.mydomain.Casino.gambling(Money)) && this(obj01) && target(obj02)")
  public void beforeExecution(Gambler obj01, Casino obj02){ 
    System.out.printf("Before Execution [this:%s] [target:%s] %n", obj01, obj02);
  }
...
Gambler paySalary 100
Before Call [this:com.mydomain.Gambler@d1c778] [target:com.mydomain.Casino@7eb366]
Casino gambling 80

おぉ!execution の アドバイスが呼出されなくなった。
当たり前か?(笑
ノッてきたぞ!

もう少し遊んでみる。
今度は、args を使ってメソッドの引数で使われている Money クラスを Pointcut 条件にしてみる。
args も this と target と同じようにクラス・インターフェイスの型で Pointcut 条件を指定する。
キャプチャもできるようだ。
within は、this と target と同じようにクラス・インターフェイスの型で Pointcut 条件を指定するのだけどキャプチャはできない。
また実行オブジェクト、対象オブジェクトというような区別もなく指定した型が Pointcut 条件 になるようだ。
!within(MyAspect) で MyAspect クラスの before() メソッドを Aspect の対象外にしています。
それというのも before() メソッドも引数で Money クラスを受けるの Pointcut 条件 にマッチして
結果再帰呼出になってしまうからです。
MyAspect クラスのメソッドを以下の内容に差し替えて再ビルド後、実行する。

MyAspect.java
...
  @Before("args(obj01) && !within(MyAspect)")
  public void before(Money obj01){ 
    System.out.printf("Before [args:%s] %n", obj01.getValue());
  }
...
Before [args:100]
Before [args:100]
Gambler paySalary 100
Before [args:100]
Before [args:100]
Casino gambling 80

むむむ… Aspect は呼出されているようだけど、なんだかよく分からない。
Around アドバイスを使ってthis、target、メソッドを print してみる。
MyAspect クラスのメソッドを以下の内容に差し替えて再ビルド後、実行する。

MyAspect.java
...
  @Around("args(Money) && !within(MyAspect)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
 
    System.out.printf(
      "Around [this:%s] [target:%s] [signature:%s] %n",
      pjp.getThis(), pjp.getTarget(), pjp.getSignature());

    Object retVal = pjp.proceed();
    return retVal;
  }
...
Around [this:null] [target:com.mydomain.Gambler@83b1b] [signature:void com.mydomain.Gambler.paySalary(Money)]
Around [this:com.mydomain.Gambler@83b1b] [target:com.mydomain.Gambler@83b1b] [signature:void com.mydomain.Gambler.paySalary(Money)]
Gambler paySalary 100
Around [this:com.mydomain.Gambler@83b1b] [target:com.mydomain.Casino@b32ed4] [signature:void com.mydomain.Casino.gambling(Money)]
Around [this:com.mydomain.Casino@b32ed4] [target:com.mydomain.Casino@b32ed4] [signature:void com.mydomain.Casino.gambling(Money)]
Casino gambling 80

なるほど、call と execution の両方に作用しているようだ。
this が null になっているところは、App クラスの main() メソッドが static だからだろう。
また、main() メソッド中 の Gambler クラスの paySalary() メソッドの呼出には、call が作用しているのだろう。
もう少し Pointcut 条件を絞ってみる。
Gamblerクラス が Casino クラスの gambling() メソッドを呼出しているところあたりに作用するようにしてみる。
MyAspect クラスのメソッドを以下の内容に差し替えて再ビルド後、実行する。

MyAspect.java
...
  @Around("this(Gambler) && target(Casino) && args(Money)")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {

    System.out.printf(
      "Around [this:%s] [target:%s] [signature:%s] %n",
   pjp.getThis(), pjp.getTarget(), pjp.getSignature());

    Object retVal = pjp.proceed();
    return retVal;
  }
...
Gambler paySalary 100
Around [this:com.mydomain.Gambler@1d0d45b] [target:com.mydomain.Casino@125d06e] [signature:void com.mydomain.Casino.gambling(Money)]
Casino gambling 80

あれ?これは、「@Around("call(void com.mydomain.Casino.gambling(Money))")」と同じでは?
最初に戻ってしまった…
なんとなくオチがついたようなので今回はこの辺で。


おわりに

this と target について、とりあえずは整理できたと思う。
本家の AspectJ には、まだまだ、ディープな Pointcut があるのだけれども
Spring が対応している AspectJ の主な Pointcut は、今回、使った思う。
残りは、アノテーションを Pointcut 条件にするものがある。
また、Spring 独自の bean なんてなのもあるようだ。
これらは、またいつかの機会に。

参考URL
http://www.eclipse.org/aspectj/doc/next/progguide/index.html
http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/aop.html
http://netail.net/aosdwiki/index.php?AspectJ%2F%B4%CA%B0%D7%A5%EA%A5%D5%A5%A1%A5%EC%A5%F3%A5%B9
http://maven.apache.org/plugins/maven-shade-plugin/
http://mojo.codehaus.org/aspectj-maven-plugin/

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

2013年5月20日月曜日

Spring Security で Digest 認証をテストする

はじめに

なぜに Digest 認証なのか?
RESTful な Web アプリでユーザ認証を考えた場合
普通に Form 認証的な感じで Cookie に Session Id を保存するパターンでもいいのでは?
と思ったのですが RESTful の要件に Stateless というのもがあり
アクセスは 1回ごとに完結しないといけないとのことです。
クライアントのセッション状態をサーバーで管理するのはよくないようです。
このあたりは、理念というか、こだわりというか、なんとなくどちらでもいいような…
まぁ、とりあえず、Cookie / Session はダメということで
Http 認証ということになる訳ですが
Basic 認証では、ちょっと…なので Digest 認証になりました。
どの場合であっても SSL にはしておきたいところです。

今回は、簡単な Web アプリとテストを作成して動作を検証してみたいと思います。
また、Spring Framework 3.2 から Spring MVC Test Framework が利用できるので、これも試してみます。
ではでは!Getting Started!!


環境

OS Windows XP
アプリケーションサーバー Tomcat 7.0.37
Java JDK 1.7.0_21
フレームワーク Spring Framework 3.2.2.RELEASE
Spring Security 3.1.4.RELEASE
ライブラリー HttpClient 4.2.5
JsonPath 0.8.1
Jackson 2.2.1
JXPath Component 1.3
ビルドツール Maven 3.0.5
※mvnコマンドが実行できるように設定しておきます。


プロジェクト構成

Cドライブ直下に「studying-spring-security-digest」フォルダを作成し各ファイルを以下のように配置します。
ファイルは、すべて UTF-8 で保存します。

  • C:\studying-spring-security-digest
    • src
      • main
        • java
          • com
            • mydomain
              • web
                • ApiController.java
        • resources
          • META-INF
            • spring
              • applicationContext-security.xml
          • log4j.properties
        • webapp
          • WEB-INF
            • spring
              • webmvc-config.xml
            • web.xml
      • test
        • java
          • com
            • mydomain
              • web
                • ApiControllerDigestAuthTest.java
    • 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-digest</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>
  
  <servlet>
    <servlet-name>springMVCServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>WEB-INF/spring/webmvc-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>springMVCServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

ログ設定ファイルを以下の内容で作成する。
Digest 認証の 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.web.authentication.www.DigestAuthenticationEntryPoint=debug, stdout
log4j.logger.org.springframework.security.web.authentication.www.DigestAuthenticationFilter=debug, stdout


Spring MVCの設定

Spring MVCの設定 ファイルを以下の内容で作成する。

webmvc-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  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
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.2.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">

  <context:component-scan base-package="com.mydomain" />
  <mvc:annotation-driven />
</beans>


Spring Securityの設定

「/」は、認証を設定せず、それ以外の URL に認証を設定しています。
「/Admin」へのアクセスは、管理者権限を持つユーザに制限しています。
セッションは使用しないので stateless としています。

Spring Security で Digest 認証を行うためには、いろいろ決め事があるみたいで
とりあえず、ユーザーのパスワードは平文で保存しておかないといけないようです。
ハッシュ化して保存したい場合は、 DigestAuthenticationFilter あたりを改造することになるのでしょうか?
Digest 認証の Filter も明示的に定義する必要があるようです。
今回、Basic 認証を使わないので、その位置に挟んでいます。
また、Digest 認証の Filter には、AuthenticationManager でなく
直接 UserDetailsService を設定するようです。
nonce の有効期限は 3秒に設定しています。

ログインユーザー情報は、InMemoryDaoImpl から取得するようにしました。
ユーザーアカウントは一般ユーザと管理者ユーザを登録しています。

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:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  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="/" security="none" create-session="stateless"/>
  
  <http entry-point-ref="digestEntryPoint" create-session="stateless">
    <intercept-url pattern="/Admin" access="ROLE_ADMIN" />
    <intercept-url pattern="/**" access="ROLE_USER" />
    <custom-filter position="BASIC_AUTH_FILTER" ref="digestFilter"/>
  </http>

  <beans:bean id="digestFilter"
    class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
    <beans:property name="userDetailsService" ref="inMemoryDaoImpl" />
    <beans:property name="authenticationEntryPoint" ref="digestEntryPoint" />
  </beans:bean>

  <beans:bean id="digestEntryPoint"
    class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
    <beans:property name="realmName" value="My Realm" />
    <beans:property name="key" value="mykey" />
    <beans:property name="nonceValiditySeconds" value="3" />
  </beans:bean>
  
  <authentication-manager>
    <authentication-provider>
      <user-service id="inMemoryDaoImpl">
        <user name="admin" password="admin-pass" authorities="ROLE_USER, ROLE_ADMIN" />
        <user name="user" password="user-pass" authorities="ROLE_USER" />
      </user-service>
    </authentication-provider>
  </authentication-manager>
</beans:beans>


Controller クラスの作成

ApiController クススを以下の内容で作成する。
呼出可能な URL は、「/」「/Home」「/Admin」「UserDetails」4つです。
「/」「/Home」「/Admin」は、文字列を返します。
「UserDetails」は、ログインユーザ情報を Json 形式で返します。

ApiController.java
package com.mydomain.web;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
public class ApiController {

  @RequestMapping("/")
  @ResponseBody
  public String index() { return "Index"; }
  
  @RequestMapping("/Home")
  @ResponseBody
  public String home() { return "Home"; }

  @RequestMapping("/Admin")
  @ResponseBody
  public String admin() { return "Admin"; }

  @RequestMapping("/UserDetails")
  @ResponseBody
  public UserDetails userDetails() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    return (UserDetails)auth.getPrincipal();
  }
}


Controller クラスのテスト作成

Spring Security が起動するように setup() メソッドの中で FilterChainProxy を
MockMvc の Filter に追加しています。
MockHttpServletRequestBuilder から Digest 認証の Authorization リクエストヘッダー を
追加できそうになかったので Apache HttpComponents を使用することにしました。
このあたりを自作してしまうと今度はそこのテストが必要になってきそうなので…
Json データの検証は、JsonPath を使う方法と Jackson と JXPath を使う方法を試してみました。

ApiControllerDigestAuthTest.java
package com.mydomain;

import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.Map;

import org.apache.commons.jxpath.JXPathContext;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.protocol.BasicHttpContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.ServletWebRequest;

import com.fasterxml.jackson.databind.ObjectMapper;


@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration()
@ContextConfiguration({
  "file:src/main/webapp/WEB-INF/spring/webmvc-config.xml",
  "file:src/main/resources/META-INF/spring/applicationContext-security.xml"})
public class ApiControllerDigestAuthTest {

  @Autowired
  private WebApplicationContext wac;
  
  @Autowired
  private FilterChainProxy springSecurityFilterChain;

  private MockMvc mockMvc;

    
  @Before
  public void setup() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
      .addFilters(this.springSecurityFilterChain).build();
  }
    
  @SuppressWarnings("unchecked")
  @Test
  public void RoleUserTest() throws Exception {

    // ===== RoleUser Test Data ===== //
    String userName = "user";
    String password = "user-pass";
    UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(userName, password);
    BasicHttpRequest request = new BasicHttpRequest("GET", "/");
    BasicHttpContext context = new BasicHttpContext();
    BasicHeader header = null;
    DigestScheme digestScheme = null;
    Header reqHeader = null;
    String resHeader = "";
    MvcResult mvcResult = null;
  

    // ===== Test 1 ===== //
    this.mockMvc
      .perform(
        get("/")
        .accept(MediaType.TEXT_PLAIN))
      .andExpect(status().isOk())
      .andExpect(content().string("Index"));


    // ===== Test 2 ===== //
    mvcResult = this.mockMvc
      .perform(
        get("/Home")
        .accept(MediaType.TEXT_PLAIN))
      .andExpect(status().isUnauthorized())
      .andReturn();
  

    // ===== Test 3 ===== //
    // Response Header から Authorization Digest Request Header を作成する。
    resHeader = mvcResult.getResponse().getHeaderValue("WWW-Authenticate").toString();
    resHeader = resHeader.substring(7); // 先頭文字列 Digest を削除
    header = new BasicHeader("WWW-Authenticate", resHeader);
  
    digestScheme = new DigestScheme();
    for (HeaderElement elm : header.getElements()) {      
      switch (elm.getName()) {
        case "realm" :
        case "nonce" :
          digestScheme.overrideParamter(elm.getName(), elm.getValue());
          break;
      }
    }
    digestScheme.overrideParamter("qop", "auth");
    reqHeader = digestScheme.authenticate(credentials, request, context);
  
    // URL 呼出し
    this.mockMvc
      .perform(
        get("/Home")
        .accept(MediaType.TEXT_PLAIN)
        .header(reqHeader.getName(), reqHeader.getValue()))
      .andExpect(status().isOk())
      .andExpect(content().string("Home"));


    // ===== Test 4 ===== //
    reqHeader = digestScheme.authenticate(credentials, request, context);
    this.mockMvc
      .perform(
        get("/Admin")
        .accept(MediaType.TEXT_PLAIN)
        .header(reqHeader.getName(), reqHeader.getValue()))
      .andExpect(status().isForbidden());
  

    // ===== Test 5 ===== //
  
    // 3秒スリープして nonce を期限切れにする。
    Thread.sleep(3000);
    reqHeader = digestScheme.authenticate(credentials, request, context);
    mvcResult = this.mockMvc
      .perform(
        get("/UserDetails")
        .accept(MediaType.TEXT_PLAIN)
        .header(reqHeader.getName(), reqHeader.getValue()))
      .andExpect(status().isUnauthorized())
      .andReturn();
  

    // ===== Test 6 ===== //
    // Response Header から Authorization Digest Request Header を作成する。
    resHeader = mvcResult.getResponse().getHeaderValue("WWW-Authenticate").toString();
    resHeader = resHeader.substring(7);
    header = new BasicHeader("WWW-Authenticate", resHeader);
  
    digestScheme = new DigestScheme();
    for (HeaderElement elm : header.getElements()) {      
      switch (elm.getName()) {
        case "realm" :
        case "nonce" :
          digestScheme.overrideParamter(elm.getName(), elm.getValue());
          break;
      }
    }
    digestScheme.overrideParamter("qop", "auth");
    reqHeader = digestScheme.authenticate(credentials, request, context);

    // URL 呼出し - JsonPath で JSON データを検証
    this.mockMvc
      .perform(
        get("/UserDetails")
        .accept(MediaType.APPLICATION_JSON)
        .header(reqHeader.getName(), reqHeader.getValue()))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.username").value(userName))
      .andExpect(jsonPath("$.authorities").isArray())
      .andExpect(jsonPath("$.authorities[?(@.authority == 'ROLE_USER')]").exists())
      .andExpect(jsonPath("$.authorities[?(@.authority == 'ROLE_ADMIN')]").doesNotExist());
  }

  @SuppressWarnings("unchecked")
  @Test
  public void RoleAdminTest() throws Exception {

    // ===== RoleAdmin Test Data ===== //
    String userName = "admin";
    String password = "admin-pass";
    UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(userName, password);
    BasicHttpRequest request = new BasicHttpRequest("GET", "/");
    BasicHttpContext context = new BasicHttpContext();
    BasicHeader header = null;
    DigestScheme digestScheme = null;
    Header reqHeader = null;
    String resHeader = "";
    MvcResult mvcResult = null;


    // ===== Test 7 ===== //
    mvcResult = this.mockMvc
      .perform(
        get("/Admin")
        .accept(MediaType.TEXT_PLAIN))
      .andExpect(status().isUnauthorized())
      .andReturn();


    // ===== Test 8 ===== //
    // Response Header から Authorization Digest Request Header を作成する。
    resHeader = mvcResult.getResponse().getHeaderValue("WWW-Authenticate").toString();
    resHeader = resHeader.substring(7);
    header = new BasicHeader("WWW-Authenticate", resHeader);

    digestScheme = new DigestScheme();
    for (HeaderElement elm : header.getElements()) {      
      switch (elm.getName()) {
        case "realm" :
        case "nonce" :
          digestScheme.overrideParamter(elm.getName(), elm.getValue());
          break;
      }
    }
    digestScheme.overrideParamter("qop", "auth");
    reqHeader = digestScheme.authenticate(credentials, request, context);

    // URL 呼出し
    this.mockMvc
      .perform(
        get("/Admin")
        .accept(MediaType.TEXT_PLAIN)
        .header(reqHeader.getName(), reqHeader.getValue()))
      .andExpect(status().isOk())
      .andExpect(content().string("Admin"));


    // ===== Test 9 ===== //
    mvcResult = this.mockMvc
      .perform(
        get("/UserDetails")
        .accept(MediaType.APPLICATION_JSON)
        .header(reqHeader.getName(), reqHeader.getValue()))
      .andExpect(status().isOk())
      .andReturn();
  
    // Jackson と JXPath で JSON データを検証
    String response = mvcResult.getResponse().getContentAsString();
    ObjectMapper mapper = new ObjectMapper();
    Map<String, Object> userDetails = mapper.readValue(response, Map.class);
    JXPathContext jxContext = JXPathContext.newContext(userDetails);
  
    assertEquals(userName, jxContext.getValue("username").toString());
    assertEquals(2, jxContext.selectNodes("authorities").size());
    assertEquals(1, jxContext.selectNodes("authorities[@authority='ROLE_USER']").size());
    assertEquals(1, jxContext.selectNodes("authorities[@authority='ROLE_ADMIN']").size());
  }
}
RoleUser Test Data
一般ユーザのテストデータ
Test 1
「/」の呼出しテスト
認証が不要な URL にアクセスする。
レスポンスコード 200:OK と共に文字列 Index を取得する。
Test 2
「/Home」の呼出しテストA
認証が必要な URL に Authorization リクエストヘッダーなしでアクセスする。
レスポンスコード 401:Unauthorized が返る。
Test 3
「/Home」の呼出しテストB
Test 2 のレスポンスヘッダー WWW-Authenticate から realm と nonce を取得し
ユーザー と パスワードを加えて Authorization リクエストヘッダー作成する。
Authorization リクエストヘッダーを付けてアクセスする。
レスポンスコード 200:OK と共に文字列 Home を取得する。
Test 4
「/Admin」の呼出しテスト
Admin 権限が必要な URL に 一般ユーザでアクセスする。
Test 3 で作成した Authorization リクエストヘッダーを使用する。
レスポンスコード 403:Forbidden が返る。
Test 5
「/UserDetails」の呼出しテストA
有効期限切れ nonce でアクセスする。
nonce を有効期限切れにするため 3秒スリープする。
Test 3 で作成した Authorization リクエストヘッダーを使用する。
レスポンスコード 401:Unauthorized が返る。
Test 6
「/UserDetails」の呼出しテストB
Test 5 のレスポンスヘッダー WWW-Authenticate から realm と nonce を取得し
ユーザー と パスワードを加えて Authorization リクエストヘッダー再作成する。
Authorization リクエストヘッダーを付けてアクセスする。
レスポンスコード 200:OK と共にログインユーザ情報を Json 形式で取得する。
JSON データを JsonPath で検証する。
RoleAdmin Test Data
管理者ユーザのテストデータ
Test 7
「/Admin」の呼出しテストA
認証が必要な URL に Authorization リクエストヘッダーなしでアクセスする。
レスポンスコード 401:Unauthorized が返る。
Test 8
「/Admin」の呼出しテストB
Test 7 のレスポンスヘッダー WWW-Authenticate から realm と nonce を取得し
ユーザー と パスワードを加えて Authorization リクエストヘッダー作成する。
Authorization リクエストヘッダーを付けてアクセスする。
Admin 権限が必要な URL に 管理者ユーザでアクセスする。
レスポンスコード 200:OK と共に文字列 Admin を取得する。
Test 9
「/UserDetails」の呼出しテスト
Test 8 で作成した Authorization リクエストヘッダーを使用する。
レスポンスコード 200:OK と共にログインユーザ情報を Json 形式で取得する。
JSON データを Jackson と JXPath で 検証する。


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-digest</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>studying-spring-security-digest</name>

  <properties>
    <java.version>1.7</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>3.2.2.RELEASE</spring.version>
    <spring-security.version>3.1.4.RELEASE</spring-security.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </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</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </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-web</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>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.2.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>4.2.5</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.jayway.jsonpath</groupId>
      <artifactId>json-path-assert</artifactId>
      <version>0.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>commons-jxpath</groupId>
      <artifactId>commons-jxpath</artifactId>
      <version>1.3</version>
    </dependency>

    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>studying-spring-security-digest</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-digest」フォルダに移動後、以下の mvn コマンドを実行します。

cd c:\studying-spring-security-digest
mvn clean package
mvn tomcat7:run

テストがパスして war ファイルが生成されれば、とりあえず OK です。
実際に直接ブラウザから「http://localhost:8080/studying-spring-security-digest/」にアクセスしてみるのもよいと思います。
Controller の URL 「/」「/Home」「/Admin」「/UserDetails」を一般ユーザと管理者ユーザで呼出してコンソールログを確認してみる。
設定した有効期限(3秒)のタイミングで nc がリセットされ 新しい nonce に変わっているのも確認できると思います。


おわりに

テストコードの中で digestScheme.authenticate() メソッドを呼出して nc をインクリメントなどして
Authorization リクエストヘッダーを更新しているのですが、この呼出しをコメントアウトしてもテストは通ってしまいます。
一度受け入れたことのある Response ダイジェスト文字列が送られてきた場合、エラーにしてほしいような気がしますが…
リプレイ攻撃とか大丈夫なのだろうか?
とりあえず、nonce の有効期限は効いているようなので問題ないかな?
RESTful の要件は満たさないのかもしれないけど
Spring Security で WebAPI の認証をする場合、Cookie / Session が無難かも?
もしくは、OAuth の対応に期待か?!
Spring MVC Test Framework は、かなりいい感じだ。
いろいろテストできそうだ!

参考URL
http://static.springsource.org/spring-security/site/docs/3.1.x/reference/basic.html
http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/testing.html
http://www.baeldung.com/2011/11/20/basic-and-digest-authentication-for-a-restful-service-with-spring-security-3-1/
http://d.hatena.ne.jp/kuwalab/20130402/p1
http://x68000.q-e-d.net/~68user/net/http-auth-2.html
http://hc.apache.org/
http://code.google.com/p/json-path/
http://jackson.codehaus.org/
http://commons.apache.org/proper/commons-jxpath/

2013年4月22日月曜日

Samba4 で Active Directory / Member Server 編 - わたしもやってみた -

はじめに

今回は、Member Server を構築したいと思います。
そして 『Samba4 で Active Directory / PDC編- わたしもやってみた -』 で構築した
Primary Domain Controller に CentOS をドメイン参加させたいと思います。
ドメインコントローラーから見るとコンピューターアカウントになります。
前提としては、 Primary Domain Controller の構築が終わっていること
そして Samba4 と Bind のインストール、NTP の時間合わせが済んでいることとします。
まだの場合は、 『Samba4 で Active Directory / インストール編 - わたしもやってみた -』 を参考に

Samba4 HOWTO にある Domain_Member をベースに
先人の方々の情報も踏まえつつ、わたしもやってみたいと思います。
ではでは Getting Started!!

環境

Domain

レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN


Primary Server

OS ContOS 6.4(CentOS-6.4-i386-minimal.iso)
Samba 4.0.5
Bind 9.8.2
NTP ntp-4.2.6p5
Host pdc
IP 192.168.0.10
Gateway 192.168.0.1
DNS 127.0.0.1


Member Server

OS ContOS 6.4(CentOS-6.4-i386-minimal.iso)
VMware Player 5.0.2
Samba 4.0.5
Bind 9.8.2
NTP ntp-4.2.6p5
Host mbr01
IP 192.168.0.100
Gateway 192.168.0.1
DNS 192.168.0.10


環境設定

ネットワークを設定する。

まず IP の設定
/etc/sysconfig/network-scripts ディレクトリにある ifcfg-eth0 ファイルを以下のように変更する。

/etc/sysconfig/network-scripts/ifcfg-eth0
...
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.0.100
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
NETWORK=192.168.0.0
BROADCAST=192.168.0.255
...

次にホスト名の設定
/etc/sysconfig ディレクトリにある network ファイルを以下のように変更する。

/etc/sysconfig/network
NETWORKING=yes
NETWORKING_IPV6=no
HOSTNAME=mbr01

そして IP とホスト名の関連付け
/etc ディレクトリにある hosts ファイルを以下のように変更する。

/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.0.100 mbr01 mbr01.mydomain.local

今回は Primary Server の DNS を利用しようと思うので Bind を止めておく。

service named stop
chkconfig named off

そして DNS を Primary Server に設定する。
/etc ディレクトリにある resolv.conf を以下のように変更する。

/etc/resolv.conf
domain mydomain.local
nameserver 192.168.0.10

ここで一度、再起動する。

reboot

ホスト名と IP 確認する。

hostname
ping mbr01
ping mbr01.mydomain.local


Kerberos の設定

Kerberos の設定ファイル krb5.conf を差し替える。
とりあえず、今の設定をバックアップして

mv /etc/krb5.conf /etc/krb5.conf_org

そして以下のような内容で /etc ディレクトリに krb5.conf を作成する。

/etc/krb5.conf
[logging]
  default = FILE:/var/log/krb5libs.log
  kdc = FILE:/var/log/krb5kdc.log
  admin_server = FILE:/var/log/kadmind.log

[libdefaults]
  default_realm = MYDOMAIN.LOCAL
  dns_lookup_realm = true
  dns_lookup_kdc = true
  ticket_lifetime = 24h
  forwardable = yes

[appdefaults]
  pam = {
    debug = false
    ticket_lifetime = 36000
    renew_lifetime = 36000
    forwardable = true
    krb4_convert = false
  }

ここで ログインと チケットの一覧を確認しておく。
administrator のパスワードは、 Primary Domain Controller 構築の時に
samba-tool domain provision で指定したものになる。

kinit administrator@MYDOMAIN.LOCAL
klist


Samba4 の設定

Member Server の設定をする。
とりあえず、samba-tool domain provision を使って
smb.conf を生成することにした。
/usr/local/samba/etc ディレクトリに smb.conf が無いことを確認する。
有れば削除する。
では、以下のコマンドで、smb.conf を生成します。

/usr/local/samba/bin/samba-tool domain provision\
  --realm=MYDOMAIN.LOCAL \
  --domain=MYDOMAIN \
  --server-role=member \
  --dns-backend=NONE \
  --adminpass 'passwd-123'

生成された smb.conf を以下のように変更する。

/usr/local/samba/etc/smb.conf
[global]
  workgroup = MYDOMAIN
  security = ads
  realm = MYDOMAIN.LOCAL
  encrypt passwords = yes

  netbios name = MBR01
  server role = member server
  passdb backend = samba_dsdb
  server services = s3fs, rpc, nbt, wrepl, ldap, cldap, kdc, drepl, winbind, ntp_signd, kcc, dnsupdate

  idmap config *:backend = tdb
  idmap config *:range = 70001-80000

  #idmap config MYDOMAIN:backend = ad
  #idmap config MYDOMAIN:schema_mode = rfc2307
  #idmap config MYDOMAIN:range = 500-40000

  idmap config MYDOMAIN:backend = rid
  idmap config MYDOMAIN:range = 10000 - 19999

  #winbind nss info = rfc2307
  #winbind trusted domains only = no

  winbind use default domain = yes
  winbind enum users  = yes
  winbind enum groups = yes

  template homedir = /home/%U
  template shell = /bin/bash
smb.conf について

idmap config MYDOMAIN:backend = rid
RID で id と gid のマップングをすることにした。
MYDOMAIN ユーザの id と gid は、10000 ~ 19999 の範囲に
それ以外は、70001 ~ 80000 の範囲にマップングするようにした。
template homedir = /home/%U
Samba4 をドメインコントローラーで構成した場合、
%U がユーザ名に展開されず、/home ディレクトリ 直下に %U ディレクトリができしまったのだけど
Member Server の場合、これが問題なく展開された。
ユーザディレクトリは /home 以下に生成されます。

今回は、Samba4 を使わず Winbind を使うことになりそうなので
Samba4 を止めておく。

service samba4 stop
Winbind について

Member Server として、Samba4 を構成する場合、
直接 winbind を使うことになるようだ。
ソースは、Samba4 のものをビルドしているけど
これって Samba3 なの?

最後に Member Server としてドメイン参加する。

/usr/local/samba/bin/net ads join -U administrator


Winbind の設定

モジュールをコピーする。

cp /usr/local/samba/lib/libnss_winbind.so /lib
ln -s /lib/libnss_winbind.so /lib/libnss_winbind.so.2
ln -s /usr/local/samba/lib/security/pam_winbind.so /lib/security
ldconfig

この状態で authconfig を実行すれば Winbind の PAM 関連のファイルがいい感じに更新される。

authconfig --enablewinbindauth --update

もう一点、初回ログイン時にホームディレクトリが自動作成されるに authconfig しておく。

authconfig --enablemkhomedir --update

nsswitch.conf を編集する。
/etc ディレクトリにある nsswitch.conf を以下の部分を変更する。

/etc/nsswitch.conf
...
passwd: compat winbind
group:  compat winbind
...

やっぱり、起動スクリプトは、ほしいということで
yum でインストールされる samba 3.6.9 の Winbind の起動スクリプトを元に作成した。
変更したところは、smb.conf と winbindd のパスになる。
/etc/rc.d/init.d ディレクトリに 以下のような内容でファイルを作成し winbind の名前で保存する。

/etc/rc.d/init.d/winbind
#!/bin/sh
#
# chkconfig: - 27 73
# description: Starts and stops the Samba winbind daemon
#        #
# pidfile: /var/run/winbindd.pid
# config:  /usr/local/samba/etc/smb.conf


# Source function library.
. /etc/rc.d/init.d/functions

# Avoid using root's TMPDIR
unset TMPDIR

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ ${NETWORKING} = "no" ] && exit 1

# Check that smb.conf exists.
[ -f /usr/local/samba/etc/smb.conf ] || exit 6

[ -f /etc/sysconfig/samba ] && . /etc/sysconfig/samba

RETVAL=0


start() {
        KIND="Winbind"
 echo -n $"Starting $KIND services: "
 daemon /usr/local/samba/sbin/winbindd "$WINBINDOPTIONS"
 RETVAL=$?
 echo
 [ $RETVAL -eq 0 ] && touch /var/lock/subsys/winbindd || RETVAL=1
 return $RETVAL
} 

stop() {
        echo
 KIND="Winbind"
 echo -n $"Shutting down $KIND services: "
 killproc winbindd
 RETVAL=$?
 [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/winbindd
 echo ""
 return $RETVAL
} 

restart() {
 stop
 start
} 

reload() {
        echo -n $"Reloading smb.conf file: "
 killproc winbindd -HUP
 RETVAL=$?
 echo
 return $RETVAL
} 

rhstatus() {
 status winbindd
 return $?
} 

# Allow status as non-root.
if [ "$1" = status ]; then
       rhstatus
       exit $?
fi

# Check that we can write to it... so non-root users stop here
[ -w /usr/local/samba/etc/smb.conf ] || exit 4

case "$1" in
  start)
   start
 ;;
  stop)
   stop
 ;;
  restart)
   restart
 ;;
  reload)
   reload
 ;;
  status)
   rhstatus
 ;;
  condrestart)
   [ -f /var/lock/subsys/winbindd ] && restart || :
 ;;
  *)
 echo $"Usage: $0 {start|stop|restart|reload|status|condrestart}"
 exit 2
esac

exit $?

サービスに登録しておく。

chmod 755 /etc/rc.d/init.d/winbind
chkconfig --add winbind
chkconfig winbind on
smbd と nmbd について

Samba4 を Member Server として構成しファイル、プリンタ共有をする場合、
smbd と nmbd を直接使うことになるようだ。
うむむ… ここまでくると、Member Server の場合は、
yum で samba3 をインストールする方が無難なのか?


CentOS でドメインログイン

まず、Winbind を起動する。

service winbind start

とりあえず、ドメインユーザとグループを確認しておこう。

/usr/local/samba/bin/wbinfo -u
/usr/local/samba/bin/wbinfo -g

では!ドメインログイン!!

ssh administrator@mbr01

uid と gid のマッピングはキャッシュされるようで
マッピングを変えてもすぐに反映されない。
そんな時は、以下のようにキャッシュを確認して

/usr/local/samba/bin/net cache list

そして、削除!

/usr/local/samba/bin/net cache flush


おわりに

なんだかんだで Samba4 はいい感じになっていると思う。
けど今後、まだ、どんどん進化していきそう。
とり残されないようにウォッチしていこう。

参考URL
https://wiki.samba.org/index.php/Samba4
https://wiki.samba.org/index.php/Samba4/Domain_Member

2013年4月21日日曜日

Samba4 で Active Directory / BDC編 - わたしもやってみた -

はじめに

今回は、Backup Domain Controller を構築したいと思います。
そして 『Samba4 で Active Directory / PDC編- わたしもやってみた -』 で構築した
Primary Domain Controller にドメイン参加させたいと思います。
前提としては、Primary Domain Controller の構築が終わっていること
そして Samba4 と Bind のインストール、NTP の時間合わせが済んでいることになります。
まだの場合は、 『Samba4 で Active Directory / インストール編 - わたしもやってみた -』 を参考に

Samba4 HOWTO にある Join_a_domain_as_a_DC をベースに
先人の方々の情報も踏まえつつ、わたしもやってみたいと思います。
ではでは Getting Started!!

環境

Domain

レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN


Primary Server

OS ContOS 6.4(CentOS-6.4-i386-minimal.iso)
Samba 4.0.5
Bind 9.8.2
NTP ntp-4.2.6p5
Host pdc
IP 192.168.0.10
Gateway 192.168.0.1
DNS 127.0.0.1


Backup Server

OS ContOS 6.4(CentOS-6.4-i386-minimal.iso)
VMware Player 5.0.2
Samba 4.0.5
Bind 9.8.2
NTP ntp-4.2.6p5
Host bdc01
IP 192.168.0.11
Gateway 192.168.0.1
DNS 192.168.0.10


環境設定

ネットワークを設定する。

まず IP の設定
/etc/sysconfig/network-scripts ディレクトリにある ifcfg-eth0 ファイルを以下のように変更する。

/etc/sysconfig/network-scripts/ifcfg-eth0
...
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.0.11
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
NETWORK=192.168.0.0
BROADCAST=192.168.0.255
...

次にホスト名の設定
/etc/sysconfig ディレクトリにある network ファイルを以下のように変更する。

/etc/sysconfig/network
NETWORKING=yes
NETWORKING_IPV6=no
HOSTNAME=bdc01

今回は Primary Server の DNS を利用しようと思うので Bind を止めておく。

service named stop
chkconfig named off

そして DNS を Primary Server に設定する。
/etc ディレクトリにある resolv.conf を以下のように変更する。

/etc/resolv.conf
domain mydomain.local
nameserver 192.168.0.10
DNS について

今回、Primary Server に Primary Domain Controller と DNSを置いているけど
ホントは、別のサーバーにした方がいいのだろうと思う。
せっかくバックアップを構築していても Primary Server が落ちたら
DNS も死ぬので Backup Domain Controller もうまく動かなくなるのかなぁと思う。
とりあえず、データのバックアップは、しているのだし
自動フェイルオーバーまで考えなければ問題ないか?

ここで一度、再起動する。

reboot

Kerberos の設定

Kerberos の設定ファイル krb5.conf を差し替える。
とりあえず、今の設定をバックアップして

mv /etc/krb5.conf /etc/krb5.conf_org

そして以下のような内容で /etc ディレクトリに krb5.conf を作成する。

/etc/krb5.conf
[libdefaults]
  dns_lookup_realm = true
  dns_lookup_kdc = true
  default_realm = MYDOMAIN.LOCAL

ここで ログインと チケットの一覧を確認しておく。
administrator のパスワードは、 Primary Domain Controller 構築の時に
samba-tool domain provision で指定したものになる。

kinit administrator@MYDOMAIN.LOCAL
klist

Samba4 の設定

Backup Domain Controller の設定に入ります。
ここでのポイントは、smb.conf や その他諸々のファイルの生成に
samba-tool domain provision を使わないことです。
代わりに samba-tool domain join を使います。
/usr/local/samba/etc ディレクトリに smb.conf が無いことを確認する。
有れば削除する。
念のため Samba4 を停止しておく。

service samba4 stop

では、以下のコマンドで、smb.conf や その他諸々のファイルを生成します。

/usr/local/samba/bin/samba-tool domain join mydomain.local DC \
  -Uadministrator \
  --realm=mydomain.local \
  --dns-backend=BIND9_DLZ
オプションについて

--dns-backend=BIND9_DLZ を指定しているけど
今回は、Backup Server の DNS を使用していないので不要かも?
とりあえず Bind 関連のファイルを生成しておくことにした。

smb.conf は、以下のようになる。

/usr/local/samba/etc/smb.conf
# Global parameters
[global]
  workgroup = MYDOMAIN
  realm = mydomain.local
  netbios name = BDC01
  server role = active directory domain controller
  server services = s3fs, rpc, nbt, wrepl, ldap, cldap, kdc, drepl, winbind, ntp_signd, kcc, dnsupdate

[netlogon]
  path = /usr/local/samba/var/locks/sysvol/mydomain.local/scripts
  read only = No

[sysvol]
  path = /usr/local/samba/var/locks/sysvol
  read only = No


DNS の設定

Backup Server の IP、ホスト名を DNS に登録していきます。
まず、IP とホスト名の関連付けから

/usr/local/samba/bin/samba-tool dns add \
  192.168.0.10 \
  mydomain.local \
  bdc01 A 192.168.0.11 \
  -Uadministrator

登録できたか確認する。

host -t A bdc01.mydomain.local.

Backup Domain Controller の objectGUID とホスト名の関連付けもする。
ldbsearch で objectGUID を調べる。

/usr/local/samba/bin/ldbsearch \
  -H /usr/local/samba/private/sam.ldb \
  '(invocationid=*)' \
  --cross-ncs objectguid

ここで CN=BDC01 の record を探す。
objectGUID が 737506d0-bfe6-40c8-815d-08c3dff7a67f だった場合
以下のような感じで objectGUID とホスト名の関連付けをする。

/usr/local/samba/bin/samba-tool dns add \
  192.168.0.10 \
  _msdcs.mydomain.local \
  737506d0-bfe6-40c8-815d-08c3dff7a67f \
  CNAME bdc01.mydomain.local \
  -Uadministrator

登録できたか確認する。

host -t CNAME 737506d0-bfe6-40c8-815d-08c3dff7a67f._msdcs.mydomain.local.

一応 ホスト名と ping で IP の確認もしておこう。

hostname
ping bdc01
ping bdc01.mydomain.local


レプリケーションの確認

とりあえず、Primary Domain Controller のSamba4、Bind を再起動する。

ssh root@pdc
service named restart
service samba4 restart
exit

Backup Domain Controller のSamba4、も再起動する。

service samba4 restart

レプリケーションを確認する。

/usr/local/samba/bin/samba-tool drs showrepl

Primary Domain Controller で追加したドメインユーザが
Backup Domain Controller にレプリケートされるか確認する。
まず、Primary Domain Controller でドメインユーザを追加する。

ssh root@pdc
/usr/local/samba/bin/samba-tool user add user01
exit

そして Backup Domain Controller で確認する。

/usr/local/samba/bin/ldbsearch \
  -H /usr/local/samba/private/sam.ldb \
  samaccountname=user01


CentOS でドメインログイン

Backup Domain Controller の場合も
Primary Domain Controller と同じく
ドメインユーザー情報に posixAccount スキーマ情報を追加して、
Winbind と PAM の設定が必要になるようです。
このあたりは、 『Samba4 で Active Directory / インストール編 - わたしもやってみた -』 を参考に


2013年4月20日土曜日

Samba4 で Active Directory / PDC編 - わたしもやってみた -

はじめに

ここからが本番! Primary Domain Controller を構築したいと思います。
前提としては、Samba4 と Bind のインストール、 NTP の時間合わせが済んでいる済んでいることになります。
まだの場合は、 『Samba4 で Active Directory / インストール編 - わたしもやってみた -』 を参考に

Samba4 HOWTO にある Samba AD DC HOWTO をベースに
先人の方々の情報も踏まえつつ、わたしもやってみたいと思います。
ではでは Getting Started!!

環境

OS ContOS 6.4(CentOS-6.4-i386-minimal.iso)
Samba 4.0.5
Bind 9.8.2
NTP ntp-4.2.6p5
レルム MYDOMAIN.LOCAL
ドメイン MYDOMAIN
Host pdc
IP 192.168.0.10
Gateway 192.168.0.1
DNS 127.0.0.1


環境設定

ネットワークを設定する。

まず IP の設定
/etc/sysconfig/network-scripts ディレクトリにある ifcfg-eth0 ファイルを以下のように変更する。

/etc/sysconfig/network-scripts/ifcfg-eth0
...
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.0.10
NETMASK=255.255.255.0
GATEWAY=192.168.0.1
NETWORK=192.168.0.0
BROADCAST=192.168.0.255
...

次にホスト名の設定
/etc/sysconfig ディレクトリにある network ファイルを以下のように変更する。

/etc/sysconfig/network
NETWORKING=yes
NETWORKING_IPV6=no
HOSTNAME=pdc

そして IP とホスト名の関連付け
/etc ディレクトリにある hosts ファイルを以下のように変更する。

/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.0.10 pdc pdc.mydomain.local

ここで一度、再起動する。

reboot

ホスト名と IP 確認する。

hostname
ping pdc
ping pdc.mydomain.local


Samba4 の設定

さて… ドメインコントローラーの設定をしますかぁ
/usr/local/samba/etc ディレクトリに smb.conf が無いことを確認する。
有れば削除する。
Server Role は ドメインコントローラー とします。
バックエンドの DNS は Bind とします。
では、以下のコマンドで、smb.conf や その他諸々のファイルを生成します。

/usr/local/samba/bin/samba-tool domain provision \
  --use-rfc2307 \
  --realm=MYDOMAIN.LOCAL \
  --domain=MYDOMAIN \
  --server-role=dc \
  --dns-backend=BIND9_DLZ \
  --adminpass 'passwd-123'

オプションについて

ここでのミソは、--use-rfc2307 を指定しているところです。
これを指定しておくと CentOS で自身のドメインにログインするのに
必要な設定を smb.conf に出力してくれるようです。

とりあえず、生成された smb.conf を確認してみる。

/usr/local/samba/bin/testparm

Kerberos の設定ファイル krb5.conf を差し替える。

mv /etc/krb5.conf /etc/krb5.conf_org
cp /usr/local/samba/private/krb5.conf /etc/krb5.conf

krb5.conf は以下のような感じ。

/etc/krb5.conf
[libdefaults]
  default_realm = MYDOMAIN.LOCAL
  dns_lookup_realm = false
  dns_lookup_kdc = true

Bind の設定

/etc ディレクトリにある named.conf を開き以下の部分を追加、変更する。

/etc/hosts
options {
  ...
  listen-on port 53 { any; };
  ...
  allow-query     { any; };
  ...
  tkey-gssapi-keytab "/usr/local/samba/private/dns.keytab";
};
...
include "/usr/local/samba/private/named.conf";

dns.keytab の権限を変更する。

chgrp named /usr/local/samba/private/dns.keytab
chmod g+r /usr/local/samba/private/dns.keytab

/etc/rrndc.key を生成する。

rndc-confgen -a -r /dev/urandom

resolv.conf の設定をする。 /etc ディレクトリにある resolv.conf を開き以下のように変更する。

/etc/resolv.conf
domain mydomain.local
nameserver 127.0.0.1

サービスの起動設定をする。

chkconfig named on


確認

とりあえず、Samba4、Bind を再起動する。

service samba4 restart
service named restart

DNS を更新する。

/usr/local/samba/sbin/samba_dnsupdate --verbose --all-names

NTP と連動するために ntp_signd ディレクトリの権限を変更する。

chmod 750 /usr/local/samba/var/lib/ntp_signd

Samba4、Kerberos、Bind の設定を確認する。
まず、Bind から
以下のコマンドでホスト、IP が返ってくるか確認する。

host -t SRV _ldap._tcp.mydomain.local.
host -t SRV _kerberos._udp.mydomain.local.
host -t A pdc.mydomain.local.

ここでうまくいかない場合は、named.conf、resolv.conf、hosts を再確認して DNS を再更新してみる。

次に Kerberos の確認をする。
以下のコマンドで ログインと チケットの一覧を確認する。
administrator のパスワードは、samba-tool domain provision で指定したものになる。

kinit administrator@MYDOMAIN.LOCAL
klist

ここでうまくいかない場合は… う~ん krb5.conf かなぁ?
それとも smb.conf を削除して、Samba4 の設定からやり直し??

最後に Samba4 の確認をする。
以下のコマンドで ログインしてみる。
administrator のパスワードは、samba-tool domain provision で指定したものになる。
quit で終了できる。

/usr/local/samba/bin/smbclient //localhost/netlogon -Uadministrator

ここでうまくいかない場合は… 今回は無かったことに…


Windows XP でドメインログイン

Windows XP からドメインにログインしてみる。

コントロールパネル / ネットワーク接続 で DNS のIPを 192.168.0.10 (PDC)にする。
コントロールパネル / システム で コンピューター名タブを選択する。

  • 「変更」ボタンを押す。
  • ドメインに「MYDOMAIN.LOCAL」を入力し「OK」ボタンを押す。
    • ユーザ / パスワードは以下のものを入力する。
      • ユーザ名 :Administrator
      • パスワード:Samba4のAdministratorのパスワード

Windows を再起動する。
Windows のログイン画面でログイン先に MYDOMAIN を選択し Samba4 のAdministratorでログインする。


CentOS でドメインログイン

CentOS で自身のドメインにログインしてみる。

ssh administrator@pdc

これがうまくいかない。
さてさて、ここからが今回の山場!
Winbind と posixAccount の設定をしなくては、ならないようだ。

最初に Winbind と PAM の設定をする。
まずは モジュールのコピー

ln -s /usr/local/samba/lib/libnss_winbind.so.2 /lib/libnss_winbind.so
ln -s /lib/libnss_winbind.so /lib/libnss_winbind.so.2
ln -s /usr/local/samba/lib/security/pam_winbind.so /lib/security
libnss_winbind.so.2 と pam_winbind.so について

これらのモジュールがインストールされていない場合があった。
きっと、再 make になるのだろうけど その際は、configure.developer を使うのだろうか?
configure の時に --with-ads --with-shared-modules=idmap_ad オプションを
指定しておくと とりあえず、インストールはされた。
openldap-devel がインストールされていれば、
特にオプションを指定しなくてもインストールされるのかもしれない。
ビルドスクリプトを解析するば分かるのだろうけど、そこまでは…

この状態で authconfig を実行すれば Winbind の PAM 関連のファイルがいい感じに更新される。

authconfig --enablewinbindauth --update

もう一点、初回ログイン時にホームディレクトリが自動作成されるに authconfig しておく。

authconfig --enablemkhomedir --update

/etc/pam.d ディレクトリにある password-auth-ac、system-auth-ac
あたりがいい感じになっているはず。
合わせて /etc/nsswitch.conf を以下のように変更する。

/etc/nsswitch.conf
...
passwd:     files winbind
group:      files winbind
shadow:     files
...

次に posixAccount スキーマ情報をドメインユーザに追加する。
ドメインユーザでCentOSにログインするには、posixAccount スキーマ情報が必要になるようだ。
Active Directory に登録されているドメインユーザ情報には、これがない。
とりあえず、administrator に posixAccount スキーマ を追加して gidNumber と uidNumber を登録してみる。
gidNumber と uidNumber は、ドメインユーザ情報の primaryGroupID と objectSid から決定する。
Samba4 には、LDAP 関連のコマンドが含まれているのでこれを利用する。
まず administrator のドメインユーザ情報を表示して primaryGroupID と objectSid を調べる。

/usr/local/samba/bin/ldbsearch -H /usr/local/samba/private/sam.ldb samaccountname=administrator

次に /root/work ディレクトリあたりに addPosixAcount.ldif 的なファイルを以下の内容で作成する。

/root/work/addPosixAcount.ldif
dn: cn=Administrator,cn=Users,dc=mydomain,dc=local
changetype: modify
add: objectclass
objectclass: posixAccount
-
add: gidNumber
gidNumber: 10513
-
add: uidNumber
uidNumber: 10500
gidNumber と uidNumber について

gidNumber は、ドメインユーザ情報の primaryGroupID を元に
また、uidNumber は、ドメインユーザ情報の objectSid の下4桁(ハイフン「-」で区切られた最後の部分)を元にして
RID のルールを想定して 10000 ~ 19999 の範囲に割り振るようにした。
例えば primaryGroupID が 513 であれば、10513 に objectSid の下4桁が 500 であれば 10500 にという感じになる。

そして ldbmodify コマンドで addPosixAcount.ldif ファイルの内容を LDAP に反映する。

/usr/local/samba/bin/ldbmodify -H /usr/local/samba/private/sam.ldb /root/work/addPosixAcount.ldif
msSFU30XXX スキーマ情報

msSFU30XXX スキーマ情報の拡張がされていれば、posixAcount スキーマ情報を追加しなくても
CentOS でドメインにログインできるのかもしれない。
新規に追加したユーザーに posixAcount スキーマ情報を追加しなくてもドメインにログインできたからだ。
なぜか administrator では、無理だった。
わたしの検証環境は、msSFU30XXX スキーマ情報の拡張された状態になっていると思う。
いろいろ試行錯誤していて Windows のツールでもって msSFU30XXX スキーマ情報を拡張したと思っている。
いつかの時点までは、新規に追加したユーザーでも posixAcount スキーマ情報がなければ
ドメインにログインできなかったように思う。
それも、今となっては、定かではない。
とりあえず、全部の PC で uid と gid が統一できるので posixAcount スキーマ情報は、
追加しておく方がよいと思う。(まぁ gid は微妙ですが…)

smb.conf は、以下のようにしておく。

/usr/local/samba/etc/smb.conf
# Global parameters
[global]
  workgroup = MYDOMAIN
  realm = MYDOMAIN.LOCAL
  netbios name = PDC
  server role = active directory domain controller
  server services = s3fs, rpc, nbt, wrepl, ldap, cldap, kdc, drepl, winbind, ntp_signd, kcc, dnsupdate
  idmap_ldb:use rfc2307 = yes

  winbind use default domain = yes

  #template homedir = /home/%U
  template shell = /bin/bash

[netlogon]
  path = /usr/local/samba/var/locks/sysvol/mydomain.local/scripts
  read only = No

[sysvol]
  path = /usr/local/samba/var/locks/sysvol
  read only = No
smb.conf について

idmap_ldb:use rfc2307 = yes
この設定が漏れていたため、なかなかドメインログインできず結構ハマった。
samba-tool domain provision の時に --use-rfc2307 オプションを指定してさえいれば…
template homedir = /home/%U
%U がユーザ名に展開されず、/home ディレクトリ 直下に %U ディレクトリができしまう…
設定が抜けているのか ビルドに失敗しているのか バグ?なのか
とりあえず、触らぬ神に祟り無しでデフォルトのままにしています。
ユーザディレクトリは /home/MYDOMAIN 以下に生成されます。
gidNumber
なぜが gid に反映されない。
これもよく分かりません…
まぁサーバーごとに違う gid になる訳でないのでいいかな?

ドメインログイン前に Samba4 を再起動して

service samba4 restart

ログイン可能なユーザなんかを表示したりしてみて

getent passwd

ドメインログイン!!

ssh administrator@pdc

最後に uid と gid を確認しておこう。

id


おわりに

これで、Active Directory を学んでいくには十分過ぎる環境が揃ったと思う。
微妙なところがいくつか残ったがこのあたりは追々潰していこう。
新しいのと入れ替えると解消されるかなぁ。
なぁ~んて。

参考URL
https://wiki.samba.org/index.php/Samba4
http://wiki.samba.org/index.php/Samba4/HOWTO
https://wiki.samba.org/index.php/Samba4/Winbind
http://opentodo.net/2013/01/samba4-as-ad-domain-controller-on-centos-6/
http://wiki.samba.gr.jp/mediawiki/index.php
http://d.hatena.ne.jp/rti7743/20110425/1303688263
http://d.hatena.ne.jp/dicdak/20090628/1246206482
http://web.chaperone.jp/w/index.php?samba%2Fuser
http://itpro.nikkeibp.co.jp/article/COLUMN/20070125/259692/