homeASCIIcasts

270: Rails 3.1の認証機能 

(view original Railscast)

Other translations: En Es De Fr

Other formats:

Written by Naomi Fujimoto

Rails 3.1には認証関連の新機能が含まれています。今回のエピソードではそのいくつかについて使い方を紹介します。実際に動作するところを見るために、URLを知っている人なら誰でもアクセスできるページを持つ簡単なアプリケーションを準備しました。

このページに機密管理を設定していく

HTTPベーシック認証

このページに参照権限を設定して、特定のユーザしかアクセスできないように制限をかけようと思います。そのためには認証機能を追加しなくてはいけませんが、一番早いのはHTTPベーシック認証を追加する方法です。Rails 3.1ではこのための新しい方法を提供しています。必要な作業は、ページのコントローラを修正して、http_basic_authenticate_withという新しいメソッドを呼び出すだけです。その時にオプションとして:name:passwordを指定します。

/app/controllers/secret_controller.rb

class SecretController < ApplicationController
  http_basic_authenticate_with :name => "frodo", :password => "thering"
  def index
  end
end

認証を一部のアクションのみに制限したい場合には、オプションで:only:exceptを指定します。言うまでもないですが、実際のアプリケーションではユーザ名とパスワードは素のビューのコード内に持たずに設定ファイル内に移すべきですが、このデモアプリではそのままで進めます。

ページにアクセスしてみるとログインダイアログが表示され、http_basic_authenticate_withで設定した正しいユーザ名とパスワードを入力しなければページを見ることができなくなりました。

ページが認証を求める

HTTPベーシックはもっと活用されるべきだと思います。サイトの一部へのアクセスに制限をかけたい場合には素早く簡単に設定できるのですが、Rails 3.1ではさらに簡単になっています。

secure_passwordを使う

しかし時にはより包括的な、複数ユーザに対応した権限付与のしくみが必要です。 その作業を簡単にするためにRails 3.1にsecure_passwordという機能が加わりました。

エピソード250 [動画を見る, 読む]では認証システムをゼロから作りましたが、Rails 3.1ではこの作業がかなり簡単にできるようになりました。今からこの作業を行い、HTTPベーシック認証を置き換えます。

まず最初に、Eメールアドレスとパスワードのフィールドを持つUserモデルを作成します。

$ rails g model user email:string password_digest:string

次にデータベースのマイグレーションを行い、テーブルを作成します。

$ rake db:migrate

ここで重要なのは、パスワードを格納するフィールドの名称をpassword_digestにすることです。次にUserモデルでhas_secure_passwordの呼び出しを追加します。

/app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password
end

これによって、入力されたパスワードを設定・認証するメソッドと、パスワードと確認パスワードを検証するバリデータと、認証機能が追加されます。前に作成したpassword_digestフィールドは、裏でパスワードのハッシュ値を保存するのに使われます。

デフォルトではパスワード用のvalidates_presence_of検証がないため、新規ユーザが作成されたときに起動される検証機能を追加する必要があります。

/app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password
  validates_presence_of :password, :on => :create
end

通常はEメールアドレスも検証するところですが、ここでは省略します。

ユーザによるアカウント作成を許可するためにUsersControllerを作成します。

$ rails g controller users

コントローラのコードは次のとおり標準的なものです。

/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ビューには、登録用フォームが含まれています。

/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 %>

登録時に確認用パスワードが一致しないと、has_secure_passwordからの検証メッセージが表示されます。

has_secure_passwordが自動的に確認用バリデータを表示する

確認用パスワードが一致すると登録が成功します。

登録は成功しますがログインはまだなので、次にEメールとパスワードフィールドを持ったログインフォームを作成します。

/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="actions"><%= submit_tag "Log in" %></div>
<% end %>

このフォームは、リソースであるテーブルデータを編集するためではないので、form_forではなくform_tagを使用しています。フォームはsessions_pathに送信されるので、新たにSessionsControllerを作成します。

