Monban どうでしょう

はじめに

はじめまして。 kbaba1001 と申します。 Ruby on Rails を用いた受託開発を主な仕事としています。

今回は Monban という gem を紹介したいと思います。

Monban は Ruby on Rails で認証機能を作るための gem です。 同系統の gem として DeviseSorcery があります。 Monban の README では次の 3 点の特徴を挙げています。

  • 依存性の注入を利用することでテストしやすくする
  • 便利な Controller Helpers を提供する
  • カスタマイズしやすい

一方で次の 3 点は行わないことになっています。

  • routes を自動的に追加しない
  • Rails Engine ベースの Controller や View を強制しない
  • User Model の変更を強制しない

Devise は Rails Engine を利用しているため導入するだけで多くの機能を使えるようになりますが、カスタマイズを行うとコードが汚くなりやすいというデメリットがあります。

Monban では少数の public メソッドを Controller Helpers として提供し、これらのメソッドの実装をサービス層で行うことでカスタマイズしやすくなっています。 セッションの操作には Devise と同様に Warden を利用しているため、 Devise をカスタマイズしたことがある人であれば Warden の知識を使うことができます。

また、Monban::Generators という gem を使うことで Monban を利用した認証機能を Scaffold により作成することができるため、 Sorcery と比べて導入しやすくなっています。

Monban を導入する

実際に Rails アプリケーションを作りながら Monban による基本的な認証機能を実装する方法について説明します。 まず rails new します。

  1| $ rails new sample

Gemfile に次の行を追加します。

   1|gem 'monban'
   2|gem 'monban-generators'

monban-generators は必須ではありませんが、 Monban を利用した認証機能を Scaffold で作成するための gem なので、 Monban の導入が楽になります。

簡単な認証機能の実装

次のコマンドを実行して認証機能を生成します。

  1| $ rails g monban:scaffold
  2|        route  resources :users, only: [:new, :create]
  3|        route  resource :session, only: [:new, :create, :destroy]
  4|       create  app/views/users/new.html.erb
  5|       create  app/views/sessions/new.html.erb
  6|       create  app/controllers/sessions_controller.rb
  7|       create  app/controllers/users_controller.rb
  8|       insert  app/controllers/application_controller.rb
  9|       create  app/models/user.rb
 10|       create  db/migrate/20150720120736_create_users.rb
 11|       create  config/locales/monban.en.yml
 12| 
 13|     Final Steps
 14|     run:
 15|       rake db:migrate

生成されたファイルについて説明します。

まず、app/controllers/application_controller.rb を開くと、次のように include Monban::ControllerHelpers が追加されています。

  1| class ApplicationController < ActionController::Base
  2| +  include Monban::ControllerHelpers
  3|   # Prevent CSRF attacks by raising an exception.
  4|   # For APIs, you may want to use :null_session instead.
  5|   protect_from_forgery with: :exception
  6| end

Monban::ControllerHelpers には次のメソッドが定義されています。

  • Controller メソッド
    • sign_in(user)
    • sign_out
    • sign_up(user_params)
    • authenticate(user, password)
    • authenticate_session(session_params)
    • reset_password(user, password)
  • Helper メソッド
    • current_user
    • signed_in?
  • filter メソッド
    • require_login

他のコントローラではこれらのメソッドを使って認証機能を実装します。 例えば app/controllers/users_controller.rb では次のようにサインアップ機能を実装しています。

   1|class UsersController < ApplicationController
   2|  skip_before_action :require_login, only: [:new, :create]
   3|
   4|  def new
   5|    @user = User.new
   6|  end
   7|
   8|  def create
   9|    @user = sign_up(user_params)
  10|
  11|    if @user.valid?
  12|      sign_in(@user)
  13|      redirect_to root_path
  14|    else
  15|      render :new
  16|    end
  17|  end
  18|
  19|  private
  20|
  21|  def user_params
  22|    params.require(:user).permit(:email, :password)
  23|  end
  24|end

create メソッド内では Monban::ControllerHelpers が提供する sign_upsign_in メソッドに認証関係の処理を任せています。 これにより create メソッドでは HTTP リクエストを受け取ってレスポンスを返すことに集中しています。 コントローラとビジネスロジックが切り離されているのでメンテナンスしやすくなっています。

