railsforumのrestful_authenticationは素晴らしい!それを見てRESTfulの理解も深まる

最近、restful_authenticationで試行錯誤していて感じたこと。restful_authenticationはユーザー認証に関する必要最小限の機能を提供してくれるが、実際に運用できるレベルに仕上げるには、要点を押さえた的確な修正が必要になる。ユーザー認証のscaffold的な位置付けだろうと。
生成されるソースコードはとても簡潔にまとめられていて、読み易い。しかし、いざ自分好みのログインに修正しようとすると、実にいろいろな手段があり、どのような仕組みにするか本当に迷ってしまう。
例えば、以前の日記で試したパスワード忘れに対応する方法も、今振り返ってみれば最悪の例だ...。アクティベーションとパスワード忘れの処理が混同しているし、修正の手順もセキュリティ的に中途半端。実装の仕方もせっかくのrestful_authenticationのRESTfulなルールを無視している。やはり、自分にはお手本となるコードが必要だ。
そう思って探していると、素晴らしいお手本が見つかった!

ここで紹介されているコードをそのまま実装すれば、以下の機能が提供される。(実際に自分で試したのは、administrator権限によるユーザー管理まで)

  • メールによるアクティベーション
  • パスワード忘れの処理
  • ユーザー情報の編集
  • アクセス権による制限
  • administrator権限によるユーザー管理
  • OpenIDによるログイン

単に機能を追加するだけでなく、ログインの手順やページ遷移、その中で表示されるメッセージ、RESTfulなコーディング等すべてが洗練されて実装されている。素晴らしいお手本。

ユーザー登録の手順

  • ログインページ

  • Sign upリンクをクリックすると...
  • ユーザー登録ページへ

  • Sign upボタンを押すと...
  • ログインページで以下のメッセージ
    • 「Thanks for signing up! Please check your email to activate your account before logging in.(サインアップして頂きありがとうございます。ログインする前に、アカウントを有効にするためのメールを確認してください。)」
  • サインアップしたユーザーは、この時点ではログインすることは出来ない。まだログアウトの状態だ。

  • そしてメールを確認すると、以下の内容で届いている。
件名: 	[localhost:3000] Please activate your new account

Your account has been created. Username: zarigani Visit this url to activate your account: http://localhost:3000/activate/4eceb24519190ad7cb95eaeb62b1c80508c82ed2
  • メールに従ってリンクをクリックすると...
  • ログインページで以下のメッセージ
    • 「Your account has been activated! You can now login.(アカウントは有効になりました。これでログインできます。)」

      • ちなみにエラーが発生した場合、以下メッセージが表示される。
      • メール記載のアクティベーションのリンクを2回以上クリックした場合は「Your account has already been activated. You can log in below.(アカウントは既に有効になっています。以下でログインできます。)」
      • アクティベーションのURLが存在しない場合は「Activation code not found. Please try creating a new account.(アクティベーションコードが見つかりません。もう一度ユーザー登録してみてください。)」
  • アカウントが有効になったことはメールでも通知される。
件名: 	[localhost:3000] Your account has been activated!

zarigani, your account has been activated. You may now start adding your plugins: http://localhost:3000/

以上の手順となっている。

restful_authenticationのジェネレーターが生成するオリジナルとの違いは...
  • サインアップ直後はログアウト状態。
# ---------- app/models/user.rb ----------
class UsersController < ApplicationController
...(中略)...
   def create
     cookies.delete :auth_token
     @user = User.new(params[:user])
     @user.save!
     #Uncomment to have the user logged in after creating an account - Not Recommended
     #self.current_user = @user
   flash[:notice] = "Thanks for signing up! Please check your email to activate your account before logging in."
     redirect_to login_path    
   rescue ActiveRecord::RecordInvalid
     flash[:error] = "There was a problem creating your account."
     render :action => 'new'
   end
...(中略)...
  • メールにパスワードを表示しない。
    • app/views/user_mailer/signup_notification.html.erbのPassword: <%= @user.password %>を削除している。
<%# ---------- app/views/user_mailer/signup_notification.html.erb ---------- %>
Your account has been created.

  Username: <%= @user.login %>
  Password: <%= @user.password %>

Visit this url to activate your account:

  <%= @url %>
  • ログアウトしない限りログイン状態を維持するRemember meオプションが最初から有効。
    • コメントタグ「<!-- Uncomment this if you want this functionality」と「-->」を削除することで実現されている。
