Help us understand the problem. What is going on with this article?

Authlogic + Railsで認証の仕組みをシンプルに作る

More than 1 year has passed since last update.

Authlogicというgemを使ってRailsに認証の仕組みを作る方法を紹介します。Authlogicはシンプルなライブラリで、Deviseのようにviewやcontrollerは提供してくれません。自分でマイグレーションファイルを作って、コントローラと認証画面を用意してあげる必要があります。面倒くさそうに感じますが、自分で作りたいようにRailsに乗って作れるという良さがあります。シンプルです。

認可に関してはPundit + Railsで認可の仕組みをシンプルに作るを参考にしてみてください。

前提

  • Rails 5.1
  • Ruby 2.4
  • Authlogic 3.5.0
  • RSpec 3.5

紹介する実装例

  • メールアクチベーション
  • パスワードリセット
  • WebAPIキー
  • テストコード

最低限の実装

まずは単純にユーザ登録とログインの実装をしてみます。

マイグレーション

ユーザ認証に必要なカラムを追加します。認証させたいモデルが存在しない場合は作ります。今回はUserモデルを作ることにします。

$ rails g model user email crypted_password password_salt persistence_token

ログイン情報などの記録もしたい場合は下記のようにマイグレーションファイルを手動で修正してください。これらのフィールドはAuthlogicが自動でアップデートしてくれます。

class CreateUser < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string    :email
      t.string    :crypted_password
      t.string    :password_salt
      t.string    :persistence_token

      # ログイン情報など
      t.integer   :login_count, default: 0, null: false
      t.integer   :failed_login_count, default: 0, null: false
      t.datetime  :last_request_at
      t.datetime  :current_login_at
      t.datetime  :last_login_at
      t.string    :current_login_ip
      t.string    :last_login_ip

      t.timestamps
    end
  end
end

Authlogicをモデルに適用

app/models/user.rb

UserモデルにAuthlogicを適用します。

class User < ApplicationRecord
  acts_as_authentic
end

acts_as_authentic のブロックでオプションを渡せます。例えば暗号アルゴリズムを変更したい場合。

class User < ApplicationRecord
  acts_as_authentic do |c|
    c.crypto_provider = Authlogic::CryptoProviders::BCrypt
  end
end

なお、デフォルトでは SCrypt になっています。

app/models/user_session.rb

認証のセッションで使うモデルファイルを作成します。これはActiveRecordではなく、 Authlogic::Session::Base を継承するクラスファイルです。

class UserSession < Authlogic::Session::Base
  secure Rails.env.production?
  httponly true
end

ここでいくつかのオプションを設定できます。例はプロダクション環境ではCookieにsecure属性を付けて、どの環境でもhttponly属性を付ける設定になっています。これ以外の設定はドキュメントを参照してください。

ユーザ登録ページ

app/controllers/users/registrations_controller.rb

パット見て普通のRailsで作るようなcontrollerと同じです。ここが良いです。誰でも分かるし、変更もしやすい。必要なのは入力画面と作成アクションだけなので newcreate だけ用意しています。

class User::RegistrationsController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      redirect_to "/"
    else
      render action: :new
    end
  end

  private
    def user_params
      params.require(:user).permit(:email, :password, :password_confirmation)
    end
end

app/views/user/registrations/new.html.erb

<%= form_for @user, url: user_registrations_path do |f| %>
  <p>
    <%= f.label :email %>
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </p>
  <p>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </p>
  <%= f.submit "sign up" %>
<% end %>

config/routes.rb

Rails.application.routes.draw do
  namespace :user do
    resources :registrations, only: [:new, :create]
  end
end

以上でユーザ登録の画面ができました。email などのバリデーションはAuthlogicが提供しくれるので、特に何もしなくても大丈夫です。

ログイン画面

app/controllers/user/sessions_controller.rb

次はログイン画面です。こちらも先程の登録と同じような感じです。違いはUserSessionクラスにパラメータを投げるだけです。 current_user_session というメソッドは後でApplicationControllerに定義します。

class User::SessionsController < ApplicationController
  def new
    @user_session = UserSession.new
  end

  def create
    @user_session = UserSession.new(user_session_params.to_h)
    if @user_session.save
      redirect_to "/"
    else
      render :new
    end
  end

  def destroy
    current_user_session.destroy
    redirect_to "/"
  end

  private

  def user_session_params
    params.require(:user_session).permit(:email, :password)
  end
end

app/views/user/sessions/new.html.erb

<%= form_for @user_session do |f| %>
  <% if @user_session.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@user_session.errors.count, "error") %> prohibited:</h2>
    <ul>
      <% @user_session.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
  <% end %>
  <%= f.label :email %><br />
  <%= f.text_field :email %><br />
  <br />
  <%= f.label :password %><br />
  <%= f.password_field :password %><br />
  <br />
  <%= f.submit "Login" %>
