Railsのユーザ認証deviseを用いた簡単かつ突っ込んだ実装

Railsのgemライブラリのひとつであるdeviseはユーザの登録やサインイン(セッション処理)など全部やってくれる素敵プラグインですが、あまりにも全部を隠蔽してやってくれすぎるので、つっこんだことと組み合わせてやろうとすると、すぐに訳わかんなくなります。

そこで、今回はdeviseのユーザ管理と共に

  • rails3_acts_as_paranoidを用いたユーザの論理削除
    • deviseのデフォルトの退会はレコード削除だが、これを回避する。
  • bcryptの暗号方式を用いた明示的なパスワード書き換え
    • encrypted_passwordの書き換え
  • rspec,factory_girl_railsを用いたテスト環境
    • deviseのモデルのテストの通し方
  • deviseをオーバライドした、認証、削除の拡張実装
    • 特定のユーザをログインさせない
    • 特定のユーザを退会させない

を実装してみたいと思います。

ちなみにRails 3.0.9です。


まずはプロジェクトを作ります。テストはrspecを用いるので-Tして、testディレクトリの生成をなくしてしまいます。

$ rails new depot -T
$ cd depot

gemのインストールをします。

$ vim Gemfile
+gem 'devise'
+gem 'rails3_acts_as_paranoid'
+gem 'bcrypt-ruby', :require => 'bcrypt'
+group :development, :test do
+ gem 'rspec-rails'
+ gem 'factory_girl_rails'
+end
$ sudo bundle install

次にrspecとdeviseをインストールします。

$ rails g rspec:install
$ rails g devise:install

deviseをインストールしたとき、以下のようなメッセージが出るのでそれに従います。

===============================================================================

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:

<%= notice %>

<%= alert %>

===============================================================================

まずはdeviseライブラリを通じて送られるメールに記載するurlについて。

$ vim config/environments/development.rb
 + config.action_mailer.default_url_options = { :host => 'localhost:3000' }

次はデフォルトurl。devise中ではhome#indexと指定されてますが、なんでもいいです。今回はRailsコメントアウトにのっとり、welcomeにしておきましょう。

$ vim config/routes.rb
+ root :to => "welcome#index"

最後にnotice,alertメッセージ。

$ vim app/views/layouts/application.html.erb
 + <p class="notice"><%= notice %></p>
+ <p class="alert"><%= alert %></p>

ルートパスのwelcome#indexでいろいろ実装してみたいと思います。

$ rails g controller welcome index

デフォルトのページを削除します。

$ rm public/index.html

ユーザモデルを作ります。注意すべきはrails g devise user。普段みたいにrails g model userではありません。

$ rails g devise user name:string status:string deleted_at:timestamp

次にログインやらログアウトへのリンクを貼りましょう。

$ vim app/views/layouts/application.html.erb
 + <% if user_signed_in? %>
+ <%= current_user.name %>さん /
+ <%= link_to "ログアウト", destroy_user_session_path, :method => 'delete' %>
+ <%= link_to "ユーザ情報の編集", edit_user_registration_path %>
+ <% else %>
+ <%= link_to "ログイン", new_user_session_path %>
+ <%= link_to "登録", new_user_registration_path %>
+ <% end %>

データベースを作って、まずは動作確認だけしましょう。(この時点ではまだ正常に動きません。とりあえずlocalhostにサーバをたてるところまで。)

$ rake db:create 
$ rake db:migrate
$ rails s


rails3_acts_as_paranoidを用いたユーザの論理削除

Userモデルのname,statusが追加できないので、それを記述できるようにしましょう。nameは記入方式で。statusはユーザ登録時には自動で"active"が記入されるようにします。また、ユーザモデルに論理削除を適用するため、acts_as_paranoid宣言を挿入します。これにより、ユーザ削除が行われるとき、データベースからレコードが削除されるのではなく、削除された日付がdeleted_atに代入される動きになります。

$ vim app/model/user.rb
class User < ActiveRecord::Base
...
+ acts_as_paranoid
...
- attr_accessible :email, :password, :password_confirmation, :remember_me
+ attr_accessible :name, :status, :deleted_at, :email, :password, :password_confirmation, :remember_me
+ before_create :reset_param
+
+ def reset_param
+ self.status = "active"
+ end
...
end

ユーザ登録、編集時にname, statusの記入項目を作る必要があるのですが、ビュー画面はdeviseが隠蔽してしまっているので、編集できるようにします。

$ rails g devise:views
$ vim app/views/devise/registrations/new.html.erb
 +   <p><%= f.label :name %><br />  
