なんとなく使い方が分かってきたので、リベンジ:Rails 2.3RC1で”Restful Authentication with all the bells and whistles”をやってみるをdeviseでやってみる。ちなみに "with all the bells and whistles" はあんまり良い意味ではないみたい。
チュートリアルの実施環境
% uname -a Linux pi 2.6.32-5-686 #1 SMP Thu Aug 12 13:38:27 UTC 2010 i686 GNU/Linux % more /proc/version Linux version 2.6.32-5-686 (Debian 2.6.32-20) (ben@decadent.org.uk) (gcc version 4.3.5 (Debian 4.3.5-2) ) #1 SMP Thu Aug 12 13:38:27 UTC 2010 % ruby -v ruby 1.8.7 (2010-08-16 patchlevel 302) [i486-linux] % gem1.8 --version 1.3.7 % rails -v Rails 3.0.0 % sqlite3 --version 3.7.2
deviseのインストール
gemで導入する。
% sudo gem1.8 install devise
次に、プロジェクトを新規作成する。
% rails new app_30 % cd app_30
Pageリソースを生成する。
% rails g scaffold Page title:string boty:text
Gemfileに以下を書き加える。
gem 'devise', '1.1.2
deviseを組み込む。
% rails g devise:install create config/initializers/devise.rb create config/locales/devise.en.yml =============================================================================== Some setup you must do manually if you haven't yet: 1. Setup default url options for your specific environment. Here is an example of development environment: config.action_mailer.default_url_options = { :host => 'localhost:3000' } This is a required Rails configuration. In production it must be the actual host of your application 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root :to => "home#index" 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> ===============================================================================
deviseをインストールした際のメッセージにしたがう。まず、config/environments/development.rbに以下を加える。
# For devise config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config/routes.rbにルートを設定する。pages#indexをルートとする。
App30::Application.routes.draw do root :to => "pages#index" resources :pages end
app/views/layouts/application.html.erb にflushを表示する。
<!DOCTYPE html> <html> <head> <title>devise with all the bells and whistles</title> <%= stylesheet_link_tag :all %> <%= javascript_include_tag :defaults %> <%= csrf_meta_tag %> </head> <body> <div class="notiece"> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> </div> <%= yield %> </body> </html>
Userの作成
deviseでログイン管理をするモデルを作成する。今回はUserとする。
% rails g devise User
deviseでは11のモジュールが用意されており目的別に使用するモジュールを選択する。今回は以下の9つを使う。
- Database Authenticatable: 自アプリケーションでパスワード管理をするならば必ず選ぶ
- Registerable:ユーザーが自分でアカウント作成&編集&削除できるようにするならば選ぶ
- Confirmable:restful_authenticationで言うactivationが必要ならば選ぶ
- Recoverable:パスワード忘れ対策が必要ならば選ぶ
- Rememberable:「次回以降自動的にログインする」の機能を実現するならば選ぶ
- Trackable:ログイン履歴をとるならば選ぶ
- Timeoutable:一定期間操作を行っていなければ自動ログアウトを実現するならば選ぶ
- Validatable:入力されたメールアドレスやパスワードのチェックをするならば選ぶ
- Lockable:規定回数以上ログインに失敗したらアカウントをロックするならば選ぶ
app/models/user.rbにおいて、上記9つのモジュールを列挙する。また、loginという項目をattr_accessibleに付け加える。
class User < ActiveRecord::Base # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, :lockable and :timeoutable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable # Setup accessible (or protected) attributes for your model attr_accessible :login, :email, :password, :password_confirmation, :remember_me end
db/migrate以下にあるXXX_devise_create_users.rbを編集し、選択したモジュールに関連するフィールドとloginフィールドを追加する。
class DeviseCreateUsers < ActiveRecord::Migration def self.up create_table(:users) do |t| t.string :login, :limit => 40 t.database_authenticatable :null => false t.recoverable t.rememberable t.trackable t.confirmable t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both # t.token_authenticatable t.timestamps end add_index :users, :email, :unique => true add_index :users, :reset_password_token, :unique => true add_index :users, :confirmation_token, :unique => true add_index :users, :unlock_token, :unique => true end def self.down drop_table :users end end
pagesのビューにアクセスする際にはログインが必要にする。app/controllers/pages_controller.rbに以下のフィルターを加える。
before_filter :authenticate_user!
一度、動かしてみる。
% rake db:migrate % rails server
http://localhost:3000/pages にアクセスして、 http://localhost:3000/users/sign_in に飛ばされた成功!
ただし、このままだと、login フィールドの入力欄および入力処理がない。なので、修正する。まず、devise関連のビューを編集できるようにするためにビューをローカルに生成する。
% rails generate devise:views
app/views/devise/registrations 以下の new.html.erbとedit.html.erbを編集する。たとえば、new.html.erbは以下のようにする。form_for でデータが送られているので、そのUserテーブルのフィールド名を書くだけで新たな項目を追加できる。
<h2>Sign up</h2> <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> <%= devise_error_messages! %> <p><%= f.label :login %><br /> <%= f.text_field :login %></p> <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 %><br /> <%= f.password_field :password_confirmation %></p> <p><%= f.submit "Sign up" %></p> <% end %> <%= render :partial => "devise/shared/links" %>
一方、app/views/devise/sessions/new.html.erb ではパスワードとlogin名でログインできるように変更する。
<h2>Sign in</h2> <%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %> <p><%= f.label :login %><br /> <%= f.text_field :login %></p> <p><%= f.label :password %><br /> <%= f.password_field :password %></p> <% if devise_mapping.rememberable? -%> <p><%= f.check_box :remember_me %> <%= f.label :remember_me %></p> <% end -%> <p><%= f.submit "Sign in" %></p> <% end %> <%= render :partial => "devise/shared/links" %>
Custom field for sign inにしたがい、config/initializers/devise.rbの変数「config.authentication_keys」を変更し、loginをSign inの際に使うようにする。
config.authentication_keys = [ :login ]
WebRickを立ち上げなおして、変更がうまくいっているかどうかを確認してみる。
RolesとPermissionsの設定
rolesとpermissionsを設定する。
% rails g scaffold Role rolename:string % rails g model Permission
db/migrate以下にあるXXX_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 end def self.down drop_table :permissions end end
db/seeds.rbを以下のように書く。
#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 = "administrator" user.password_confirmation = "administrator" user.save user.confirm! role = Role.find_by_rolename('administrator') user = User.find_by_login('admin') permission = Permission.new permission.role = role permission.user = user permission.save
次に、app/models以下のファイルをいくつか変更しなければならない。はじめに、rolesとusersの間の関係を定義する。rolesとusersは多対多の関係なので、has_many :through宣言で関係を示す。
role.rb:
class Role < ActiveRecord::Base has_many :permissions has_many :users, :through => :permissions end
permission.rb
class Permission < ActiveRecord::Base belongs_to :user belongs_to :role end
usr.rbの編集については、リベンジ:Rails 2.3RC1で”Restful Authentication with all the bells and whistles”をやってみるではいろいろと行ったが、deviseの場合は特に何かをする必要はないらしい。Userモデルに格納されるデータのvalidationについては、config/initializers/devise.rbで定義する様子。
なので、user.rbにはとりあえず以下だけを付け加えておく。
has_many :permissions has_many :roles, :through => :permissions def has_role?(rolename) self.roles.find_by_rolename(rolename) ? true : false end
roleのチェック
リベンジ:Rails 2.3RC1で”Restful Authentication with all the bells and whistles”をやってみるでは、authenticated_system.rbにnot_logged_in_required, check_role, check_administrator_roleと permission_denied が付け加えられている。
not_logged_in_requiredは、ログインしていない場合はアクセスでき、ログインしている場合は、元のページかuser_rootへ飛ばされるというフィルター。check_role(role)は、呼び出されたときcurrent_userが引数で与えられたroleを与えられているかどうかをチェックし、与えられていない場合は元のページかuser_rootへ飛ばすというメソッド。check_administrator_roleは、current_userにadministratorという役割が与えられているかどうかを真か偽で返すメソッド。permission_deniedは、元のページがリファラーに保存されていたら、そのページにそうでなければuser_rootへ飛ばすメソッド。
app/controllers/application_controller.rbに上記を用意する。
class ApplicationController < ActionController::Base protect_from_forgery def check_role(role) unless user_signed_in? && @current_user.has_role?(role) if user_signed_in? permission_denied else store_referer access_denied end end end def check_administrator_role check_role('administrator') end def not_logged_in_required user_signed_in? || permission_denied end def access_denied respond_to do |format| format.html do store_location flash[:error] = "You must be logged in to access this feature." redirect_to user_session_path end format.xml do request_http_basic_authentication 'Web Password' end end end def permission_denied respond_to do |format| format.html do host_name = App30.config.action_mailer.default_url_options domain_name = "http://#{host_name}" http_referer = session[:refer_to] if http_referer.nil? store_referer http_referer = ( session[:refer_to] || domain_name ) end flash[:error] = "You don't have permission to complete that action." if /\A#{domain_name}/ =~ http_referer session[:refer_to] = nil redirect_to root_path else redirect_to_referer_or_default(root_path) end end format.xml do headers["Status"] = "Unauthorized" headers["WWW-Authenticate"] = %(Basic realm="Web Password") render :text => "You don't have permission to complete this action.", :status => '401 Unauthorized' end end end # Store the URI of the current request in the session. # # We can return to this location by calling #redirect_back_or_default. def store_location session[:return_to] = request.request_uri end def store_referer session[:refer_to] = request.env["HTTP_REFERER"] end # Redirect to the URI stored by the most recent store_location call or # to the passed default. def redirect_back_or_default(default) redirect_to(session[:return_to] || default) session[:return_to] = nil end def redirect_to_referer_or_default(default) redirect_to(session[:refer_to] || default) session[:refer_to] = nil end end
コントローラーの編集
restful_authenticationの場合、アカウントの作成などをすべて自前で用意する必要があったが、deviseでは既に用意済み。パスがどうなっているのか確認する。
% rake routes root /(.:format) {:action=>"index", :controller=>"pages"} new_user_session GET /users/sign_in(.:format) {:action=>"new", :controller=>"devise/sessions"} user_session POST /users/sign_in(.:format) {:action=>"create", :controller=>"devise/sessions"} destroy_user_session GET /users/sign_out(.:format) {:action=>"destroy", :controller=>"devise/sessions"} user_password POST /users/password(.:format) {:action=>"create", :controller=>"devise/passwords"} new_user_password GET /users/password/new(.:format) {:action=>"new", :controller=>"devise/passwords"} edit_user_password GET /users/password/edit(.:format) {:action=>"edit", :controller=>"devise/passwords"} user_password PUT /users/password(.:format) {:action=>"update", :controller=>"devise/passwords"} user_registration POST /users(.:format) {:action=>"create", :controller=>"devise/registrations"} new_user_registration GET /users/sign_up(.:format) {:action=>"new", :controller=>"devise/registrations"} edit_user_registration GET /users/edit(.:format) {:action=>"edit", :controller=>"devise/registrations"} user_registration PUT /users(.:format) {:action=>"update", :controller=>"devise/registrations"} user_registration DELETE /users(.:format) {:action=>"destroy", :controller=>"devise/registrations"} user_confirmation POST /users/confirmation(.:format) {:action=>"create", :controller=>"devise/confirmations"} new_user_confirmation GET /users/confirmation/new(.:format) {:action=>"new", :controller=>"devise/confirmations"} user_confirmation GET /users/confirmation(.:format) {:action=>"show", :controller=>"devise/confirmations"} user_unlock POST /users/unlock(.:format) {:action=>"create", :controller=>"devise/unlocks"} new_user_unlock GET /users/unlock/new(.:format) {:action=>"new", :controller=>"devise/unlocks"} user_unlock GET /users/unlock(.:format) {:action=>"show", :controller=>"devise/unlocks"} roles GET /roles(.:format) {:action=>"index", :controller=>"roles"} roles POST /roles(.:format) {:action=>"create", :controller=>"roles"} new_role GET /roles/new(.:format) {:action=>"new", :controller=>"roles"} edit_role GET /roles/:id/edit(.:format) {:action=>"edit", :controller=>"roles"} role GET /roles/:id(.:format) {:action=>"show", :controller=>"roles"} role PUT /roles/:id(.:format) {:action=>"update", :controller=>"roles"} role DELETE /roles/:id(.:format) {:action=>"destroy", :controller=>"roles"} pages GET /pages(.:format) {:action=>"index", :controller=>"pages"} pages POST /pages(.:format) {:action=>"create", :controller=>"pages"} new_page GET /pages/new(.:format) {:action=>"new", :controller=>"pages"} edit_page GET /pages/:id/edit(.:format) {:action=>"edit", :controller=>"pages"} page GET /pages/:id(.:format) {:action=>"show", :controller=>"pages"} page PUT /pages/:id(.:format) {:action=>"update", :controller=>"pages"} page DELETE /pages/:id(.:format) {:action=>"destroy", :controller=>"pages"}
user_controllerを作成する。上のrake routesで表示しているとおり、Userリソースに対するnew, create, edit, update, destroyはdeviseが担当して作ってくれているので不要。なので、showとindexだけ用意する。なお、管理者権限保有者がユーザーを強制退会させたい場合には、user_controllerにdestroyを用意する必要がある。deviseで用意されている"user_registration DELETE"は、現在、ログインしているユーザーが自分のアカウントを消すときの処理なので、強制退会目的にはつかえない。
% rails g controller Users index show
以下のように編集する。
class UsersController < ApplicationController layout 'application' before_filter :login_required, :only => [:show] before_filter :check_administrator_role, :only => [:index] def index @users = User.find(:all) end def show @user = current_user end end
roles_controllerを編集する。
class RolesController < ApplicationController layout 'application' before_filter :check_administrator_role def index @user = User.find(params[:user_id]) @all_roles = Role.find(:all) end def update @user = User.find(params[:user_id]) @role = Role.find(params[:id]) unless @user.has_role?(@role.rolename) @user.roles << @role end redirect_to :action => 'index' end def destroy @user = User.find(params[:user_id]) @role = Role.find(params[:id]) if @user.has_role?(@role.rolename) @user.roles.delete(@role) end redirect_to :action => 'index' end end
ビューの編集
次はビューの編集。まず、最初にこのアプリケーションのレイアウトを作成する。app/view/layout/application.html.erb を次のようにする。
<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title><%= @title -%></title> <%= stylesheet_link_tag :all %> <%= javascript_include_tag :defaults %> <%= csrf_meta_tag %> </head> <body> <div class="notiece"> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> </div> <p> <% if user_signed_in? %> Logged in as: <%= link_to h(current_user.login.capitalize), users_show_path(current_user) %> | <%= link_to 'Edit Profile', edit_user_registration_path(current_user) %> | <%= link_to 'Change Password', edit_user_password_path(current_user) %> | <%= link_to 'Sign Out', destroy_user_session_path(current_user) %> | <% if current_user.has_role?('administrator') %> <%= link_to 'Administer Users', users_index_path %> <% end %> <% else %> <%= link_to 'Sign In', new_user_session_path %> | <%= link_to 'Sign Up', new_user_registration_path %> | <%= link_to 'Forgot Password?', new_user_password_path %> <% end %> </p> <hr> <%= yield %> </body> </html>
app/views/roles/_role.html.erbの編集。
<li> <%= role.rolename %> <% if @user.has_role?(role.rolename) %> <%= link_to 'remove role', user_role_url(:id => role.id, :user_id => @user.id), :method => :delete %> <% else %> <%= link_to 'assign role', user_role_url(:id => role.id, :user_id => @user.id), :method => :put %> <% end %> </li>
app/views/roles/index.html.erb:
<h2>Roles for <%=h @user.login.capitalize %></h2> <h3>Roles assigned:</h3> <ul><%= render :partial => 'role', :collection => @user.roles %></ul> <h3>Roles available:</h3> <ul><%= render :partial => 'role', :collection => (@all_roles - @user.roles) %></ul>
他のapp/views/rolesのビューに関しては削除しても良いし、拡張してもよい。
app/views/users/index.html.erbの編集。
<% @title= "All Users" %> <h2><%= @title %></h2> <table> <tr> <th>Username</th> <th>Email</th> <th>Roles</th> </tr> <%= render :partial => 'user', :collection => @users %> </table>
app/views/users/_user.html.erbの編集。
<tr class="<%= cycle('odd', 'even') %>"> <td><%=h user.login %></td> <td><%=h user.email %></td> <td><%= link_to 'edit roles', user_roles_path(user) %>]</td> </tr>
app/views/users/show.html.erbの編集。
<% @title = "User:#{@user.login}" %> <h2><%= @title %></h2> <p>Joined on: <%= @user.created_at.to_s(:long) %></p>
config/routesの編集
以下のようにする。
App30::Application.routes.draw do root :to => "pages#index" devise_for :users get "users/index" get "users/show" resources :users do resources :roles end resources :pages end
最後にpublic/index.htmlを削除する。
動作確認
% rake db:reset % rake server
http://localhost:3000/pages にアクセスし、ID: admin, Password: administrator でログインしてみる。
おわりに
リベンジ:Rails 2.3RC1で”Restful Authentication with all the bells and whistles”をやってみるでやってみたこととほとんど同じことが以上で出来ているはず。restful_authenticationよりdeviseの方がいろいろと用意してあるので動かす段階まで用意するのはかなり楽だと思った。ただ、カスタマイズするとなるとブラックボックスに隠されているのでてこずりそうな予感。
Restful Authentication with all the bells and whistlesの後半に対応する deviseでOpenIDを使う方法に関しては、ククログ:Rails 3.0 beta4でDeviseを使ってOpenID認証で詳しく説明されている。