homeASCIIcasts

283: Sorceryで認証 

(view original Railscast)

Other translations: En Fr Es

Other formats:

Written by Naomi Fujimoto

エピソード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テンプレートも標準的なもので、新規ユーザを作成するフォームにemailpasswordpassword_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アクションがありますがコードを追加する必要がないので、次にテンプレートを見てみます。ここでは、簡単なログインフォームで、emailpasswordのためのテキストフィールドと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を検討してみる価値があるでしょう。