ちゃっぴさんのblogに変な返答をしてしまったので、言い出しっぺの法則に基づき、方法論を考えてみました。
テーマはあるWebアプリケーション(以下、システムと言う場合はアプリケーション全体を指す)があったとして、
そのシステム全体でデータに対するアクセス権を管理するためにはどうしたらよいだろうか?です。
ここではOOP(オブジェクト指向)でいうObject単位での権限管理を考えます。
これらのObjectはRDBMSなどで管理されるような永続性のあるデータを想定しています。
ユーザの保有する権限の管理
例えば、システム内でプロジェクトを表現するクラスProjectがあったとして、
参照、新規、更新、削除の4つの権限を持つとしましょう。
さらに、Projectのインスタンス単体でこれらの権限が設定できるとしたならば、
ユーザ側の情報に、どのProjectに対して何権限を持つのかの情報を持たなくてはなりません。
まず、権限の種別を表す列挙を用意します。
public enum AuthorityType {
/** 参照権 */
SELECT,
/** 登録権 */
INSERT,
/** 更新権 */
UPDATE,
/** 削除権 */
DELETE,
}
そして、権限管理クラスでは対象となるオブジェクトのID・型とその権限を保持するようにします。
今回は簡単のためIDによって対象データを特定する方式をとっています。
一般的なWebシステムを想定するとすれば、このユーザの権限情報はセッションに保持されます。
このオブジェクトをProjectオブジェクトを操作するたびに権限確認のために引き渡すのはデータの取り回しがあまりに大変なので
HTTPの1リクエストごとに、1つのThreadが立てられることを利用してjava.lang.ThreadLocalに
この権限情報を格納して利用するとしましょう
。
ThreadLocalはそのThread内だけをスコープとするグローバル変数のようなものとイメージしてもらうと大体当たっていると思います。
/**
* ユーザが保持する権限を管理するクラス
* ユーザ単位で1インスタンスが作られる。
*/
public class AuthorityManager {
/** ThreadLocalでThread内をスコープとするグローバル変数を作る */
private static ThreadLocal<AuthorityManager> manager = new ThreadLocal<AuthorityManager>();
private static final String SESSION_KEY = "AuthorityManager";
/** セッションから権限情報を取得してThreadLocalに設定する */
public static void initManager(HttpServletRequest request) {
HttpSession session = request.getSession();
AuthorityManager auth = (AuthorityManager) session.getAttribute(SESSION_KEY);
manager.set(auth);
}
/** ThreadLocalから権限情報を解放する */
public static void releseManager() {
manager.remove();
}
/** 保有権限のList */
private List<Tips> authList;
static class Tips {
/** 対象オブジェクトのID */
int id;
/** 対象オブジェクトの型 */
Class objectType;
/** 権限タイプ */
AuthorityType type;
}
}
/**
* HTTPのリクエスト毎に権限情報を設定するFilter
*/
public class AuthorityInitFilter implements Filter {
@Override
public void init(FilterConfig config) throws ServletException {}
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 権限情報をThreadLocalに設定
HttpServletRequest httpRequest = (HttpServletRequest)request;
AuthorityManager.initManager(httpRequest);
try {
// 実際のServletの処理
chain.doFilter(request, response);
} finally {
// 権限情報をThreadLocalから解放する
AuthorityManager.releseManager();
}
}
}
実装は省いていますが、保有データからユーザ側に保有権限のListを持たせ、権限認証の際に割符のように
操作対象となるオブジェクトと付き合わせようという思惑なのをイメージして頂けますでしょうか。
権限チェックを漏れがないように執り行う
さて、Thradにアクセス権限の情報を持ちました。
あとは対象となるProjectオブジェクトから権限のある場合のみデータが参照できるように網羅しなくてはなりません。
これは、Projectオブジェクトへのアクセス全てに対して
「権限チェックをし権限のないアクセスであれば例外を投げるという処理」
を差し挟むということですから、AOP(アスペクト指向)で言う「横断的関心事」となります。
さて、この横断的関心事ですが、AOP対応の言語であれば、言語機能を用いてウィービング
(アスペクトはWeaving、縫い付けると表現します)すればよいのでしょうが
ご承知のとおりJavaはそうではないので、なんらかの手段を講じなければなりません。
ProjectオブジェクトがDIコンテナによって管理されている場合、DIコンテナのAOP機能でウィービングする手が使えるでしょう。
DIコンテナは現行の非AOPな言語でAOPを実践するための工夫と捉えることもできます。
DIコンテナを用いていなくとも、Projectオブジェクトの取得する口が特定できるならそこを抑えてProxyを噛ませる手が使えます。
GoFデザインパターンのProxyパターンはOOPの範囲で出来るAOPへの対処法とも言えます
。
場合によってはダサいですがProxyクラスさえも用意せず、Projectオブジェクトの各メソッドに
権限チェック処理を直接実装する手法を用います。
横断的関心事を分離できていないわけですが、setter、getterなんぞそもそも処理が希薄なので割りきることもできることでしょう。
注意点として、Proxyパターンや、直接Projectオブジェクトに権限チェックを実装するような場合、メソッドの網羅は保証できません。
なんせ、手で個別のメソッドに対応を差し挟むわけですから。
機械的な網羅ではないのでヒューマンエラーで容易に漏れてしまいますし、なにより記述が面倒です…。
もちろん、Projectクラス自体をアーキテクト管理下に置いて、勝手に改変されないような状況である必要があります。
ここまですると、開発時に参照件のないProjectオブジェクトを取得して、その情報を参照しようとした時点で
実行時例外を起こすことができるようになるわけです。
もちろん、処理効率的にはよくないわけですが、セキュリティ的にはいくらかの安全性を担保できることでしょう。
まとめ
設計パラダイムとしては、まず該当オブジェクトへの操作に対する権限チェックをアスペクトとして捉える事。
また、これをうまくウィービングして全ての参照/設定メソッドに網羅させること。
そして、処理の実行体であるThreadと、操作対象であるオブジェクト
(上記例ではProjectオブジェクト)との間の権限確認を行います
。
Threadを用いないでオブジェクトを操作することはできないですし、アスペクト指向によってオブジェクトに対するアクセス全てに
権限確認処理をウィービングすることで、ヒューマンエラーによる誤操作をすべてはじき出すわけです。
また、悪意ある故意の操作に対しても処理をはじくことがある程度できます。
もっともThreadの持つ権限管理データまで改ざんされるとアウトなわけですが…。
なお、ここで挙げた設計方法は、1案に過ぎません。よりよい方法論もあることでしょう。
この方法論を試すのは自由にしていただいて構いませんが、
私はこの設計方法を採用して発生したいかなる損失に対してもなんらの責任は負わないものとします。
注釈
*1元のコメントではシステムの範囲を誤解させてしまいました… orz
*2
RDBMSでのプライマリキーとなることも考慮してIDによって対象となるObjectを対応付けていますが、
本当はProjectオブジェクトへの参照によって対応付けを行いたいところです。
Projectオブジェクトの数が限られ、システム全体で完全にキャッシュされるようなケースであれば容易でしょう。
全てキャッシュできるほどにProjectオブジェクトが少ないわけではないとすれば、
Projectオブジェクトの参照は同一に遅延ロードを実装する手法も考えられます。
なお、アクセス速度を考えるとListよりはMapなどの構造を取った方が適切でしょう。
このあたりは実際のデータを鑑みて考慮する必要があります。
*3
ただし、通常ServletではThreadがプールされるため、HTTPのリクエストの処理開始で格納し、
処理終了時には削除するようにしておかなければいけません。
これはjavax.servlet.Filterで処理することで漏れなく確実な処理を行うことができます。
*4
アスペクトがDIのすべてとは言いませんが、AOP対応言語ではDIコンテナを使う意味があまりないはずです。
DIはコンストラクタに対するアスペクトとして実装することができてしまう。
強いて言えば、設定ファイルを読み込んでリフレクションで設定を行うところが面倒なので、
そのあたりをやってくれる程度には便利と思いますが。
*5
例えば、Projectオブジェクトを取得するためのサービスが限定されているようなケースでは、
その出口を抑えて、GoFデザインパターンのProxyパターンを適用すればよいのです。
Proxyクラスの実装はProjectオブジェクトを継承し、全機能をProjectに委譲するようにしておきます。
ただし、getterメソッド全てに参照件のチェックを差し挟むし、setter全てに更新件のチェックを差し挟むわけです。
*6
ProxyパターンでAOPをする場合の最大の障壁は、オブジェクトの生成箇所をどのようにして抑えるかという点。
new演算子で生成されると手の打ちようがありません。
生成に関するデザインパターンなどを用いて、オブジェクトの生成部を一元管理できているようなケースではどうにかなります。
DIコンテナも生成をDIコンテナが一身に背負うことでProxyパターンによるAOPを成し遂げているわけです。
*7
変更する人が状況をよく理解している少数であったとしても、修正に際してAOPの対応漏れは容易に発生します。
誰もが変更できる状況下では維持するためのコストは膨大になることでしょう。
*8
1Threadあたり1Userという状況下である必要があります。
世の中にはあるいは1Threadあたり複数userとなるシステムもあるかもしれませんが、
そういうケースではこの手法ではうまくいかないことになってしまいます。
投稿日時 : 2008年5月13日 0:22