<%# ---------- app/views/sessions/new.html.erb ---------- %>
<% form_tag session_path do -%>
<p><label for="login">Login</label><br/>
<%= text_field_tag 'login' %></p>

<p><label for="password">Password</label><br/>
<%= password_field_tag 'password' %></p>

<!-- Uncomment this if you want this functionality<%# この行を削除する %>
<p><label for="remember_me">Remember me:</label>
<%= check_box_tag 'remember_me' %></p>
--><%# この行を削除する %>
...(中略)...
    • デフォルトの有効期間は2週間だが、user.rbの以下の部分を修正することで調整できる。
# ---------- app/models/user.rb ----------
require 'digest/sha1'
class User < ActiveRecord::Base
...(中略)...
   # These create and unset the fields required for remembering users between browser closes
   def remember_me
     remember_me_for 2.weeks
   end
...(中略)...

パスワードを忘れた場合の手順

  • ログインに失敗すると次のメッセージが表示される。「Your username or password is incorrect.(ユーザー名またはパスワードが違っています。)」

  • Forgot passwordのリンクをクリックすると...(Forgot passwordのリンクのみ自分で追加)
  • ユーザー登録した時のメールアドレスを入力するページに移動する。
  • もし間違ったメールアドレスを入力すると次のメッセージが表示される。「Could not find a user with that email address.(そのメールアドレスのユーザーは見つかりませんでした。)」

  • メールアドレスが正しく入力されれば...
  • ログインページで以下のメッセージ
    • 「A password reset link has been sent to your email address.(パスワードを再設定するリンクをお客様のメールアドレス宛てに送信しました。)」

  • パスワードを再設定するリンクを記載したメールが届く。
件名: 	[localhost:3000] You have requested to change your password

zarigani, to reset your password, please visit http://localhost:3000/reset_password/b1e0f1b91da6255640e538d61d58fa0e6b675e6c
  • メール記載のリンクをクリックすると、パスワード再設定のページへ移動する。

  • パスワードを正しく入力してReset your passwordボタンを押すと...
  • ログインページへ移動して次のメッセージが表示される。「Password reset.(パスワードは再設定されました。)」

  • その後、パスワードが再設定されたことを知らせるメールも届く。
件名: 	[localhost:3000] Your password has been reset.

zarigani, Your password has been reset

ユーザー情報の編集

  • Todoリストページを求めてログインするとメニューバーに表示されるユーザー情報と共にTodoリストが表示された。
    • もし、目指すページがない時は、以下のユーザー名Zariganiのリンクページが表示される。
    • スタイルシートは未設定なので、メニューバーは一般的なリストとして表示されている。

  • 順にリンクを辿ってみると...
  • ユーザー名Zariganiのリンクページ

  • Edit Profileのリンクページ

  • Change Passwordのリンクページ

  • Log Outするとメッセージが表示される。「You have been logged out.(ログアウトしました。)」


アクセス権による制限とadministrator権限によるユーザー管理

  • zariganiでログインして、http://localhost:3010/usersへアクセスしてみると...
    • ルートページhttp://localhost:3010/へリダイレクトされてアクセスできない。
    • メッセージは「You don't have permission to complete that action.(そのアクションを実行する権限がありません。)」

  • adminでログインしてみる。(パスワード: admin)

  • メニューにAdminister Usersのリンクが追加されているのでクリックすると...
  • ユーザーリストが表示されて以下の設定が可能だ。
    • Enabled?... そのユーザーの有効・無効の設定(無効だとログインできないユーザー)
    • Roles... アクセス権限の設定
      • デフォルトではユーザーadminに、Administrator権限が設定されている状態
      • ユーザーzariganiについては、アクセス権限なしの状態


何が起こっているのか?
  • UsersControllerでは、before_filter :check_administrator_role... が実行されて、index, destroy, enableアクションについては、ユーザーがadministrator権限を持っていることを要求している。
    • not_logged_in_requiredはログインしていないことを要求する。
    • login_requiredはログインしていることを要求する。
# ---------- app/controllers/users_controller.rb ----------
class UsersController < ApplicationController
   layout 'application'
   before_filter :not_logged_in_required, :only => [:new, :create] 
   before_filter :login_required, :only => [:show, :edit, :update]
   before_filter :check_administrator_role, :only => [:index, :destroy, :enable]
...(中略)...
  • ユーザーzariganiにはアクセス権の設定はない状態
  • ユーザーadminについては、マイグレーションファイルでadministrator権限が設定されている状態
