2009年11月5日木曜日

JAAS/AJDT/Eclipse

Eclipse 3.5(Galileo) に AspectJ 2.0.1 を入れたので、AspectJ による認証コードの分離を Eclipse上でやってみる。 以下のような流れで。
  1. 素のコンポーネントを作って、よそから自由に使える事を確認する。
  2. このコンポーンネントに権限チェックを追加して、勝手に使えなくする。
  3. 認証された権限保持者が正しく実行すると、コンポーネントが使えるのを確認。
  4. これらのプロジェクトを AspectJで リファクタする
■ 1 素のコンポーネントを作る
  • confidential-component という名前の Java Project を作る。
  • 以下のクラスを作る。
    package study.jaas_aop.component;
    public class ConfidentialComponent {
       public String summary() {
          return "summary";
       }
       public String detail() {
          return "detail";
       }
    }
  • client という名前の Java Projectを作る。
  • Java Build Path の Projectタブ で、confidential-componentプロジェクトへの参照を指定する。
  • もう一つ、以下のクラスを作る。
    package study.jaas_aop.client;
    
    import study.jaas_aop.component.ConfidentialComponent;
    
    public class App {
       public static void main(String[] args) {
          ConfidentialComponent comp = new ConfidentialComponent();
          System.out.println(comp.summary());
          System.out.println(comp.detail());
       }
    }
  • 実行して、"summary" "detail" と2行表示されるのを確認する。
まあ、ここまで当たり前。 ■ 2 パーミッションチェックを行う
  • confidential-component プロジェクトで、以下のようなパーミッションクラス MyPermission を作る
    package study.jaas_aop.component;
    import java.security.BasicPermission;
    
    public class MyPermission extends BasicPermission {
       public MyPermission(String name) {
          super(name);
       }
    }
  • ConfidentialComponent の summary()、detail() で checkPermission()する
    public String summary() {
       AccessController.checkPermission(new MyPermission("summary"));
       return "summary";
    }
    public String detail() {
       AccessController.checkPermission(new MyPermission("detail"));
       return "detail";
    }
  • 実行して、"summary"でアクセス例外が出るのを確認する
    Exception in thread "main" java.security.AccessControlException: access denied (study.jaas_aop.component.MyPermission summary)
勝手には使えないようになった。 ■ 3. 正当なクライアントが、正しい作法で呼び出す まずは呼び出し側の変更から解説して、そのためのログインモジュール(ダミー)を作ってみる。 ◆呼び出し側の変更
  1. summary(), detail() 両メソッドの呼び出しを PrivilegedAction でラップして、Subject.doAsPrivileged()経由で実行
  2. Subject は、LoginContext から得る
  3. Subject を返す前に、LoginContext の login() メソッドが実行されている必要がある。
まとめると以下のコードになる。
public static void main(String[] args) throws Exception {
   LoginContext lc = new LoginContext("study");
   lc.login();
   Subject subject = lc.getSubject();

   final ConfidentialComponent comp = new ConfidentialComponent();
   Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {
      @Override
      public Object run() {
         System.out.println(comp.summary());
         return null;
      }
   }, null);
   Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {
      @Override
      public Object run() {
         System.out.println(comp.detail());
         return null;
      }
   }, null);
}
LoginContext のコンストラクタに渡している文字列"study"は、ログイン設定ファイル内のエントリ名で、これをキーとしてログインモジュール(後述)が関連付けられる。ここではファイル名 my_login.config とし、client プロジェクトディレクトリ直下に置く。
study {
  study.jaas_aop.lm.DummyLoginModule required;
};
このログイン設定ファイが実行時に読み込まれるように、[Run Configuration]の[VM arguments]で以下のように指定する。
-Djava.security.auth.login.config=my_login.config
◆ログインモジュール作成 続いて上記ファイルで指定した、DummyLoginModule を作る。ただし、今回はクライアント目線中心でやってるので、本格的な認証ロジックは省略したダミーのログインモジュールとする。つまり LoginContext.login()が呼ばれると、常に固定 ユーザ"manager"がログイン成功するという筋書きとする。(後で"manager"を"worker"に変える。) プロジェクト名 login-module で Java Project を作成し、以下の2クラスを書く
package study.jaas_aop.lm;

import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

public class DummyLoginModule implements LoginModule {
   private static final String USER_NAME = "manager";
   private Subject subject = null;
   public void initialize(Subject subject, 
         CallbackHandler callbackHandler,
         Map<String, ?> sharedState, 
         Map<String, ?> options) {
      this.subject = subject;
   }
   public boolean login() {
      return true;
   }
   public boolean commit() throws LoginException {
      subject.getPrincipals().add(new DummyPrincipal(USER_NAME));
      return true;
   }
   public boolean abort() throws LoginException {
      return false;
   }
   public boolean logout() throws LoginException {
      return false;
   }
}
package study.jaas_aop.lm;