$ rails g controller sessions

このコントローラは、new, create, destroyアクションを持ち、ユーザのログイン、ログアウトを制御します。

/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end

  def create
    if # authenticated?      session[:user_id] = user.id
      redirect_to root_url, :notice => "Logged in!"
    else
      flash.now.alert = "Invalid email or password"
      render "new"
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, :notice => "Logged out!"
  end
end

createアクションはまだ完成しておらず、ログインしたユーザが認証されているかどうかを判断するコードを、追加で記述する必要があります。ここで、Rails 3.1で追加されたsecure_passwordを利用することができます。

まず最初に、フォームに入力された値とEメールアドレスが一致するユーザを取得します。そのユーザに対して、has_secure_passwordで提供されるauthenticateメソッドを呼び出して、フォームで入力されたパスワードを渡します。このメソッドが、入力されたパスワードとデータベース内のパスワードダイジェストを比較します。一致するユーザが見つからなければfind_by_emailnilを返すので、認証の前にユーザが存在するかどうかをチェックします。

/app/controllers/sessions_controller.rb

def create
  user = User.find_by_email(params[:email])
  if user && user.authenticate(params[:password])
    session[:user_id] = user.id
    redirect_to root_url, :notice => "Logged in!"
  else
    flash.now.alert = "Invalid email or password"
    render "new"
  end
end

secure_passwordで認証する場合に必要なのはこれだけです。これをテストするためにログインしてみます。間違ったパスワードを入力すると、認証が失敗します。

ユーザ名かパスワードが違うとエラーが発生する

正しい認証情報でログインすると、認証が成功しトップページにリダイレクトされます。

アプリケーションの他の場所では、現在ログイン中のユーザを取得しなくてはいけないので、ApplicationControllercurrent_userメソッドを追加し、ビューからアクセスできるようにヘルパーメソッドにします。このメソッドは、セッションから現在のユーザを取得します。

/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery
  
  private
  
  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
  
  helper_method :current_user
end

この認証用メソッドの優れた点は、Userモデルがコード2行のみと非常にシンプルであることです。

/app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password
  validates_presence_of :password, :on => :create
end

これと比べると、エピソード250で紹介した方法ではよりずっと複雑なUserモデルを使用していました。そうは言うものの、もう一行だけ、attr_accessible行をモデルに追加して、ユーザ登録フォームからはEメール、パスワード、確認用パスワードのフィールドのみを編集できるようにしておくのがいいでしょう。

/app/models/user.rb

class User < ActiveRecord::Base
  attr_accessible :email, :password, :password_confirmation
  has_secure_password
  validates_presence_of :password, :on => :create
end

HTTPSを追加する

アプリケーションで認証を処理する場合、秘密情報を平文で送信したくないので、SSLを用いてHTTPSに切り替えるのがいいでしょう。Rails 3.1の前は、これを行うには手作業かプラグインを介す必要がありましたが、今はずっと簡単に実装する方法があります。

コントローラをHTTPSのみでアクセスされるよう制限をかけるには、ただ単にforce_sslクラスメソッドの呼び出しを追加するだけです。さらにコントローラ内のある特定のアクションのみに制限をかけるには、:onlyあるいは:exceptオプションを用いてbefore_filterと同じ要領で設定を行えます。

/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery
  force_ssl
  private
  
  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
  
  helper_method :current_user
end

force_sslメソッドはテスト(test)と本番(production)モードでのみHTTPSを強制します。アプリケーションのサーバを本番(production)モードで再起動して登録ページを再度読み込むと、SSL版にリダイレクトしようとします。

ページがSSLを求める

サーバがHTTPSをサポートしていないのでこれはエラーになりますが、もしサポートしていればこのページのセキュア版が表示されます。

Rails 3.1の認証機能についての今回のエピソードは以上です。認証に関する新しい追加機能によって、Railsアプリケーションに認証機能を実装するのがずっと簡単になりました。