<% end %>

config/routes.rb

Rails.application.routes.draw do
  get "sign_in" => "user/sessions#new"
  delete "sign_out" => "user/sessions#destroy"
  namespace :user do
    resources :registrations, only: [:new, :create]
    resources :sessions, only: :create
  end
end

controllers/application_controller.rb

Deviseで提供されている current_user メソッドと同様のことができると便利なので作ります。UserSessionクラスに問い合わせるといまアクセスしているユーザのCookieを読み取ってセッション情報を作ってくれます。その中にUserモデルのオブジェクトが入っているので、それを返しているだけです。

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  helper_method :current_user_session, :current_user

  private
    def current_user_session
      return @current_user_session if defined?(@current_user_session)
      @current_user_session = UserSession.find
    end

    def current_user
      return @current_user if defined?(@current_user)
      @current_user = current_user_session && current_user_session.user
    end

    # ログイン必須controllerのbefore_actionで呼ぶ
    def require_login
      return redirect_to sign_in_path unless current_user
    end
end

ログイン済み判定

Userモデルの persistence_token と、Cookieの user_credentials を比較しています(モデル名によってCookieKeyは変わる)。

Cookieの user_credentials には persistence_tokenrecord_id:: で連結されていて、 record_id をもとにDBに保存されている persistence_token を探して一致すれば認証という流れになっている。


以上で、ユーザ認証の実装は終了です。ソースコードだけで見ると面倒くさそう…って思うかもしれませんが、やってみると普段Railsで開発しているのとさほど変わらない程度の実装です。見えないところが少ないのでコントロールしやすいです。

ユーザステータス

Authlogic::Session::MagicStates クラスがユーザのステータスを管理する機能を提供しています。 active / approved / confirmed の3つ。それぞれ user.active? などと問い合わせることができます。enumっぽい。メールでのアクチベーションを実装する場合や、運営側からの承認フラグを実装したいときなどに利用します。DBにフィールドが必要なので、使いたい場合はマイグレーションファイルを作成します。

$ rails g migration AddStatesToUser active:boolean approved:boolean confirmed:boolean
# Authlogic::Session::MagicStates
t.boolean   :active, default: false
t.boolean   :approved, default: false
t.boolean   :confirmed, default: false

これらフィールドのどれかがfalseだとAuthlogicでログインを拒否する動作になっています。なので自前で管理する必要はありません。

メールアクチベーション

メールアドレスの存在確認をするために、本登録用メールを送信して記載されているURLにアクセスするとアカウントを有効化したい場合があります。それも割りとサクッと実装できます。前述した MagicStatesactive フィールドを利用します。

またアクチベーションキーとして perishable_token というフィールドを利用するので、こちらも追加します。

$ rails g migration AddActiveToUser active:boolean perishable_token
class AddActiveToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :active, :boolean, null: false, default: false
    add_column :users, :perishable_token, :string
  end
end

app/controllers/user/mail_activate_controller.rb

User.find_using_perishable_token は渡された文字列を user.perishable_token と比較して該当ユーザが指定期間以内に作られたかどうか(updated_atフィールド)を判断しています。つまりトークンの有効期限みたいな感じですね。

class User::MailActivateController < ApplicationController

  def create
    @user = User.find_using_perishable_token(params[:activate_code], 1.week)
    raise ActiveRecord::RecordNotFound unless @user

    if @user.activate!
      flash[:notice] = "Your account has been activated!"
      UserSession.create(@user, false) # Log user in manually
      redirect_to "/"
    end
  end

end

app/controllers/user/registrations_controller.rb

     if @user.save
 +     @user.deliver_mail_activate_instructions!
       redirect_to "/"
     else

app/models/user.rb

認証メール送信メソッドと、アクチベーションするメソッドを定義します。

class User < ApplicationRecord
  acts_as_authentic

  def deliver_mail_activate_instructions!
    NotifierMailer.mail_activate_instructions(self).deliver_now
  end

  def activate!
    self.active = true
    save
  end
end

app/mailers/notifier_mailer.rb

class NotifierMailer < ActionMailer::Base
  default from: 'notifications@example.com'

  def mail_activate_instructions(user)
    @user = user
    mail(to: user.email, subject: "Mail Activate Instructions")
  end
end

app/views/notifier_mailer/mail_activate_instructions.html.erb

アクチベーションは perishable_token フィールドの値を見て行うので、トークンを付与したURLをメールで送付します。

<%= user_mail_activate_url(@user.perishable_token) %>

config/routes.rb

  namespace :user do
 +    get "mail_activate/:activate_code", controller: "mail_activate", action: "create", as: "mail_activate"
  end