import java.security.Principal;

public class DummyPrincipal implements Principal {
   private String name;

   public DummyPrincipal(String name) {
      this.name = name;
   }
   public String getName() {
      return name;
   }
}
上記コードを簡単に説明すると、App.main() での LoginContext.login() 実行中に、DummyLoginModule のinitialize(), login(), commit()が順に呼ばれ、最終的に ユーザ"manager"に対応づく Principal が、Subject に関連付けられるという事になる。(注意:あくまでもダミーなので、ちゃんと本物の LoginModule を書くときの参考にはならない。) さて、これでログインモジュールができたが、"manager"が認証されたとして、何をする事が許されているのかを、さらに定義する必要がある。これは プロジェクトディレクトリ直下に security.policy ファイルを置いて、そこで ConfidentialComponent のメソッドへのアクセス権を指定する事にする。
grant Principal study.jaas_aop.lm.DummyPrincipal "manager" {
  permission study.jaas_aop.component.MyPermission "summary";
  permission study.jaas_aop.component.MyPermission "detail";
};
このsecurity.policyが実行時に読まれるようにするためには、VM argument で以下の指定を追加する。
-Djava.security.policy=security.policy
あと、[Configure Build Path] で、client プロジェクトから login-moduleプロジェクトへの参照も設定しておく。 以上で、準備ができたので動かしてみる。ちゃんと"summary"と"detail"の2行が表示されているはず。 ここで manager の他に、もう一人 worker というユーザもいる状況を試してみる。この人には summary()へのアクセスは許可されているが、detail()へのアクセスは許されていない。 security.policy中の manager プリンシパルの下に、以下を追記する。
grant Principal study.jaas_aop.lm.DummyPrincipal "worker" {
 permission study.jaas_aop.component.MyPermission "summary";
};
DummyLoginModule の USER_NAME を"worker"に変更して、再度試してみると、"summary"と表示された行の下に、AccessControlException がスタックトレースされ、"access denied (~~detail)"と言うメッセージが出ているはず。 とまあこんな感じでsecurity.policy ファイルの設定が効いているのが分かる。以後、security.policy の効き目がはっきりわかるように、USER_NAME は"worker"のままにしておく。 ■ 4. AspectJでリファクタする 認証コードを追加した結果、一番最初の App.main()と比べると、関心の異なるコードが入り乱れてかなりゴチャゴチャしている。これをまず直してみる。
  • client プロジェクトのコンテキストメニューから、[Convert to AspectJ Project]を実行する
  • 新規アスペクト"AuthAspect"を作成する。
    package study.jaas_aop.client;
    
    import java.security.PrivilegedAction;
    
    import javax.security.auth.Subject;
    import javax.security.auth.login.LoginContext;
    import javax.security.auth.login.LoginException;
    
    import study.jaas_aop.client.App;
    import study.jaas_aop.component.ConfidentialComponent;
    
    public aspect AuthAspect {
      private Subject subject;
    
      public pointcut upfontLoginPoint()
         :execution(void App.main(String[]));
    
      public pointcut authOperationCalls()
        :call(public * ConfidentialComponent.*(..));
    
      before() : upfontLoginPoint() {
        try {
          LoginContext lc = new LoginContext("study");
         lc.login();
         subject = lc.getSubject();
        } catch (LoginException ex) {
          throw new RuntimeException(ex);
        }
      } 
      Object around() 
        : authOperationCalls() && !cflowbelow(authOperationCalls()) {
          return Subject.doAsPrivileged(subject, 
            new PrivilegedAction<Object>() {
              public Object run() {  return proceed();  }
            }, null);
      }
    }
  • App.main()を、元の認証コードのないやつに戻す。
  • ユーザ"worker"で実行してみると、summary() 成功/detail()失敗となり、ちゃんと認証が利いているのがわかる。
ついでに confidential-component 側も AOP でリファクタしておく。
  • confidential-component プロジェクト上で[Convert to AspectJ Project]を実行
  • 新規アスペクト PermAspect を作成
    package study.jaas_aop.component;
    
    import java.security.AccessController;
    
    public aspect PermAspect {
      public pointcut authOperations()
        :execution(public * ConfidentialComponent.*(..));
     
      before() : authOperations() {
        String name = thisJoinPointStaticPart.getSignature().getName();
        AccessController.checkPermission(new MyPermission(name));
      }
    }
  • ConfidentialComponent の checkPermission() を呼んでいる行を削除して、一番最初のやつにもどす。
  • 実行して、挙動に変化が無く正しく動いている事を確認する。
以上、認証コード記述から AOP によるリファクタまで。 パッっと見、アスペクトのコードが多めに見えるかもしれないけど、コンポーネントの呼び出し箇所が増えたり、コンポーネント自体が複雑なロジックを持つようになったら、分離による効果が高くなるのはすぐ分かると思う。

0 件のコメント:

コメントを投稿