レベル: 初級 Scott Davis, Founder, ThirstyHead.com
2009年 4月 28日 Grails は、単純なログイン・インフラストラクチャーからロール・ベースの許可に至るまで、セキュアな Web アプリケーションを構築するために必要なあらゆる基本ビルディング・ブロックを提供します。連載「Grails をマスターする」では今回、Scott Davis が Grails アプリケーションをセキュアにするための実践的な方法を説明します。また、アプリケーションのセキュリティー機能を新たな方向へと展開させる上で役に立つプラグインについても学んでください。
今回の記事では引き続き Blogito という名前の「ごく簡易的なブログ」を作成します。前回の記事 (「カスタム URI とコーデックで Grails を作り直す」) では User をスタブ化しました。name フィールドは URI の最も重要な部分であるためです。今回はいよいよ、この User サブシステムを完全に実装します。ログインを有効にして User がログインしているかどうかによってその User の操作を制限する方法、さらに User のロールに応じた許可を設定する方法を学んでください。
まずは User が新しいエントリーを投稿できるように、ログインの手段が必要となります。
認証
複数のユーザーをサポートするブログ・サーバーでは、認証を行うのが賢い考えでしょう。偶然にでも故意にでも、例えば John Doe というユーザーが Jane Smith としてブログ・エントリーを投稿するといった事態は当然避けたいはずです。認証インフラストラクチャーをセットアップすれば、「ユーザーが何者か」という疑問は明らかになります。さらにこの後すぐに、ちょっとしたアクセス許可も行われるようにします。許可インフラストラクチャーを追加することによって、「ユーザーに許可される操作は何か」もはっきりとするからです。
リスト 1 は、前回作成した grails-app/domain/User.groovy ファイルです。
リスト 1. User クラス
class User {
static constraints = {
login(unique:true)
password(password:true)
name()
}
static hasMany = [entries:Entry]
String login
String password
String name
String toString(){
name
}
}
|
login フィールドと password フィールドは用意できているので、後はコントローラーとフォームを提供すればよいだけです。そこで、grails-app/controllers/UserController.groovy というファイルを新規に作成して、リスト 2 に記載するコードを追加します。
リスト 2. UserController に login、authenticate、および logout クロージャーを追加する
class UserController {
def scaffold = User
def login = {}
def authenticate = {
def user = User.findByLoginAndPassword(params.login, params.password)
if(user){
session.user = user
flash.message = "Hello ${user.name}!"
redirect(controller:"entry", action:"list")
}else{
flash.message = "Sorry, ${params.login}. Please try again."
redirect(action:"login")
}
}
def logout = {
flash.message = "Goodbye ${session.user.name}"
session.user = null
redirect(controller:"entry", action:"list")
}
}
|
空の login クロージャーは、ブラウザーで http://localhost:9090/blogito/user/login にアクセスするとgrails-app/views/user/login.gsp ファイルがレンダリングされることを意味するに過ぎません (このファイルはまもなく作成します)。
authenticate クロージャーは、便利な GORM メソッド findByLoginAndPassword() を使ってデータベース内で User を検索します。このメソッドは、その名前から想像できるように login および password の値と、フォーム・フィールドに入力されて params ハッシュマップによって計算された値とが一致する User を検索します。該当する User が見つかるとセッションに追加し、見つからない場合にはログイン・フォームにリダイレクトして User にもう一度、正しいクレデンシャルを入力する機会を与えます。logout クロージャーは User に別れを告げ、そのユーザーをセッションから削除してから EntryController の list アクションにリダイレクトします。
次に login.gsp を作成します。リスト 3 に記載するコードを手入力するか、あるいは以下の手順を使うこともできます。
- コマンドラインに
grails generate-views User と入力します。
- create.gsp を login.gsp にコピーします。
- コピーしたコードから不要な部分を削除します。
リスト 3. login.php
<html>
<head>
<meta name="layout" content="main" />
<title>Login</title>
</head>
<body>
<div class="body">
<h1>Login</h1>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<g:form action="authenticate" method="post" >
<div class="dialog">
<table>
<tbody>
<tr class="prop">
<td class="name">
<label for="login">Login:</label>
</td>
<td>
<input type="text" id="login" name="login"/>
</td>
</tr>
<tr class="prop">
<td class="name">
<label for="password">Password:</label>
</td>
<td>
<input type="password" id="password" name="password"/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="buttons">
<span class="button">
<input class="save" type="submit" value="Login" />
</span>
</div>
</g:form>
</div>
</body>
</html>
|
フォームの action が UserController.groovy 内のクロージャーの名前と同じく authenticate となっていることに注目してください。入力要素である login と password に含まれる名前は、authenticate クロージャーに含まれる params.login と params.password にそれぞれ対応します。
grails run-app と入力して、この認証インフラストラクチャーを試してみてください。ログイン名には jsmith、パスワードには foo と入力します (「カスタム URI とコーデックで Grails を作り直す」で、Blogito の grails-app/conf/BootStrap.groovy に数人のユーザーを追加したことを思い出してください)。すると、図 1 に示すように、ログインに失敗するはずです。
図 1. ログイン試行の失敗によるエラー・メッセージ
今度は同じ jsmith をログイン名として使用し、パスワードには wordpass と入力してみてください。これだったらログインできるはずです。
grails-app/views/entry/list.gsp にウェルカム・メッセージが表示されない場合は (表示されないはずです)、login.gsp の <g:if test="${flash.message}"> ブロックをそのまま list.gsp ファイルの先頭にコピーします。その上でもう一度 jsmith としてログインし、図 2 に示すメッセージが表示されることを確認します。
図 2. ログインの成功を確認するフラッシュ・メッセージ
これで認証が機能することを確認できたので、次はログイン/ログアウトを容易にする TagLib を作成します。
許可のための TagLib の作成
Google や Amazon のような Web サイトでは、ヘッダーに控えめなテキスト・リンクがあり、そこからログインおよびログアウトができるようになっています。これと同じことを、Grails ではわずか数行のコードで実現することができます。
まず、コマンド・プロンプトで grails create-tag-lib Login と入力します。このコマンドによって新しく作成された grails-app/taglib/LoginTagLib.groovy に、リスト 4 のコードを追加します。
リスト 4. LoginTagLib.groovy
class LoginTagLib {
def loginControl = {
if(session.user){
out << "Hello ${session.user.name} "
out << """[${link(action:"logout", controller:"user"){"Logout"}}]"""
} else {
out << """[${link(action:"login", controller:"user"){"Login"}}]"""
}
}
}
|
次に、リスト 5 に示す新しい <g:loginControl> タグを grails-app/views/layouts/_header.gsp に追加します。
リスト 5. ヘッダーに <loginControl> タグを追加する
<div id="header">
<p><g:link class="header-main" controller="entry">Blogito</g:link></p>
<p class="header-sub">A tiny little blog</p>
<div id="loginHeader">
<g:loginControl />
</div>
</div>
|
仕上げとして、リスト 6 に記載するわずかな loginHeader <div> 用 CSS フォーマット設定のコードを web-app/css/main.css に追加します。
リスト 6. loginHeader <div> の CSS フォーマット設定
#loginHeader {
float: right;
color: #fff;
}
|
Grails を再起動してから jsmith としてログインすると、図 3 に示すような画面が表示されます。
図 3. ログイン TagLib の動作
基本的な許可
Blogito がユーザーを認識するようになったので、次のステップではユーザーが実行できる操作を制限します。例えば、Entry は誰でも読めるようにしなければなりませんが、Entry の作成、更新、削除については、ログインしたユーザーにだけ許可しなければなりません。このような制限を可能にするために Grails には beforeInterceptor が用意されています。その名前からわかるように、このインターセプターは対象のクロージャーが呼び出される前に操作を許可する、理想的なフックです。
EntryController にリスト 7 のコードを追加してください。
リスト 7. EntryController に許可を追加する
class EntryController {
def beforeInterceptor = [action:this.&auth, except:["index", "list", "show"]]
def auth() {
if(!session.user) {
redirect(controller:"user", action:"login")
return false
}
}
def list = {
//snip...
}
}
|
auth と list の微妙ながらも重要な違いは、list はクロージャーであり、一方の auth は private メソッドであるという点です (クロージャーは定義で等号を使用しますが、メソッドは括弧を使用します)。クロージャーはエンド・ユーザーに URI として公開される一方、メソッドにはブラウザーからアクセスすることはできません。
auth メソッドは、User がセッションに入っているかどうかをチェックします。入っていなければログイン画面にリダイレクトし、false を返して元のクロージャー呼び出しをブロックします。
クロージャーが呼び出される前には必ず、beforeInterceptor が auth メソッドを呼び出します。このアクションは Groovy 表記に従い、アンパサンド (&) 記号を使用して this クラスの auth メソッドを指します。auth の呼び出しが適用されないクロージャーは、except リストに指定されています。少数のクロージャー呼び出しだけをインターセプトしたい場合には、この except を置き換えることができます (beforeInterceptor についての詳細は、「参考文献」を参照してください)。
Grails を再起動して、beforeInterceptor をテストしてください。ログインしないで http://localhost:9090/blogito/entry/create にアクセスしようとすると、ログイン画面にリダイレクトされます。jsmith としてログインしてもう一度アクセスすると、今度は問題なく新しい Entry を作成できるはずです。
きめ細かな許可
beforeInterceptor で大まかな許可を行うのがまず先決ですが、許可用のフックは個々のクロージャーに追加することもできます。例えば、現状ではオリジナルの作成者のみならず、ログインした User の誰もが Entry を編集することができます。このセキュリティー・ホールを修正するには、EntryController.groovy の edit クロージャーの適切な位置に、4 行のコードを追加するという方法があります。
リスト 8. edit クロージャーに許可を追加する
def edit = {
def entryInstance = Entry.get( params.id )
//limit editing to the original author
if( !(session.user.login == entryInstance.author.login) ){
flash.message = "Sorry, you can only edit your own entries."
redirect(action:list)
}
if(!entryInstance) {
flash.message = "Entry not found with id ${params.id}"
redirect(action:list)
}
else {
return [ entryInstance : entryInstance ]
}
}
|
上記と同じ 4 行のコードで、delete および update クロージャーを制限することもできます (もちろんそうすべきです)。同じコードを何度もコピー・アンド・ペーストすることを考えると嫌悪感を覚える場合には (そうであるべきです)、private メソッドを 1 つ作成して、3 つのクロージャーすべてでそのメソッドを呼び出すこともできます。また、多くのコントローラーで同じ beforeInterceptor と private メソッドを使っていることがわかったら、Java クラスで一般的に行うように、共通の振る舞いを抜き出して 1 つのマスター・コントローラーに組み込み、他のコントローラーにマスター・コントローラーを拡張させるという手もあります。
許可インフラストラクチャーをさらに堅牢にするためには、もう 1 つ追加できるものがあります。それはロールです。
ロールの追加
ロールを User に割り当てると、User をグループ化するのに便利です。グループ化した後は、個々にではなくグループに権限を割り当てることができます。例えば、今のところは誰でも新しい User を作成することができます。つまり、その個人がログインしているかどうかをチェックするだけでは十分セキュアとは言えないので、User アカウントを管理する権限を管理者だけに与えたいと思います。
リスト 9 では role フィールドを User に追加するとともに、このフィールドの値を author または admin のいずれかに制限しています。
リスト 9. User に role フィールドを追加する
class User {
static constraints = {
login(unique:true)
password(password:true)
name()
role(inList:["author", "admin"])
}
static hasMany = [entries:Entry]
String login
String password
String name
String role = "author"
String toString(){
name
}
}
|
role はデフォルトで author に設定されることに注目してください。inList 制約は有効な 2 つの選択肢しかないコンボ・ボックスを表示します。図 4 に、この様子を示します。
図 4. 新しいユーザー・ロールを author または admin のいずれかに制限する
grails-app/conf/BootStrap.groovy に User として admin を作成します (リスト 10 を参照)。author という role は、忘れずに既存の 2 つの User の両方に追加してください。
リスト 10. User として admin を追加する
import grails.util.GrailsUtil
class BootStrap {
def init = { servletContext ->
switch(GrailsUtil.environment){
case "development":
def admin = new User(login:"admin",
password:"password",
name:"Administrator",
role:"admin")
admin.save()
def jdoe = new User(login:"jdoe",
password:"password",
name:"John Doe",
role:"author")
//snip...
def jsmith = new User(login:"jsmith",
password:"wordpass",
name:"Jane Smith",
role:"author")
//snip...
break
case "production":
break
}
}
def destroy = {
}
}
|
最後にリスト 11 のコードを追加して、すべての User アカウントの操作を admin ロールを持つユーザーに制限します。
リスト 11. User アカウントの管理を admin ロールに制限する
class UserController {
def beforeInterceptor = [action:this.&auth,
except:["login", "authenticate", "logout"]]
def auth() {
if( !(session?.user?.role == "admin") ){
flash.message = "You must be an administrator to perform that task."
redirect(action:"login")
return false
}
}
//snip...
}
|
ロール・ベースの許可をテストするため、jsmith としてログインし、http://localhost:9090/blogito/user/create. にアクセスしてみてください。図 5 に示すログイン画面にリダイレクトされるはずです。
図 5. 非管理者に対してはアクセスがブロックされます
今度は admin ユーザーとしてログインします。すると、すべてのクロージャーにアクセスできるはずです。
プラグインによる次のレベルへの展開
この「ごく簡易的な」ブログ・アプリケーションのための「ごく簡易的な」認証および許可システムが配備されました。ここから新しい方向に展開させるのは簡単です。例えば、User に許可するアカウント管理は自分のアカウントだけで、他のアカウントは管理できないようにする必要があります。また、admin は自分の Entry だけでなく、すべての Entry を編集できなければなりません。いずれの場合にしても、コード行を戦略的にいくつか配置するだけで新しい機能を追加することができます。
単純なのは、機能が足りないせいだと勘違いされることがよくあります。Blogito は今でも 200 行足らずのコードですが、単体テストと結合テストが組み込まれています。これを確認するには、コマンドラインに grails stats と入力してください。その結果はリスト 12 のとおりですが、Blogito が複雑ではないからと言って、活躍する準備が整っていないというわけではありません。
リスト 12. 「ごく簡易的な」アプリケーションのサイズ
$ grails stats
+----------------------+-------+-------+
| Name | Files | LOC |
+----------------------+-------+-------+
| Controllers | 2 | 95 |
| Domain Classes | 2 | 32 |
| Tag Libraries | 2 | 21 |
| Unit Tests | 5 | 20 |
| Integration Tests | 1 | 10 |
+----------------------+-------+-------+
| Totals | 12 | 178 |
+----------------------+-------+-------+
|
この連載の最初の記事から私が目標としているのは、コアの Grails そのものに備わった機能と、Groovy 言語の簡潔な表現力を皆さんに証明することです。例えば、Grails でのコーデックをいったん理解すれば、パスワードを平文で表示する代わりに、データベースに保管されたパスワードを簡単にハッシュ化することができます (HashCodec についての詳細は、「参考文献」を参照してください)。grails-app/utils/HashCodec.groovy を作成して、リスト 13 のコードを追加してください。
リスト 13. 単純な HashCodec の作成
import java.security.MessageDigest
import sun.misc.BASE64Encoder
import sun.misc.CharacterEncoder
class HashCodec {
static encode = { str ->
MessageDigest md = MessageDigest.getInstance('SHA')
md.update(str.getBytes('UTF-8'))
return (new BASE64Encoder()).encode(md.digest())
}
}
|
HashCodec が用意できたら、後は単純に UserController の login、save、update クロージャーで User.password への参照を User.password.encodeAsHash() に変更するだけです。驚くべきことに、このたった 10 行のコードで、アプリケーションにまったく新しいレベルの機能が加わります。
しかし、少ないコードによるメリットもある時点で限界に達するときがきます。そうした場合、Grails では「作成するか、それとも買うか?」といった典型的な質問ではなく、「作成するか、それともプラグインをダウンロードするか?」といった質問がされます。http://grails.org/plugin/list#security+tags には、grails install-plugin と入力するだけで認証および許可の問題の解決を試みる数々のプラグインが用意されているのです。
例えば、Authentication プラグインが提供する便利な機能の 1 つを使えば、admin にアカウントを作成させる代わりに、User がアカウントにサインアップできるようになります。さらに、「この E メール・アドレスを使用して新規ユーザー・アカウントが作成されました。このリンクをクリックして新規アカウントを確認してください」という確認メッセージを User に送信するようにプラグインを構成することもできます。
OpenID プラグインは、これとは別の方法をとります。それは、エンド・ユーザーに当然忘れてしまうような別のユーザー名とパスワードの組み合わせを作らせる代わりに、ユーザーが選択する OpenID プロバイダーに認証を委任するという方法です。LDAP (Lightweight Directory Access Protocol) プラグインもこれと同じような方法を使って、Grails アプリケーションで既存の LDAP インフラストラクチャーを活用できるようにします。
Authentication プラグインと OpenID プラグインはどちらも認証という枠に限られていますが、許可のソリューションを提供するプラグインもあります。JSecurity プラグインはセキュリティー・フレームワーク全体を利用して、User、Role、Permission といったドメイン・クラスのボイラープレート・コードを提供します。また、Spring Security (旧称 Acegi Security) ライブラリーを利用する Spring Security プラグインでは、既存の Spring Security の知識とソース・コードを再利用することができます。
アプリケーションの要件はそれぞれに大きく異なるため、Grails にはこのように数々の認証および許可ストラテジーが用意されています。機能がステップアップするたびに、アプリケーションの複雑性も同じだけ増していくのはやむを得ません。私は上記で取り上げたプラグインの多くを本番のアプリケーションで使ったことがありますが、それは必ず、私が最初に紹介した単純な手作りのストラテジーよりもプラグインを使用するメリットが上回っていることを確信した上でのことです。
まとめ
Blogito はこれでセキュアになりました。User にはログイン/ログアウトの手段があるだけでなく、今回作成した LoginTagLib のおかげで、ログインまたはログアウトするための便利なリンクも用意されています。認証を検証する EntryController の beforeInterceptor で説明したように、場合によっては単にアプリケーションへのログインを用意するだけで十分なセキュリティーになりますが、それだけでは不十分な場合には、ロールを追加することで許可の形式を一層強力にします。単純なロールを User に追加することで、ユーザー管理へのアクセスを管理者だけに制限できるようになります。
Blogito がセキュアになったので、連載「Grails をマスターする」の次回の記事では、目の前に控えている主要なタスクに専念することができます。そのタスクとは、認証済みユーザーがファイルをアップロードするための方法、そしてエンド・ユーザーが Atom フィードを購読するための方法を提供することです。このタスクが完了すれば、Blogito は正真正銘のブログ・アプリケーションになります。それでは、次回の記事まで Grails を楽しみながらマスターしてください。
参考文献 学ぶために
製品や技術を入手するために
- Grails: Grails の最新リリースをダウンロードしてください。
- Grails に用意された以下のセキュリティー・プラグインを調べてください。
- Blogito: Blogito アプリケーションの完成版をダウンロードできます。
議論するために
著者について
記事の評価
|