拡張の方法

Monban::ControllerHelpers が提供しているメソッドはビジネスロジックと切り離されています。 例えば Monban::ControllerHelpers#sign_up は次のように実装されています。

   1|module Monban
   2|  module ControllerHelpers
   3|    # Sign up a user
   4|    #
   5|    # @note Uses the {Monban::Services::SignUp} service to create a user
   6|    #
   7|    # @param user_params [Hash] params containing lookup and token fields
   8|    # @yield Yields to the block if the user is signed up successfully
   9|    # @return [Object] returns the value from calling perform on the {Monban::Services::SignUp} service
  10|    def sign_up user_params
  11|      Monban.config.sign_up_service.new(user_params).perform.tap do |status|
  12|        if status && block_given?
  13|          yield
  14|        end
  15|      end
  16|    end
  17|
  18|# 後略

Monban::ControllerHelpers#sign_up では Monban.config.sign_up_service が返すクラスを new して perform メソッドを呼び出しています。 perform の戻り値についてはコメントで Object を返すように指示されています。

つまり、Monban.config.sign_up_service は次の条件を満たせばどのように実装しても良いということになります。

  • initialize の引数として user_params をとること
  • perform メソッドを実装していること
  • perform メソッドは何かしらの Object を返すこと

Monban.config.sign_up_service はデフォルトでは Monban::Services::SignUp が設定されています。 Monban::Services::SignUp は monban 中の /lib/monban/services/sign_up.rb で次のように定義されています。

   1|module Monban
   2|  module Services
   3|    # Sign up service. Signs the user up
   4|    # @since 0.0.15
   5|    class SignUp
   6|      # Initialize service
   7|      #
   8|      # @param user_params [Hash] A hash of user credentials. Should contain the lookup and token fields
   9|      def initialize user_params
  10|        digested_token = token_digest(user_params)
  11|        @user_params = user_params.
  12|          except(token_field).
  13|          merge(token_store_field.to_sym => digested_token)
  14|      end
  15|
  16|      # Performs the service
  17|      # @see Monban::Configuration.default_creation_method
  18|      def perform
  19|        Monban.config.creation_method.call(@user_params.to_hash)
  20|      end
  21|
  22|      private
  23|
  24|      def token_digest(user_params)
  25|        undigested_token = user_params[token_field]
  26|        unless undigested_token.blank?
  27|          Monban.hash_token(undigested_token)
  28|        end
  29|      end
  30|
  31|      def token_store_field
  32|        Monban.config.user_token_store_field
  33|      end
  34|
  35|      def token_field
  36|        Monban.config.user_token_field
  37|      end
  38|    end
  39|  end
  40|end

initialize では引数に user_params を受け取って、 password をハッシュ化して @user_params に代入します。 perform メソッドでは @user_params から User を create してオブジェクトを返します。 これにより Monban::ControllerHelpers#sign_upMonban.config.sign_up_service に期待する振る舞いを満たしていることがわかります。

もし Monban.config.sign_up_serviceMonban::Services::SignUp 以外にしたい場合、 config/initializers/monban.rb を作成して、次のように設定します。

   1|Monban.configure do |config|
   2|  config.sign_up_service = MySignUp
   3|end

MySignUp クラスを独自に作成すれば、 Monban::ControllerHelpers#sign_up を実行した際に呼び出されます。 これにより app/controllers/users_controller.rb を変更することなくサインアップに関する機能を変更することができます。

拡張の例

拡張の例として、サインアップ時に「パスワードの確認」を入力できるようにしてみましょう。 rails g monban:scaffold で作成されたサインアップ機能では、email アドレスとパスワードを 1 度入力するだけなので、 2 度入力できるようにします。

まず View を変更します。 app/views/users/new.html.hamlpassword_confirmation を入力するフォームを追加します。

  1| = form_for @user do |form|
  2|   - if @user.errors.any?
  3|     = pluralize(@user.errors.count, "error")
  4|     prevented your account from being created:
  5|     %ul
  6|       - @user.errors.full_messages.each do |error_message|
  7|         %li= error_message
  8|   %div
  9|     = form.label :email
 10|     = form.email_field :email
 11|   %div
 12|     = form.label :password
 13|     = form.password_field :password
 14| +  %div
 15| +    = form.label :password_confirmation
 16| +    = form.password_field :password_confirmation
 17|   %div
 18|     = form.submit "Sign up"

