283: Sorceryで認証
Other formats:
エピソード250[動画を見る, 読む]で、Railsアプリケーションにゼロから認証機能を追加しました。既存のサードパーティ製のツールを使いたい場合は、一部の機能を自動化してくれるgemがいくつかありますが、今回のエピソードではSorceryを紹介します。
Sorceryはシンプルなツールです。わずか20のメソッドを持つだけですが、およそ必要な認証機能のすべてを提供します。そのシンプルさに反してフル機能を持ち、またモジュール方式をとっているため、例えばパスワードの初期化、活動ログなど、必要な部品のみを選択して有効化できます。Sorceryは他の認証用gemと比較するとより低いレベルで動作し、コントローラ層やビュー層のコーディングは開発者にゆだねられています。今回のエピソードではこのSorceryを使って既存のRailsアプリケーションに認証機能を付加していきます。
はじめに
対象とするアプリケーションはいたってシンプルです。ウェルカム画面を持っていて、そこには秘密のページへのリンクがあります。現時点では秘密のページは誰でも見ることができますが、ログインしたユーザのみがアクセスできるように制限をかけようと思います。このためにはアプリケーションに認証機能を追加する必要があり、ここでSorceryが登場します。
Sorceryはgemとして提供され、通常の方法でインストールを行います。Gemfileに参照情報を追加し、bundleコマンドを実行します。
/Gemfile
gem 'sorcery'
Bundlerが終了したら、Sorceryの初期化ファイルを追加するために次のコマンドを実行します。(この詳細は後ほど説明します。)
$ rake sorcery:bootstrap
次にsorcery_migrationを作成します。これを使って、追加したいSorceryモジュールを選択します。ここでは、単純なパスワード認証に必要なcoreモジュールと、remember_meモジュールをインクルードします。すべてのモジュールのリストを参照するためには、SorceryのREADMEをチェックしてください。
$ rails g sorcery_migration core remember_me
create db/migrate/20110914221626_sorcery_core.rb
create db/migrate/20110914221627_sorcery_remember_me.rb
コマンドを実行すると、選択したモジュールに応じていくつかのmigrationファイルが生成されます。sorcery_coreのmigrationを見ると、新しいusersテーブルに追加される属性が確認できます。
/db/migrate/20110914221626_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :username, :null => false
t.string :email, :default => nil
t.string :crypted_password, :default => nil
t.string :salt, :default => nil
t.timestamps
end
end
def self.down
drop_table :users
end
end
デフォルトではmigrationはusernameフィールドを作成します。usernameフィールドはemailフィールドと同じく必要ないため、該当行をコメントアウトします。
/db/migrate/20110914221626_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration
def self.up
create_table :users do |t|
# t.string :username, :null => false
t.string :email, :default => nil
t.string :crypted_password, :default => nil
t.string :salt, :default => nil
t.timestamps
end
end
end
Sorceryの設定を変更して、 usernameの代わりにemailを使用するよう指定します。このためにSorceryの初期化ファイルを修正します。このファイルの最初で、有効化したいモジュールを特定します。coreモジュール以外で使用するのはremember_meモジュールだけですが、ここでそれを追加します。
/config/initializers/sorcery.rb
# The first thing you need to configure is which modules you need in your app. # The default is nothing which will include only core features (password encryption, login/logout). # Available submodules are: :user_activation, :http_basic_auth, :remember_me, # :reset_password, :session_timeout, :brute_force_protection, :activity_logging, :external Rails.application.config.sorcery.submodules = [:remember_me] # Rest of file omitted.
他にもここで指定できる設定オプションがあり、それらはファイル内でわかりやすく説明されています。ほとんどは変更する必要はありませんが、ひとつだけusername_attribute_nameは変更する必要があります。このオプションを有効化して、その値をユーザを特定するのに使うフィールドである:emailに変更します。
/config/initializers/sorcery.rb
config.user_config do |user| # -- core -- user.username_attribute_name = :email # change default username # attribute, for example, # to use :email as the login. # Other options omitted. end # This line must come after the 'user config' block. config.user_class = "User" # define which model authenticates # with sorcery. end
ファイルの最後には、Sorceryが認証に利用するモデルの名前を指定するための設定項目があり、デフォルトではUserになっています。このアプリケーションにはまだUserモデルがないため、ここで作成することにします。すでにUserのフィールドを定義するmigrationファイルがあるので、Railsに対してモデル作成時にそれを生成しないように指定します。
$ rails g model user --skip-migration
UserモデルでSorceryを有効化するために、コードを一行追加します。
/app/models/user.rb
class User < ActiveRecord::Base authenticates_with_sorcery!end
これにより、Userモデルで認証を扱えるようにいくつかのメソッドが追加されます。しかし、検証(validation)や属性を保護する機能は追加されません。それらを追加するかどうかは開発者にゆだねられます。
/app/models/user.rb
class User < ActiveRecord::Base authenticates_with_sorcery! attr_accessible :email, :password, :password_confirmation validates_confirmation_of :password validates_presence_of :password, :on => :create validates_presence_of :email validates_uniqueness_of :email end
ここでmigrationを実行し、usersテーブルを作成します。
$ rake db:migrate
コントローラとビューを追加する
Userモデルの準備ができたので、対応するコントローラを設定します。まず登録手続きを処理するUsersControllerを作成します。
$ rails g controller users new
ログイン処理をおこなうSessionsControllerも作成します。
$ rails g controller sessions new
これは、エピソード250で認証機能をゼロからに作ったときの作業に似ているので、ここは簡単に触れるだけにします。UsersControllerは標準的なもので、newアクションでUserを新規作成し、 createアクションでは渡されたパラメータからUserを新規作成します。
/app/controllers/users_controller.rb
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テンプレートも標準的なもので、新規ユーザを作成するフォームにemail、password、password_confirmationの各フィールドと、検証エラーを表示するためのコードが含まれています。
/app/views/users/new.html.erb
<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 %>
<div class="field">
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
</div>
<div class="actions"><%= f.submit %></div>
<% end %>
SessionsControllerの中身は、より興味深いものになっています。newアクションがありますがコードを追加する必要がないので、次にテンプレートを見てみます。ここでは、簡単なログインフォームで、emailとpasswordのためのテキストフィールドとremember_meフィールドのためのcheckboxを持ったものを作ります。
/app/views/sessions/new.html.erb
<h1>Log in</h1>
<%= form_tag sessions_path do %>
<div class="field">
<%= label_tag :email %>
<%= text_field_tag :email, params[:email] %>
</div>
<div class="field">
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<div class="field">
<%= check_box_tag :remember_me, 1, params[:remember_me] %>
<%= label_tag :remember_me %>
</div>
<div class="actions"><%= submit_tag "Log in" %></div>
<% end %>
ログインフォームを処理するためのcreateアクションを作成します。Sorceryではloginというメソッドが提供され、 3つのパラメータをとります。ユーザ名かメールアドレス、入力されたパスワード、remember_meチェックボックスの値です。このメソッドが認証を行い、一致するものが見つかった場合にそのUserを返します。これを確認して、ユーザが見つかったらトップページにリダイレクトします。見つからなかったら、フラッシュメッセージと共にログインフォームを再度表示します。
/app/views/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = login(params[:email], params[:password], ↵
params[:remember_me])
if user
redirect_back_or_to root_url, :notice => "Logged in!"
else
flash.now.alert = "Email or password was invalid."
end
end
end
ユーザが見つかった場合にトップページにリダイレクトさせるときに、redirect_toを使う代わりに、Sorcery が提供するredirect_back_or_toというメソッドを使います。このメソッドはredirect_toと同じように振るまいますが、SorceryがURLを保存している場合は、コードで指定したURLではなく保存されたURLにリダイレクトします。これで便利なのは、あるページにアクセスしたユーザをまずログインさせたいという場合に、Sorceryがそのユーザをログインページに誘導し、ログインが成功したら最初に訪れようとしていたページに戻してくれる点です。
ユーザがログアウトする手段も提供しなくてはいけないので、コントローラにdestroyアクションも追加します。Sorceryにはlogoutというメソッドがあり、これを呼び出すことでユーザをログアウトさせることができます。ユーザがログアウトした後は、トップページにリダイレクトします。
/app/views/controllers/sessions_controller.rb
def destroy logout redirect_to root_url, :notice => "Logged out!" end
次にroutesファイルを開いて、すべてをつなげるために、デフォルトの自動生成されたアクションを次のルートで置き換えます。
/config/routes.rb
Auth::Application.routes.draw do get "logout" => "sessions#destroy", :as => "logout" get "login" => "sessions#new", :as => "login" get "signup" => "users#new", :as => "signup" resources :users resources :sessions get "secret" => "home#secret", :as => "secret" root :to => "home#index" end
認証に関連する機能を処理するいくつかの名前付きルート(named routes)やリソース(resources)があります。
新しく作成したこれらのページにユーザがアクセスできるように、新規にリンクが必要です。それらをレイアウトページに追加して、アプリケーションのすべてのページから見えるようにします。current_userメソッドを使って、ユーザがログインがしているかどうかを確認できます。もしログインしていたら、メールアドレスをログアウトのリンクと一緒に表示します。もし現在のユーザがいない場合は、登録かログインをさせるためのページへのリンクを表示します。
/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Auth Example</title>
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body class="<%= params[:controller] %>">
<div id="container">
<div class="user_nav">
<% if current_user %>
Logged in as <%= current_user.email %>.
<%= link_to "Log out", logout_path %>
<% else %>
<%= link_to "Sign up", signup_path %> or
<%= link_to "Log in", login_path %>.
<% end %>
</div>
<% flash.each do |name, msg| %>
<%= content_tag :div, msg, :id => "flash_#{name}" %>
<% end %>
<%= yield %>
</div>
</body>
</html>
サイトをテストする準備ができました。トップページにアクセスすると、登録とログインのリンクが表示されています。
登録をクリックすると登録フォームが表示され、そのサイトに対してユーザ登録を行うことができます。その後、ログインをクリックして、登録した情報でログインすることができます。
権限を与える
サイトにログインしたので、秘密のページにアクセスすることができます。ログアウトしてから秘密のページにアクセスしてみると、まだページを見ることができます。そこで権限を付与する機能を追加して、ログインユーザだけがページにアクセスできるよう制限をかけます。
秘密の(secret)ページは、HomeController内のアクションです。Sorceryが提供するrequire_loginというbefore_filterを使えば、アクションへのアクセスを制限することが可能です。これを使ってsecretアクションへのアクセスを制限します。
/app/controllers/home_controller.rb
class HomeController < ApplicationController before_filter :require_login, :only => :secret def index end def secret end end
このフィルターが起動されると、Sorceryはnot_authenticatedメソッドを呼び出します。ApplicationControllerでこのメソッドをオーバーライドするのがいいでしょう。そうすることによって、権限が付与されなかったときの動作を制御することができます。ログインページにリダイレクトして警告メッセージを表示します。
/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
private
def not_authenticated
redirect_to login_url, :alert => "First log in to view ↵
this page."
end
end
ログアウトした状態で再度秘密のページにアクセスしてみると、ログインページにリダイレクトされてそこには警告メッセージが表示されています。
ログインすると秘密のページにリダイレクトされます。これはログインページにリダイレクトされたときに見ようとしていたページをSorceryが憶えているからです。
Sorceryに関する今回のエピソードは以上です。今回紹介した以外にも多くの機能があるので、ドキュメンテーションを参照してください。少し低いレベルで動作する認証ソリューションを探している場合は、Sorceryを検討してみる価値があるでしょう。



