#250 Authentication from Scratch
- Download:
- source codeProject Files in Zip (103 KB)
- mp4Full Size H.264 Video (23.6 MB)
- m4vSmaller H.264 Video (16.4 MB)
- webmFull Size VP8 Video (42.9 MB)
- ogvFull Size Theora Video (33.4 MB)
ほとんどすべてのRailsアプリケーションは、なんらかの認証機能を必要とします。パスワード認証機能を提供するライブラリとしてもっともよく使われるのが、Authlogic、Devise、 Restful Authentication、Clearanceの4つですが、どれを使うのがいいのでしょうか?AuthlogicとResful Authenticationはここ数ヶ月更新されていないので、残るのはDeviseかClearanceです。どちらもRailsのengineという仕組みによって、いくつかのコントローラとビューをアプリケーションに付加し認証機能を提供するものです。認証のような重要な機能を扱う場合、engineという仕組みは最適とは言えません。というのも、controllerやviewで提供される多くの機能を結局は上書きすることになりやすいからです。このような場合、engineという仕組みの利点は活かされず、アプリケーション全体を複雑なものにしてしまいます。
もちろんengineを使うのがふさわしい場合もあり、どんなときも使うべきでないというわけではありませんが、ここでは別の方法を検討したほうがいいかもしれません。認証機能の実装のためにはengineよりもgeneratorの方が向いているかも知れません。すべてのコードがアプリケーション内にあり、カスタマイズしやすいからです。例えば、Ryan BatesのNifty Authenticationには、アプリケーションにパスワード認証機能を 追加するためのベースになる簡単なコードを生成するジェネレータが含まれています。しかしこの記事はNifty Authenticationを紹介するのが目的ではありません。ここではパスワード認証をゼロから作る方法を紹介することにしましょう。それによって、実際にengineやgeneratorを使うときにも、その中身がどのような仕組みで動いているかについて理解が深まることでしょう。
はじめに
認証のしくみをゼロから作るにあたり、まずはauth
という名前でRails 3のアプリケーションを新規作成しましょう。
$ rails new auth
続いて、新しく作成したauth
ディレクトリに cd
し、ユーザ登録(sign-up)プロセスを作ります。ユーザを作成するためのコントローラが必要なので、UsersController
を作成し、 その中にnew
アクションを作ります。
$ rails g controller users new
このコントローラと一緒に、ユーザのメールアドレスとパスワードを保存するためのUser
モデルも作成します。言うまでもないですが、パスワードは決して素のテキストで保存するべきではありません。その代わりにハッシュ値とソルトを保存します。
$ rails g model user email:string password_hash:string password_salt:string
モデルを作成したら、データベースをmigrateしてusersテーブルを作成します。
$ rake db:migrate
次にUsersController
内のnew
アクションとcreate
アクションにコードを記述します。
class UsersController < ApplicationController def new @user = User.new end def create @user = User.new(params[:user]) if @user.save redirect_to root_url, :notice => "Signed up!" else render "new" end end end
これはごく標準的なコントローラのコードです。new
アクションで、新しいUser
を生成します。create
アクションでは、入力フォームから渡された引数に基づいてUser
を生成します。新しく生成されたUser
が有効なら、スタートページにリダイレクトします。(スタートページはまだ作成されていません。)Userが正しく生成されなければnew
アクションのテンプレートを再表示します。
ここで、new
テンプレートを作りましょう。ここには、email
、password
、 password_confirmation
の各フィールドとエラーメッセージを表示するためのコードが含まれます。
<h1>Sign Up</h1> <%= form_for @user do |f| %> <% if @user.errors.any?%> <div class="error_messages"> <h2>Form is invalid</h2> <ul> <% for message in @user.errors.full_messages %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <p> <%= f.label :email %><br /> <%= f.text_field :email %> </p> <p> <%= f.label :password %><br /> <%= f.password_field :password %> </p> <p> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation %> </p> <p class="button"><%= f.submit %></p> <% end %>
先ほど作成したUser
モデルには属性としてpassword
と password_confirmation
を持っていません。その代わりにUser
モデル内にこれらを処理するアクセサメソッドを作ります。
次に、routesファイルを少し修正します。controllerジェネレータは、次のようなrouteを生成しました。
get "users/new"
このrouteを/sign_upに変えて、users#newにアクセスするように指示し、名前を"sign_up"
にします。また、root routeをユーザ登録フォームに対応づけます。最後にrecources :usersを追加して、createアクションが機能するようにします。
Auth::Application.routes.draw do get "sign_up" => "users#new", :as => "sign_up" root :to => "users#new" resources :users end
ここでサーバを起動して、登録フォームのページにアクセスすると、エラーが表示されます。
このエラーが表示されたのは、ユーザ情報を入力するフォームにはpassword
フィールドがあるにもかかわらずデータベースには対応するフィールドがないためUser
モデルにもpassword
属性がないからです。そこでモデルでpassword属性を生成し、それと同時にpassword_confirmation
フィールドを処理する属性も作ります。
パスワード確認には、validates_confirmation_of
を使えば2つのフィールドへの入力が一致しているかも同時にチェックしてくれます。ちょうどいいタイミングなので、フォーム内の他の入力確認、メールアドレスとパスワードの存在チェック、メールアドレスの重複チェックも追加しましょう。
class User < ActiveRecord::Base attr_accessor :password validates_confirmation_of :password validates_presence_of :password, :on => :create validates_presence_of :email validates_uniqueness_of :email end
Userモデルを作成したときに、暗号化したパスワードを保存するためにpassword_hash
とpassword_salt
を作りました。フォームが登録されるときにpassword
フィールドの値を暗号化し、結果のハッシュ値とソルトをこれらのフィールドに保存します。パスワードの暗号化には、bcryptが便利です。ここではbcrypt-ruby gemを使うことにします。まずGemfileにgemへの参照情報を記述して、bundleコマンドを実行し、gemがインストールされたことを確認します。
source 'http://rubygems.org' gem 'rails', '3.0.3' gem 'sqlite3-ruby', :require => 'sqlite3' gem 'bcrypt-ruby', :require => 'bcrypt'
次にUser
モデルを編集し、パスワードが保存される前に暗号化するように修正します。これを実現するために、before_save
コールバックを使用し、この後作成するencrypt_password
メソッドを呼び出します。このメソッドはパスワードが存在するかをチェックし、もし存在すれば、BCrypt::Engine
の2つのメソッドgenerate_salt
とhash_secret
を使って、ソルトとハッシュ値を生成します。
class User < ActiveRecord::Base attr_accessor :password before_save :encrypt_password validates_confirmation_of :password validates_presence_of :password, :on => :create validates_presence_of :email validates_uniqueness_of :email def encrypt_password if password.present? self.password_salt = BCrypt::Engine.generate_salt self.password_hash = BCrypt::Engine.hash_secret(password, password_salt) end end end
これで、ユーザ情報が登録されると、password_hash
とpassword_salt
がデータベースに保存されるようになりました。ユーザ登録フォームにアクセスして情報を正しく入力すると、homeページにリダイレクトされます。そこでデータベース内のusers
テーブルを見てみると、新しいユーザの情報が、暗号化されたパスワードのハッシュ値とソルトと共に登録されているのがわかります。
$ rails dbconsole SQLite version 3.6.12 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite> .mode column sqlite> .header on sqlite> SELECT * FROM users; id email password_hash password_salt created_at updated_at ---------- --------------------- ------------------------------------------------------------ ----------------------------- -------------------------- -------------------------- 1 eifion@asciicasts.com $2a$10$Jh./oyCeThSChUCY8Of6F.fiHP8m4gMkZNjUR3vsDgvupUPgumNs. $2a$10$Jh./oyCeThSChUCY8Of6F. 2011-01-26 21:51:56.399518 2011-01-26 21:51:56.399518
ログイン
これで半分まで来ました。ユーザ登録(sign up)ができるようになりましたが、まだログイン(sign in)ができません。それではこれから、ログインフォームを処理するsessions
コントローラを新しく作成しましょう。
$ rails g controller sessions new
new
ビューのファイル内に、ログイン用のフォームを作成します。
<h1>Log in</h1> <%= form_tag sessions_path do %> <p> <%= label_tag :email %><br /> <%= text_field_tag :email, params[:email] %> </p> <p> <%= label_tag :password %><br /> <%= password_field_tag :password %> </p> <p class="button"><%= submit_tag %></p> <% end %>
ここではform_for
ではなくform_tag
を使います。form_for
を使った場合、フォーム名に対応するモデルがあるということを示すことになるからです。今回、Session
モデルはないので、form_forは使えません。フォームからのデータはsessions_path
にPOSTされ、SessionController
のcreate
アクションを呼び出します。フォームには、email addressとpasswordという2つのフィールドがあります。
ここで、ルーティングにも変更を加えます。ジェネレータが生成した"sessions/new"
を、"log_in"
ルートに置き換えます。また、ユーザ登録フォームが正しく動くよう、resources :sessions
も追加しておきます。
Auth::Application.routes.draw do get "log_in" => "sessions#new", :as => "log_in" get "sign_up" => "users#new", :as => "sign_up" root :to => "users#new" resources :users resources :sessions end
SessionsController
内にcreate
アクションを作成し、ユーザがログインしたときの処理を記述します。そこでは、User
モデルのnew class methodをコールすることでユーザ認証をおこないます。ユーザが正しく認証されたら、このメソッドがUser
のレコードを返します。その場合、ユーザIDをセッション変数に格納して、スタートページにリダイレクトし、ユーザにはflashメッセージを表示して正しくログインしたことを知らせます。認証が成功しなかった場合は、別のflashメッセージを表示してフォームを再表示します。ポイントは、他のページへリダイレクトするのではなく、flash.now
を使ってページを表示しています。
class SessionsController < ApplicationController def new end def create user = User.authenticate(params[:email], params[:password]) if user session[:user_id] = user.id redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end end
ここで、User.authenticateメソッドを作ります。このメソッドは入力されたメールアドレスをもとにユーザを検索します。ユーザが検索されたら、ユーザ登録のときと同様にフォームから入力されたパスワードをそのユーザのpassword_salt
を用いて暗号化します。パスワードから生成されたハッシュ値が、保存されたハッシュ値と一致すればパスワードが正しいということでユーザが返されるか、そうでなければnil
が返されます。Rubyでは、else
ステートメントがなくてもnil
が返されるので、実際のところelse
ステートメントは不要ですが、ここでは分かりやすさのためあえて付け加えてあります。
def self.authenticate(email, password) user = find_by_email(email) if user && user.password_hash == BCrypt::Engine.hash_secret ↵ (password, user.password_salt) user else nil end end
これをテストする前に、アプリケーションのレイアウトファイルを修正し、flashメッセージが表示されるようにします。
<!DOCTYPE html> <html> <head> <title>Auth</title> <%= stylesheet_link_tag :all %> <%= javascript_include_tag :defaults %> <%= csrf_meta_tag %> </head> <body> <% flash.each do |name, msg| %> <%= content_tag :div, msg, :id => "flash#{name}" %> <% end %> <%= yield %> </body> </html>
正しくないユーザやパスワードでログインしようとすると、ログインフォームが再表示され、さらにそれに加えて、ログインが正しく行われなかったことを知らせるflashメッセージが表示されます。
正しいログイン情報を入力すると、homeページにリダイレクトされ、正しくログインできたことを知らせるflashメッセージが表示されます。
ログアウト
ここまでのところはうまく行きました。次にログアウト処理が必要です。まず、新しいrouteである"log_out"
を追加しましょう。
Auth::Application.routes.draw do get "log_in" => "sessions#new", :as => "log_in" get "log_out" => "sessions#destroy", :as => "log_out" get "sign_up" => "users#new", :as => "sign_up" root :to => "users#new" resources :users resources :sessions end
このrouteは、SessionsController
のdestroy
アクションに対応付けられています。このアクションは、user_id
セッション変数を削除することでユーザをログアウトし、スタートページにリダイレクトします。
def destroy session[:user_id] = nil redirect_to root_url, :notice => "Logged out!" end
正しく動作するかどうか、/log_out
にアクセスして確認します。スタートページにリダイレクトされて、"Logged out(ログアウトしました)!"とflashメッセージが表示されます。
リンクを追加する
ユーザがログインやログアウトのためにブラウザのアドレスバーにURLをタイプするよりも、ページへのリンクを準備する方がずっと便利でしょう。レイアウトファイル内のflashメッセージを表示するコードの直前に以下のコードを置きます。
<div id="user_nav"> <% if current_user %> Logged in as <%= current_user.email %> <%= link_to "Log out", log_out_path %> <% else %> <%= link_to "Sign up", sign_up_path %> or <%= link_to "Log in", log_in_path %> <% end %> </div>
current_user
メソッドがまだないので、ここで作ることにします。ApplicationController
内に記述します。
class ApplicationController < ActionController::Base protect_from_forgery helper_method :current_user private def current_user @current_user ||= User.find(session[:user_id]) if ↵ session[:user_id] end end
current_user
メソッドは、セッション変数から現在のユーザのidを得て、それをインスタンス変数にキャッシュします。それをヘルパーメソッドとして作成し、アプリケーションのビューのコードの中でも利用できるようにします。
ページをリロードすると、sign up(ユーザ登録)とlog in(ログイン)のリンクが表示されています。ログインすると、ログイン情報と、ログアウト用のリンクが表示されます。
これで認証システムが一通りできあがりました。今回のエピソードでは多くのコードを扱いましたが、そのほとんどがコントローラ内とビュー内に記述するものでした。認証のロジックはすべてUser
モデル内のself.authenticate
メソッドとencrypt_password
メソッドで発生します。このコードはとてもシンプルです。
あなたも、engineを使わずに認証システムをゼロから作るのであれば、パスワード認証はそれほど複雑ではなく、コントローラやビューを、あなたのアプリケーションに合うように好きなようにカスタマイズすることが可能です。ここで説明したものは、最低限のしくみです。実システムでは、Userモデル内にパスワードの長さやメールアドレスのフォーマットのチェック機能を追加するのもいいでしょう。
Userモデルに追加するべき大事なものは、一括設定できる属性を制限するコードです。これは、Userモデル内にattr_accessorを追加することで実現します。
class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation #rest of code omitted end
これによって、Userモデル内のpassword_hashなどのフィールドを入力フォーム以外から更新できないようにします。
今回のエピソードはこれで終わりです。gemとして入手できる認証のしくみはそのまま利用できて便利ですが、通常のパスワード認証がどのようなしくみで動作しているのかを理解することはとても大切です。もしパスワード認証に加えて、サードパーティによる外部認証が必要な場合は、Simple OmniAuthについて説明しているエピソード241[動画を見る、原文を読む]を参照してください。