+ <%= f.text_field :name %></p>
$ vim app/views/devise/registrations/edit.html.erb
 +   <p><%= f.label :name %><br />  
+ <%= f.text_field :name %></p>
+ <p><%= f.label :status %><br />
+ <%= f.text_field :status %></p>

ルートパスの画面上に現存ユーザと論理削除されたユーザの一覧を表示します。

$ vim app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
def index
@users = User.all
@deleted_users = User.only_deleted
end
end
$ vim app/views/welcome/index.html.erb
<h1>Welcome#index</h1>

<p>現存ユーザ</p>
<ul>
<% @users.each do |user| %>
<li><%= user.id %>:<%= user.name %>(<%= user.status %>)</li>
<% end %>
</ul>

<p>削除ユーザ</p>
<ul>
<% @deleted_users.each do |user| %>
<li><%= user.id %>:<%= user.name %>(<%= user.status %>)</li>
<% end %>
</ul>



実装が終わったので、実際に確認してみましょう。

画面の「登録」のリンクから登録ユーザ、削除ユーザの二人を登録します。

ここで、削除ユーザで画面の「ユーザ情報の編集」から「Cancel my account」をクリックして、削除します。

ここで、削除したユーザを復活させます。復活の作業はコンソールからやってしまいます。(deleted.findは引数にIDを入力してください。登録ユーザ、削除ユーザの順にやったので、今回は2です。)

$ rails console
irb(main):001:0> User.only_deleted.find(2).recover
irb(main):002:0> quit

ユーザが戻りました。


bcryptを用いたパスワード書き換え

deviseで生成されるパスワードの暗号方式をbcryptにします。

$ vim config/initializers/devise.rb
+ config.encryptor = :bcrypt

ここで、先ほど登録したユーザ二人のパスワードを消してしまいます。

$ sqlite3 db/development.sqlite3
sqlite> .header on
sqlite> update users set encrypted_password="";
sqlite> select * from users;
sqlite> .quit

ログインできなくなってしまったことを確認してください。

ここで、bcryptでパスワードを生成して代入してみましょう。

$ rails console
irb(main):001:0> p BCrypt::Password.create("hogefuga")
"$2a$10$XNCWLaHNFmTvMQkDxeqlMujEw7uSgHEY529oOSBp3B0aryk19oTQa"
=> "$2a$10$XNCWLaHNFmTvMQkDxeqlMujEw7uSgHEY529oOSBp3B0aryk19oTQa"
irb(main):002:0> quit

表示された、"hogefuga"の暗号化されたパスワードを直接データベースに代入します。

$ sqlite3 db/development.sqlite3 
sqlite> update users set encrypted_password="$2a$10$XNCWLaHNFmTvMQkDxeqlMujEw7uSgHEY529oOSBp3B0aryk19oTQa";
sqlite> .quit

先ほどログインできなかったユーザが、パスワードに"hogefuga"を用いるでログインできるようになりました。

この方法でしたら、明示したパスワードで機械的にユーザの情報を編集できるので、初期ユーザの初期パスワードを決めるといったことや、deviseの登録インターフェイス以外の方法でユーザを生成するといったことが可能になります。



rspec,factory_girl_railsを用いたテスト環境

RSpecといえば、Railsの駆動開発フレームワークでおなじみ。Factory girlは聞きなれない方もいるかと思いますが、fixtureをrubyのコードで生成できるgemライブラリです。

これらをテストするにあたって、まずはひとつモデルを追加しましょう。ついでにscaffoldの画面には、ログインしたユーザしか表示できないようフィルタをかけておきます。

$ rails g scaffold item name:string user_id:integer
$ vim app/controllers/items_controller.rb
class ItemsController < ApplicationController
+ before_filter :authenticate_user!

テストの前に簡単に使えることを確認しましょう。まずはモデルが増えたので、development環境のデータベースを更新しておきましょう。

$ rake db:migrate

次に、UserとItemの関連付け、及び画面上にitemのリストが表示されるようにしましょう。

$ vim app/models/user.rb
 + has_many :items
$ vim app/models/item.rb
 + belongs_to :user
$ vim app/controllers/welcome_controller.rb
  def index
@users = User.all
@deleted_users = User.only_deleted
@items = Item.all
end
$ vim app/views/welcome/index.html.erb
 +<p>アイテムリスト</p>
+<ul>
+<% @items.each do |item| %>
+<li><%= item.name %>(<%= item.user.name %>さん)</li>
+<% end %>
+</ul>

