なんとなく使い方が分かってきたので、リベンジ: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
enddb/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
endpagesのビューにアクセスする際にはログインが必要にする。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
enddb/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
endroles_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認証で詳しく説明されている。