Webアプリケーションの認証と承認について
自分の中での今のところのベストプラクティスを記述してみようと。
認証と承認
主体を特定する認証する処理と、その主体がある操作を実行してよいかどうかを承認する処理は分けて考える。
認証(Authentication)
リクエスト内の主体のIDとパスワードを使用して、その主体として認証する。
以下のような方法がある。
(1)javax.servlet.Filterを使って主体を認証する。
(2)TomcatのRealm機能を使って主体を認証する。
(2)の場合は承認処理も独自実装されているため、ここでの設計ガイドラインの適用外とする。軽量なアプリケーションであれば、(2)選択肢も現実的な解として検討する。
基本的にはどのような方法でも構わないが、その後の承認処理等の関係から、Filter実行時にはすでに主体の認証が済んでいることが望ましい。
認証処理のタイミングが遅れるほど、その他の処理の実行可能ポイントが後ろにずれ込むことになり、設計上の自由度も狭くなる。
認証された主体の情報は、セッション等の一時領域に格納する。以降の処理はこの一時領域に格納された主体情報を利用して行われる。認証処理と後続の処理(承認処理も含む)の間には、同一の主体情報を利用する以外に依存関係はない。
Filter動作イメージ例:
- 一時領域から(存在すれば)既存の主体情報を取得する。
- まだ主体が特定されていなければ、ログイン画面に遷移させる。[return]
- リクエスト内に主体のIDとパスワードを含む場合、その組み合わせが正しいかどうかチェックする。
- NGの場合、再度ログイン画面に遷移する。[return]
- 主体のIDを元に主体情報を取得する。
- 取得した主体情報をセッションに格納する。
- 後続のFilterへ処理を委譲する。
Filterの実装仕様に対するヒント:
- NG回数をインクリメントして回数に上限を持たせる方式も検討する。
- パスワードはDB上にそのまま格納しない。変換例 = (ユーザID + パスワード) * MD5ハッシュ
- 認証処理は、サービス層の実装クラスにそのまま委譲する。
- DIコンテナでFilterに対するDIが可能であれば、サービスインスタンスをFilterにDIする。(不可能であれば、直接newでも良い)
- 余裕があればSUN標準仕様であるJAAS認証について調査すると良い。ただし、個人的にはWebアプリケーションにおけるユーザの認証という用途にはあまり向かない気がするが、認証/承認の概念の下地として参考になる点が多い。
- コード上ではパスワードはbyte[]変数として保持すること。
クラス名に関するヒント:
- Filterクラス: AuthenticationFilter
- サービスクラス: Authenticator
- 主体情報: User
- DAOクラス: UserDAO
承認(Authorization)
認証によって特定された主体情報を用いて、現在のリクエストを実行する権限があるかどうかを承認する。
以下のような方法がある。
(1)javax.servlet.Filterを使って権限を承認する。
(2)TomcatのRealm機能を使って権限を承認する。
(3)バウンダリのコントローラやサービスオブジェクトに対するAOPで権限を承認する。
(2)の場合は承認処理も独自実装されているため、ここでの設計ガイドラインの適用外とする。軽量なアプリケーションであれば、(2)選択肢も現実的な解として検討する。
(3)よりも(1)の方が現状では現実的と思われるため、(3)についてはここでは触れない。
承認処理が行われないバウンダリの操作=全ユーザに実行可能、ということになるため、高いセキュリティが求められるシステムではFilterによる一括チェックで漏れなくカバーすると良い。
ここでいう承認とは、プレゼンテーション層の範囲で特定可能な操作単位に対する承認である。ロバストネス分析で言うバウンダリに対するアクセス権限ともいえる。より具体的にいうと、ある画面へ遷移してよいか?あるフォームボタンを押しても良いか?というレベルの承認である。
(例1)Strutsでいえば、あるActionクラスを実行できるかどうか、など
(例2)JSFでいえば、あるbacking-beanのメソッドを実行できるかどうか、など
たとえば、ある帳票オブジェクトの閲覧権限があるかどうか、などのビジネスロジックよりの承認処理が必要な場合は、別途、サービス層〜ビジネスロジック層で実施することになる。
Filter動作イメージ例:
- 一時領域から(存在すれば)既存の主体情報を取得する。
- まだ主体が特定されていなければ、実行権限不正画面に遷移させる。[return]
- リクエスト内から操作のIDを取得する。
- 主体と操作の関連情報(権限)をチェックする。
- NGの場合、実行権限不正画面に遷移する。[return]
- 後続のFilterへ処理を委譲する。
Filterの実装仕様に対するヒント:
- 先に認証Filterがかかっていることを想定しているため、承認Filter実行時にはすでに認証は済んでいるはず。つまり、この時点で主体情報が存在しないのはバグまたは何らかの異常事態である。
- 承認処理は、サービス層の実装クラスにそのまま委譲する。
- DIコンテナでFilterに対するDIが可能であれば、サービスインスタンスをFilterにDIする。(不可能であれば、直接newでも良い)
- たとえば、サブシステムという区分も承認ルールの1条件として必要である場合、Thread-Context Entityパターンを利用してサブシステム情報を承認Filterで取得する方法がある。この場合、承認Filterよりも先にサブシステム情報をコンテキストオブジェクトに設定するFilterをかけておく必要がある。
この方式をとる場合、対称性を考慮すると主体情報も同様に認証Filterとは別の独自Filterでコンテキストオブジェクトに設定するFilterを導入することも検討する。このような場合、主体情報とサブシステム情報は大抵サービス層でも利用することになるため、このコンテキスト設定Filterはどちらにしろ必要になってくるはず。
クラス名に関するヒント:
- Filterクラス: AuthorizationFilter
- サービスクラス: Authorizer
- 関連情報(権限): AuthorizationRule / Permission / AccessRule
- DAOクラス: AuthorizationRuleDAO / PermissionDAO / AccessRuleDAO
- コンテキスト設定Filter: SetUserContextFilter / SetSubsystemContextFilter