http://localhost:3000/itemsにアクセスして、Itemを追加したあと、ルート上で表示されるかどうか確認してください。追加するアイテムは、user_id指定なので、idを決め打ちしておいてください。例えば1など。

前準備はおわったので、いよいよtest環境の方を実装しましょう。まずはテストで用いるデータを記述します。

$ vim spec/factories.rb
require 'factory_girl'

Factory.define :user, :class => User do |f|
f.id 1
f.name 'Test User'
f.email 'user@test.com'
f.password 'foobar'
f.password_confirmation "foobar"
end

Factory.define :item, :class => Item do |f|
f.name "Test Item"
f.user_id 1
end

次にdeviseを用いたユーザモデルのテストを書くために、Deviseのヘルパーをincludeする必要があります。

$ mkdir spec/support
$ vim spec/support/devise.rb
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
end

テストを書く前に、テストにサインインの検証を書くため、少々冗長ですが一番わかりやすい方法として、indexアクション中にサインインの結果に応じて、ok,noを返す変数を作ります。

$ vim app/controllers/welcome_controller.rb
def index
...
+ @signin = user_signed_in? ? "ok" : "no"
end

テストコードをかきます。Factory.create(:user)で先ほど作ったテスト用のユーザデータを生成します。また、sign_inでログイン状態にしてしまうことが可能です(spec/support/devise.rbでのヘルパーのため)。最初のテストはサインインとしてアクセスできるかどうか、二つ目のテストは"Test Item"というItemがちゃんとfindできているかどうかを検証しています。

$ vim spec/controllers/welcome_controller_spec.rb 
require 'spec_helper'

describe WelcomeController do
before (:each) do
@user = Factory.create(:user)
sign_in @user
end

describe "GET 'index'" do
it "should be successful" do
get 'index'
response.should be_success
assigns[:signin].should == "ok"
# pp response
end

it "all items finded" do
Factory.create(:item)
get 'index'
response.should be_success
assigns[:items].count.should == 1
assigns[:items].first.name.should == "Test Item"
end
end
end

データベースを生成して、rspecを実行します。

$ rake db:create RAILS_ENV=test
$ rake db:migrate RAILS_ENV=test
$ rspec spec/controllers/welcome_controller_spec.rb
..

Finished in 0.58165 seconds
2 examples, 0 failures

出来ました。



deviseをオーバライドした、認証、退会の拡張実装

ユーザのログイン認証の方式の応用をやってみましょう。せっかくstatus属性を作ったので、statusが"active"のユーザしかログインできないようにします。

$ vim app/models/user.rb
 +  def self.find_for_authentication(conditions)
+ super(conditions.merge(:status => "active"))
+ end

ユーザ情報の編集でstatusをpendingにすると、ログインできなくなります。

次に退会時にItemの数をチェックして、Itemがあるようであれば、退会できないようにしてみます。

これは隠蔽されてるdeviseのcontroller部分にオーバーライドして記述する必要があります。

$ rails g controller Registrations
$ vim app/controllers/registrations_controller.rb
class RegistrationsController > Devise::RegistrationsController  
def destroy
count = current_user.items.count
if count != 0
redirect_to(:back, :alert => 'user.items != zero')
else
super
end
end
end

コードは書けたのですが、ルーティングを一部修正する必要があります。

$ vim config/routes.rb
 - devise_for :users
+ devise_for :users, :controllers => { :registrations => "registrations" }


また、registrationが呼び出すテンプレートが今までapp/views/devise/registrationsのものを活用していたのですが、今回のオーバーライドにより、app/views/registrationsから呼び出すようになるので、ファイルを移動する必要があります。

mv app/views/devise/registrations app/views/

先ほどアイテムを追加したときに関連付けたユーザでログインして、画面の"ユーザ情報の編集"から"Cancel my account"をしてみましょう。

アイテムを持つユーザは"user.items != zero"とエラーがでてログアウトできませんでした。



参考

http://www.func09.com/wordpress/archives/532
http://www.terut.net/?p=448
http://kitbc.s41.xrea.com/main/?use_devise
http://d.hatena.ne.jp/lounge1975/20110416/1302953405
http://d.hatena.ne.jp/choripon/20101229/1293634455
http://blog.livedoor.jp/nizoraul/archives/3555323.html
http://d.hatena.ne.jp/babie/20100729/1280381392
http://memo.yomukaku.net/entries/228
http://blog.codahale.com/2007/02/28/bcrypt-ruby-secure-password-hashing/
http://rorguide.blogspot.com/2011/04/overwrite-devise-registrations.html
http://rspec.rubyforge.org/rspec/1.3.0/