次に、モデルを拡張します。現在、 users テーブルのスキーマは下記のようになっています。

   1|create_table "users", force: :cascade do |t|
   2|  t.string   "email",           null: false
   3|  t.string   "password_digest", null: false
   4|  t.datetime "created_at",      null: false
   5|  t.datetime "updated_at",      null: false
   6|end

データベースには password_digest を書き込めれば良いので、スキーマは変更せずに app/models/user.rb を下記のように書き換えます。

  1| class User < ActiveRecord::Base
  2|   validates :email, presence: true, uniqueness: true
  3| 
  4| +  attr_accessor :password, :password_confirmation
  5| +  validates_presence_of :password, :password_confirmation
  6| +  validates_confirmation_of :password
  7| end

app/controllers/users/users_controller.rb の Strong Parameters で password_confirmation を受け入れるようにします。

  1| class Users::UsersController < Users::ApplicationController
  2|   skip_before_action :require_login, only: [:new, :create]
  3| 
  4|   def new
  5|     @user = User.new
  6|   end
  7| 
  8|   def create
  9|     @user = sign_up(user_params)
 10| 
 11|     if @user.valid?
 12|       sign_in(@user)
 13|       redirect_to root_path
 14|     else
 15|       render :new
 16|     end
 17|   end
 18| 
 19|   private
 20| 
 21|   def user_params
 22| -    params.require(:user).permit(:email, :password)
 23| +    params.require(:user).permit(:email, :password, :password_confirmation)
 24|   end
 25| end

上記のコントローラの create 部分からわかるように、サインアップに関するビジネスロジックは sign_up メソッドに切り出されています。 これは前節で説明したとおり Monban.config.sign_up_service に設定したクラスに処理を委譲します。 /config/initializers/monban.rb で独自のクラスを使うように設定します。

   1|require Rails.root.join('app/services/sign_up')
   2|
   3|Monban.configure do |config|
   4|  config.sign_up_service = Services::SignUp
   5|end

app/services/sign_up.rb というファイルを次の内容で作成します。

   1|# app/services/sign_up.rb
   2|module Services
   3|  class SignUp
   4|    def initialize(user_params)
   5|      @user_params = user_params
   6|    end
   7|
   8|    def perform
   9|      User.new(@user_params) {|user|
  10|        user.update(password_digest: Monban.hash_token(@user_params[:password])) if user.valid?
  11|      }
  12|    end
  13|  end
  14|end

perform メソッドでは User モデルでパラメータの妥当性を検証した後、 paramspassword の値を Monban.hash_token によりハッシュ化して password_digest カラムに書き込みます。

これは Monban.config.sign_up_service として求められるインタフェースを満たしています。 そのため、 UsersController#create を変更する必要はありません。

以上により、パスワードの確認機能が使えるようになりました。 サインアップ機能がコントローラから切り離されているため、コントローラをほぼ変更する必要がありませんでした。 また、 Monban の機能を上書きするのではなく置き換えることで拡張しているので、無理のない実装となりました。

おわりに

さて、簡単ではありますが Monban を用いた認証機能の導入と拡張方法について説明しました。 私の経験上、認証機能はサービスによって独自の仕様が必要となるケースが多く、拡張しやすい gem の方がメンテナンス性が良いと考えています。 Monban は手軽さと拡張性のバランスが取れた軽量の gem として使いやすいと感じています。 また、 Monban の拡張は上書きではなく置き換えにより行うことが多いので、 Monban の想定を超える拡張が必要な場合には Monban を捨てて独自に実装するという手段もあります。

もし、認証関係の gem を選定する機会があれば Monban を候補として検討してみてはいかがでしょうか。

更新日時:2015/09/06 22:17:26
キーワード:
参照:[Rubyist Magazine 0051 号] [0051 号 巻頭言] [分野別目次] [各号目次]