パスワードリセット(リマインダー)

メールアクチベーションでも利用した perishable_token をパスワード再設定用にも利用します。仕様としては、パスワードリセット画面にメールアドレスを入力してもらって存在すれば、該当メールに対して perishable_token 付きのリセットURLを送付します。そのURLにアクセスすると、新しいパスワードが設定できるようにする感じにします。

app/controllers/user/password_resets_controller.rb

メールアドレスを入力する画面と、パスワードを再設定する画面が必要なのでそれぞれのアクションを作ります。

class User::PasswordResetsController < ApplicationController
  before_filter :load_user_using_perishable_token, :only => [ :edit, :update ]

  def new
  end

  def create
    @user = User.find_by_email(params[:email])
    if @user
      @user.deliver_password_reset_instructions!
      flash[:notice] = "Instructions to reset your password have been emailed to you"
      redirect_to "/"
    else
      flash.now[:error] = "No user was found with email address #{params[:email]}"
      render :new
    end
  end

  def edit
  end

  def update
    @user.password = params[:password]
    @user.password_confirmation = params[:password]

    # 更新後、自動ログインさせたくないのであれば下記を使う
    # if @user.save_without_session_maintenance
    if @user.save
      flash[:success] = "Your password was successfully updated"
      redirect_to "/"
    else
      render :edit
    end
  end


  private

  def load_user_using_perishable_token
    @user = User.find_by_perishable_token(params[:id])
    unless @user
      flash[:error] = "We're sorry, but we could not locate your account"
      redirect_to "/"
    end
  end
end

app/mailers/notifier_mailer.rb

 +  def password_reset_instructions(user)
 +    @user = user
 +    mail(to: user.email, subject: "Password Reset Instructions")
 +  end

app/models/user.rb

reset_perishable_token! を呼んで強制的にperishable_tokenを更新します。これをしないとURLが流出してしまった時に好きなようにパスワードを変えられてしまいます。ちなみにAuthlogicはデフォルトでサインイン・アウト時にperishable_tokenを更新します。

 +  def deliver_password_reset_instructions!
 +    reset_perishable_token!
 +    NotifierMailer.password_reset_instructions(self).deliver_now
 +  end

app/views/notifier_mailer/password_reset_instructions.html.erb

<%= edit_user_password_reset_url(@user.perishable_token) %>

app/views/user/password_resets/new.html.erb

<%= form_tag user_password_resets_path do %>
  <%= text_field_tag :email %>
  <%= submit_tag "Reset Password" %>
<% end %>

app/views/user/password_resets/edit.html.erb

<%= form_tag user_password_reset_path, :method => :put do %>
  <%= password_field_tag :password %>
  <%= submit_tag "Update Password" %>
<% end %>

config/routes.rb

  namespace :user do
 +    resources :password_resets, onlye: [:new, :create, :edit, :update]
  end

WebAPIキー

ユーザ情報にアクセスするようなWebAPIを開発した時に認証が必要になりますが、意外と面倒です。簡易なAPIキーで良ければAuthlogicで提供されているSingle access tokenを利用できます。

まず single_access_token フィールドを利用するので作ります。

$ rails g migration AddSingleAccessTokenToUser single_access_token
class AddSingleAccessTokenToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :single_access_token, :string, null: false
  end
end

あとはUserSessionクラスに single_access_allowed_request_types オプションを渡せば完了です。

class UserSession < Authlogic::Session::Base
  single_access_allowed_request_types :all
  # クエリストリングのキーを変更したい場合(デフォルトは user_credentials)
  # params_key :api_key
end

これで以下のようなURLでログイン済み扱いとして振る舞えます。

テストコード

認証周りは不安要素が多いのでテストしておきたいですよね。今回はリクエストテストを書くことにしました。認証ロジック自体はAuthlogicでテストされているので、利用者側としては自分で実装したログイン画面をテストする方針にします。なおRail5からコントローラテストよりもリクエストテストを推奨しているので、コントローラテストは書きません。

今回はRSpecとFactoryGirlを使います。

単純にログイン画面にIDとパスワードをPOSTすればいいので下記のように書きます。

def login admin
  post new_sign_in_url, :params => { user_session: { :email => user.email, :password => "password" } }
end

login create(:user)

そしてログインできるできないのテストを書いてみます。

require 'rails_helper'

RSpec.describe "Console::Hoge", type: :request do
  describe "GET /console/hoge" do
    it "ログインしていないとアクセスできない" do
      get console_hoge_path
      expect(response).to have_http_status(302)
    end

    it "ログインするとアクセスできる" do
      login create(:user)
      get console_hoge_path
      expect(response).to have_http_status(200)
    end
  end
end

以上です。提供していることがシンプルなので理解もしやすく使いやすいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした