目的
Warden による 認証を行う際のポイントを押さえることを目的とします。
そのためのサンプルの紹介と解説を行います。
Warden は認証の仕組みを提供してくれるもので、認証の方法自体は自分で実装する必要があります。
Rack::Auth による認証とは異なり、認証したユーザの情報はクッキーに保存されます。
クッキーに保存するユーザの情報の制御が可能です。Rack::Auth がサポートしないログアウトも実現できます*1。
Warden は基盤として使われることが多く、Warden を基盤にユーザの管理を含めた機能を提供してくれるライブラリもありますが、 本ページでは直接 Warden を使って認証を実現する方法を説明します。
本サンプルでは Web アプリケーションフレームワークに Sinatra を使っています。
必要なポイントは各節にある表を見るだけで把握できるようにまとめています。詳細が知りたい場合は、さらに周辺の説明文を読んでください。
サンプル
サンプルのダウンロード
以下のファイルをダウンロードし、解凍してください。
以下のファイルが入っています。
- app.ru
- Web アプリケーションを起動するプログラム。Rack 用のファイル。Warden Rack Middle の組み込みを行なっている。
- app.rb
- Web アプリケーション本体。
- helpers.rb
- Warden の機能へのアクセスを簡単にするためのヘルパーメソッドの定義。
- warden_strategies.rb
- ストラテジー(=認証方法)の定義。
- user.rb
- 仮想的なユーザ情報の定義。
- views フォルダ(Web 画面の各種テンプレート)
- layout.erb
- 全ての画面共通のレイアウトを定義(head および body)。
- login.erb
- ログイン画面の定義。
- unauthenticated.erb
- ログイン失敗時画面の定義。
- top.erb
- ログインできた場合のトップページ画面の定義。ログインユーザ情報の表示を行う。
- layout.erb
サンプルの実行
サンプルを動かすには事前に Warden をインストールする必要があります。 まだ、Warden をインストールしていない場合は、以下のコマンドを実行してください。
gem install warden
サンプルを動かすには、以下のコマンドを実行してください。
rackup app.ru
ポート 9292 で Web サーバが起動すると思うので「http://localhost:9292/」にアクセスしてください*2。
サンプルアプリケーションの概要
サンプルアプリケーションの画面遷移を示します。
最初、ルートにアクセスすると未ログイン状態なので、ログイン画面へと遷移します。
入力画面では、メールアドレスとパスワードの入力ができます。
user.rb にて簡易なハッシュによるユーザ管理定義をしており、以下の組み合わせによるログインが可能です。
ユーザ名 | パスワード |
taro | password1 |
jiro | password2 |
saburo | password3 |
ログインに成功するとユーザ情報表示画面へと遷移し、ログインしたユーザの情報
- 名前
- メールアドレス
- 年齢
が表示されます。
これらの情報はセッションからアクセスできるようになり、セッションの有効期限が失効するか、ログアウトするまでの間、いつでも参照できるようになります。
解説
認証の定義
Warden による認証機能の有効化
app.ru の以下の定義がそれに当たります。
require 'warden' # ... enable :sessions use Warden::Manager do |manager| manager.default_strategies :custom_login_strategy manager.failure_app = Sinatra::Application end
enable :sessions および use は、Sinatra の機能です。以下の役割があります。
文 | 役割 |
enable :sessions | Sinatra のセッション機能を有効にします。Warden は認証したユーザの情報をセッションで管理するため、セッション管理機能は必須です。 |
use | Rack Middleware をアプリケーションに組み込む機能です。Warden は Rack Middleware です。組み込みは必須です。 |
use Warden::Manager ブロックの中で以下の2つを定義する必要があります。
定義値 | 役割 |
default_strategies | 利用するストラテジー(=認証方法)を決めます*3。 |
failure_app | 認証に失敗した際に呼びされる Web アプリケーションを決めます。 |
default_strategies に指定している :custom_login_strategy は、後で定義する独自の認証方式に付けた名前です。名前は任意です。自分で付けます。
この事例ではストラテジーを1つだけ指定していますが複数の指定も可能です。
スコープという概念があり、Web アプリケーションの場所ごとに異なるストラテジー(=認証方法)を適用することも可能なようです。本ページでは触れません。
failure_app に指定している Sinatra::Application は、メイン Web アプリケーションを表す Sinatra の機能です。
通常は認証失敗時、同じ Web アプリケーション内で処理を完結させることが多いでしょうが、別の Web アプリケーションに処理を委譲することが可能です。
認証失敗時、failure_app にて指定した Web アプリケーションに URL '/unauthenticated' に POST 要求が飛びます。これは Warden の仕様です。
ストラテジー(=認証方法)の定義
warden_strategies.ru の以下の定義がそれに当たります。
Warden::Strategies.add :custom_login_strategy do def valid? params['username'] || params['password'] end def authenticate! user = get_user(params['username'].strip) if user.nil? || user[:password] != params['password'].strip fail!('ログインできませんでした。') else user[:username] = params['username'] success!(user) end end end
Warden::Strategies.add にてストラテジー(=認証方法)を定義します。
本サンプルでは user.rb にて定義しているハッシュ形式のユーザ管理情報を元にログインの認証方法を事例としています。
:custom_login_strategy は、このストラテジー(=認証方法)に自分が付けた名前です。 「Warden による認証機能の有効化」時にこのストラテジーを標準として定義しています。
Warden::Strategies.add ブロックの中で以下の2つのメソッドを定義する必要があります。
メソッド名 | 役割 |
valid? | 認証が可能かどうかの判断をする。必要な情報が揃っており認証が可能であれば true を、そうでなければ false を返すよう定義する。 |
authenticate! | 具体的な認証ロジックを定義する。認証に成功した場合は success! を、失敗した場合は fail! を呼び出すよう定義する。 |
valid? および authenticate! メソッド内では params ハッシュにアクセスできる。params にはフォームからの入力が入っています。(※ Sinatra を主として使っている人が陥りやすい注意点はこちらを参照のこと。)
valid? はフォームから必要な入力が来ていることの確認を行うのが基本になります。事例では username と password の2つの入力があることを確認しています。
authenticate! には独自の認証処理を書きます。
事例ではフォームから受け取った username の値を元にユーザを検索し、ユーザのパスワードが入力値と一緒であることの確認を行なっています。ユーザが見つからなかったり、パスワードが一致しなかった場合は認証失敗としています。この定義は要件によりデータベースアクセスによるユーザ確認などに置き換わる箇所になります。
- 認証に失敗した場合、fail! メソッドを呼ぶ。
既に説明している通り、認証に失敗した場合 failure_app にて指定した Web アプリケーションの URL -'/unauthenticated' に POST 要求が飛びます。fail! メソッドの第1引数に指定したメッセージは、遷移先にて env['warden'].message で取得出来ます。メッセージを特に利用しない場合は省略可能です。
- 認証に成功した場合、success! メソッドを呼ぶ。
success! メソッドにはユーザを識別する情報を渡してください。
この情報は、のちにクライアント(クッキー)とサーバ(セッション)それぞれが持つ認証情報の元になります。
本事例では第1引数のみ指定していますが、fail! メソッドと同様、メッセージが必要な場合は第2引数にメッセージの指定ができます。
ユーザ情報の扱いの定義
認証が確立すると、クッキーによってクライアントとサーバ間にセッションが確立されます。
サーバのセッション情報にはユーザの情報が格納されるようになり、セッションからいつでもユーザの情報が参照できるようになります。
warden_strategies.ru の以下の定義でユーザ情報の扱い方を定義しています。
Warden::Manager.serialize_into_session do |user| user[:username] end Warden::Manager.serialize_from_session do |username| user = get_user(username) # パスワード情報はセキュリティ的な観点から通常は不要なものとして削除しておく。 user.delete(:password) return user end
それぞれの定義の役割をまとめます。
ブロック名 | 役割 |
Warden::Manager.serialize_into_session | success! から受け取った情報を元に、セッションに格納するユーザ情報を決定する。認証に成功した際、1度だけ呼び出される。 |
Warden::Manager.serialize_from_session | セッションに格納されているユーザ情報を元に実際のユーザ情報を決定する。ユーザ情報要求時に毎回呼び出される。 |
ユーザ情報の取得は env['warden'].user にて行います。
この2つの定義次第で、ユーザの管理方法を以下の2つから選択できます。
- ログイン時に全てのユーザ情報を格納し、以降、その情報を利用する。
- ログイン時にはキーのみを格納し、必要時に詳細な情報を取得する。
事例では2番目の方法を採用していています。
2つの方法には以下のようなトレードオフがあります。
- ログイン時に全てを格納する方法
- ユーザ情報の取得が速い(例えばデータベースから情報を取得する場合、データベースへのアクセスは初回のみで、以降はデータベースへの参照が不要になる)。
- サーバの使用メモリが多い。
- 最新の情報を保つためには工夫が必要になる(ログイン後にユーザ情報の変更がある場合、変更の反映が必要となる*4)。
- ログイン時はキーのみを格納し、アクセス毎にユーザ情報を取得する方法
- ユーザ情報の取得が遅い(例えばデータベースから情報を取得する場合など、データベースへのアクセスするための負荷が掛かる)。
- サーバの使用メモリが少ない。
- 最新の情報を保てる。
本サンプルでは後者の方法を採用しています。
認証の利用
ここまで定義した Warden による認証機能を使うためのヘルパーメソッドを helpers.rb にまとめました。
helpers do def warden request.env['warden'] end def login? warden.user != nil end def login_user warden.user end def authenticate! warden.logout warden.authenticate! end def logout warden.logout end end
Warden の機能にアクセスするには env['warden'] にアクセスします。
env とはいわゆる Rack が定義する env です。Sinatra では、request.env が同一のものを表します。
env['warden'] が持つ各種メソッドを使い、認証の実行やユーザ情報へのアクセス、ログアウトなどの操作を行います。以下、主要なメソッドをまとめます。
メソッド | 役割 |
authenticate! | 認証を行います。認証成功時は処理が返ってきますが、認証失敗時は failure_app にて指定した Web アプリケーションの URL '/unauthenticated' に POST リダイレクトされ、処理は戻ってきません。 |
user | ユーザ情報を取得します。Warden::Manager.serialize_from_session が定義した情報です。未ログイン時は nil が返ってきます。 |
logout | ログアウトし、セッションからユーザ情報が消滅します。 |
ヘルパーメソッドでは、これらの機能をわかりやすく使えるように定義しています。 ヘルパーメソッドを定義するかどうかは好みの問題です。env['warden'] に直接アクセスして利用してもなんの問題もありません。
authenticate! ヘルパーメソッドで、warden.authenticate! の前に warden.logout を呼んでいるのは、既にログイン済みのユーザが再ログインをしようとした場合、認証を促すための措置です。
最後に参考までに画面遷移のためのハンドラーの定義を紹介します。
get '/' do if login? then erb :top else erb :login end end post '/login' do authenticate! redirect "/" end post '/unauthenticated' do erb :unauthenticated end get '/logout' do warden.logout erb :login end
ルート(get '/')にアクセスがあった際、
- 既ログインであればユーザ情報表示画面を表示。
- 未ログインであればログイン画面を表示。
という判断を行なっています。
ログイン要求('post /login')があった場合は、認証を行い成功した場合はルートにリダイレクトし、ユーザ情報表示画面へと遷移させています。
認証に失敗した場合、post '/unauthenticated' の要求があるのは Warden の仕様です。
get/post/redirect/erb などは Sinatra の機能です。以下が参考になります。
トラブルシューティング
params の値にアクセスできない
Sinatra を主体として使っている人が陥りやすい問題であると考え、 トラブルシューティングの項目の1つとして書くことにしました。
Warden::Strategies.add ブロック内で params にアクセスする際、シンボルではなく必ず文字列でアクセスしてください。
Sinatra アプリケーション内のリクエスト・インスタンススコープで使える params という同名の機能が存在しますが、 Warden::Strategies.add ブロック内で使える params は Warden が提供する全く別の機能です。
Sinatra では
params[:username]
という形でフォームの情報にアクセスすることがありますが、Warden::Strategies.add ブロック内での params にアクセスする際は、
params['username']
と書く必要がある点に注意が必要である。シンボルで値を参照しようとすると nil が返ってきます。
補足
認証時のデータベース接続をその後の処理でも利用する
本事例ではユーザ情報にグローバル変数を用いていますが、 データベースなどで本格的にユーザ情報の管理を行い始めると、 データベース接続を各所で利用したくなるはずです。
例えば以下の場所です。
- Warden::Strategies.add の authenticate!
- Warden::Manager.serialize_from_session
- get, post などのハンドラ
これら各所でリクエスト単位の情報共有を行うには Rack の env を使います。
Warden は env メソッドを提供してくれています。リクエスト単位で実行される箇所で env メソッドが使えます。
Sinatra は request.env メソッドを提供してくれています。get, post などのハンドラで request.env メソッドが使えます。
簡単な事例を示します。ポイントにコメントを書いたので参考にしてください。
def get_dao(env) # env[:myapp_dao] にデータベースアクセスオブジェクトを入れる env[:myapp_dao] ||= MyAppDao.new end def close_dao(env) # データベースアクセスオブジェクトがあればクローズし痕跡を消す if env[:myapp_dao] then env[:myapp_dao].close env[:myapp_dao] = nil end end Warden::Strategies.add :custom_login_strategy do # ... def authenticate! # データベースアクセスオブジェクトを使ってユーザの認証を行う dao = get_dao(env) user = dao.get_user(params['username'].strip) if user.nil? || user[:password] != params['password'].strip fail!('ログインできませんでした。') else user[:username] = params['username'] success!(user) end end end Warden::Manager.serialize_from_session do |username| user = get_dao(env).get_user(username) # パスワード情報はセキュリティ的な観点から通常は不要なものとして削除しておく。 user.delete(:password) return user end # ... get '/any_url' do dao = get_dao(request.env) # dao で何かする ... end # ... after do # リクエストの最後にデータベースをクローズする close_dao(request.env) end
コメント
本ページの内容に関して何かコメントがある方は、以下に記入してください。
コメントはありません。 コメント/warden/auth