# ---------- db/migrate/20080801140457_create_permissions.rb ----------
class CreatePermissions < ActiveRecord::Migration
   def self.up
     create_table :permissions do |t|
       t.integer :role_id, :user_id, :null => false
       t.timestamps
     end
     #Make sure the role migration file was generated first    
     Role.create(:rolename => 'administrator')
     #Then, add default admin user
     #Be sure change the password later or in this migration file
     user = User.new
     user.login = "admin"
     user.email = "info@yourapplication.com"
     user.password = "admin"
     user.password_confirmation = "admin"
     user.save(false)
     user.send(:activate!)
     role = Role.find_by_rolename('administrator')
     user = User.find_by_login('admin')
     permission = Permission.new
     permission.role = role
     permission.user = user
     permission.save(false)
   end
  
   def self.down
     drop_table :permissions
     Role.find_by_rolename('administrator').destroy   
     User.find_by_login('admin').destroy   
   end
end
新たに独自の権限moderatorを設定したくなったら...
  • rolesテーブルに、name = "moderator"のレコードを追加する。(以下はマイグレーションを利用する方法)
# ---------- db/migrate/20080801140457_create_permissions.rb ----------
class CreatePermissions < ActiveRecord::Migration
   def self.up
     create_table :permissions do |t|
       t.integer :role_id, :user_id, :null => false
       t.timestamps
     end
...(中略)...
     Role.create(:rolename => 'moderator')
...(中略)...
  • moderatorを要求するメソッドcheck_moderator_roleを追加する。
# ---------- lib/authenticated_system.rb ----------
module AuthenticatedSystem
   protected
...(中略)...
  def check_moderator_role
    check_role('moderator')
  end
...(中略)...
  • これで before_filter :check_moderator_role とすれば、moderator権限が要求されることになる。

ユーザーの状態によるusersテーブルの変化

  • 操作によってユーザーの状態が変化する時、usersテーブルのフィールドは、抜粋すると以下のように変化していく。

イベント remember_token remember_token_expires_at activation_code activated_at password_reset_code enabled
新規ユーザー登録直後 07db98435f337e7acbe5b485dc5eebed4a30c2ec t
アクティベーション完了 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 t
パスワードリセット依頼 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 b1e0f1b91da6255640e538d61d58fa0e6b675e6c t
パスワードリセット完了 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 t
Remember meチェックありのログイン c22dea3efeacbf022ba6902ee6a3273df800493c 2008-08-18 04:37:06 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 t
ログアウト 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 t
administrator権限によるdisable 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 f
administrator権限によるenable 07db98435f337e7acbe5b485dc5eebed4a30c2ec 2008-07-28 02:57:05 t

RESTfulのルールに従ったコントローラー構成

  • app/controllers/以下にはrestful_authenticationを制御する5つのコントローラーが定義されている。
  • 唯一の例外users_controller.rbのenableアクションを除いて、アクションメソッドとして定義されるのはindex、new、create、show、edit、update、destroyのみ。
  • ほぼ完全にRESTfulのルールを守っている。
  • コントローラーは以下の役割を担っている。
    • sessions_controller.rb: ログイン(new, create)、ログアウト(destroy)
    • users_controller.rb: 新規ユーザー登録(new, create)、ユーザー情報の修正(edit, update)、ログイン情報の表示(show)、administrator権限によるユーザーリストの取得・有効無効の設定。
    • accounts_controller.rb: アクティベーション(show)、ログイン状態でのパスワード変更(edit, update)
    • passwords_controller.rb: Forgot Passwordのページ(new)、Reset Passwordボタンの処理(create)、メール記載のForgot Passwordのページ(edit)、Rset Your Passwordボタンの処理(update)
    • roles_controller.rb: ユーザー別のアクセス権リスト(index)、ユーザーにアクセス権を追加する(update)、ユーザーからアクセス権を削除する(destroy)
  • RESTfulな設計とはこのようにするのかと、思い知らされた。
    • map.resourcesで:collectionや:memberオプションを多用し始めたら、負け組。
    • そんな時は新たなくくりでコントローラーを抜き出して、index、new、create、show、edit、update、destroyアクションで置き換えられないか考えて直してみる。
    • 例えばここでは、UsersControllerのactivateアクションは、AccountsControllerのshowアクションに置き換えられている。


ビューを整えて、日本語訳すれば、そのまま使えそうな感じだ!これでlogin_engine、user_engineから移行することができる。