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と同じです。ここが良いです。誰でも分かるし、変更もしやすい。必要なのは入力画面と作成アクションだけなので new
と create
だけ用意しています。
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_token
と record_id
が ::
で連結されていて、 record_id
をもとにDBに保存されている persistence_token
を探して一致すれば認証という流れになっている。
- 該当コード: authlogic/session/cookies.rb
以上で、ユーザ認証の実装は終了です。ソースコードだけで見ると面倒くさそう…って思うかもしれませんが、やってみると普段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にアクセスするとアカウントを有効化したい場合があります。それも割りとサクッと実装できます。前述した MagicStates
の active
フィールドを利用します。
またアクチベーションキーとして 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
以上です。提供していることがシンプルなので理解もしやすく使いやすいと思います。