docker-compose下でrails newして Rails6.1 + Sorcery を試す( Sorcery の仕組み少し解説)

17 min読了の目安(約15800字TECH技術記事 2

devise は試してみたので、
今度は 同じ認証認可gem である Sorcery を試してみましょう

TL;DR

大体以下記事や Sorcery のチュートリアルと同じです

今回やること、やらないこと

  • やること
    • 基本的なEmail&passwordログインでの Sorcery の導入
    • Sorcery を使ったときの User モデルの解説
  • やらないこと
    • パスワードリセットなど、 Sorcery のサブモジュール導入
    • SNS認証

docker-compose の準備, bundle init して rails new する

今までは過去記事と同じ部分もなるべく転記するようにしてたのですが
今回はさすがに全く同じなので過去記事をご参照ください。

http://localhost:3000/ でいつものアレが出ましたでしょうか。
OKですね。Ctrl+c でサーバを閉じましょう。

ここまでの手順を全部同じで実施します。
(というか私自身が記事をみながら実施しました笑)

root_url となる View を作成する

今回は home にしましょう。
appコンテナ のターミナルに戻って

bundle exec rails g controller Home index

すると、出力がこんな感じ

Running via Spring preloader in process 10616
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb
      invoke  helper
      create    app/helpers/home_helper.rb
      invoke  assets
      invoke    css
      create      app/assets/stylesheets/home.css

いいですね。
app/controllers/home_controller.rb
app/views/home/index.html.erb などいったんそのままでOKとしましょう。
ルーティングはあとで行います。

Sorcery を導入する

Gemfile に以下を追加。
ついでにコントローラー作成時に使う gem 'action_args' も入れちゃいましょう。

Gemfile
# どこかに追加
gem 'sorcery'
gem 'action_args'

Gemfile を編集したら appコンテナ の
ターミナルに戻って作業を進めます。

bundle install
bundle exec rails g sorcery:install

bundle exec rails g sorcery:install すると以下のようなものが生成結果が出力されますね。

      create  config/initializers/sorcery.rb
    generate  model User --skip-migration
       rails  generate model User --skip-migration 
      invoke  active_record
      create    app/models/user.rb
      insert  app/models/user.rb
      create  db/migrate/20210104034511_sorcery_core.rb

生成された migration ファイルを覗いてみましょう。
※ migration ファイルのファイル名は bundle exec rails g sorcery:install を実施したタイミングで変わります。

db/migrate/20210104034511_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email,            null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps                null: false
    end

    add_index :users, :email, unique: true
  end
end

devise の Database authenticatable に必要なカラムとほぼ一緒ですね。
違いといえば salt が users に存在するぐらいでしょうか。
今回はサブモジュールなしで導入するのでこのまま db:migrate しましょう、

appコンテナ のターミナルに戻って

bundle exec rails db:migrate

OKですね。

User モデルにバリデーションを追加する

bundle exec rails g sorcery:install で生成された User モデルはこんな感じです。

app/models/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
end

Sorcery チュートリアルにのっとってバリデーションを追加します。

app/models/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, presence: true
  validates :email, uniqueness: true
end
  • パスワード最低3文字
  • passwordpassword_confirmation を比較して一致か確認
  • password_confirmation が空でない
  • email が空でない、一意

ですね。

待って、 :password と :crypted_password ?

と、ここで不思議に思ったあなたは真面目ですね。
そうです、 :password や :crypted_password なんてカラムは users に存在しません。
users のカラムは

  • email
  • crypted_password
  • salt

でしたね。
ではなぜ :password などという指定が出てくるのでしょうか?

これは、Sorcery がよしなにやってくれてるんですね。
具体的には app/models/user.rb の authenticates_with_sorcery! がキモで、
雑に言ってしまえばここのコードを読めばわかります。

もうちょっと詳しく解説すると、上記 Sorcery の sorcery/lib/sorcery/model.rb の
init_orm_hooks!authenticates_with_sorcery! で call されていますね。

    def init_orm_hooks!
      sorcery_adapter.define_callback :before, :validation, :encrypt_password, if: proc { |record|
        record.send(sorcery_config.password_attribute_name).present?
      }

      sorcery_adapter.define_callback :after, :save, :clear_virtual_password, if: proc { |record|
        record.send(sorcery_config.password_attribute_name).present?
      }

      attr_accessor sorcery_config.password_attribute_name
    end

で、ここの sorcery_adapter.define_callback :before, :validation, :encrypt_passwordencrypt_password が指定されています。
encrypt_password をみてみましょう。

      # creates new salt and saves it.
      # encrypts password with salt and saves it.
      def encrypt_password
        config = sorcery_config
        send(:"#{config.salt_attribute_name}=", new_salt = TemporaryToken.generate_random_token) unless config.salt_attribute_name.nil?
        send(:"#{config.crypted_password_attribute_name}=", self.class.encrypt(send(config.password_attribute_name), new_salt))
      end

      def clear_virtual_password
        config = sorcery_config
        send(:"#{config.password_attribute_name}=", nil)

        return unless respond_to?(:"#{config.password_attribute_name}_confirmation=")

        send(:"#{config.password_attribute_name}_confirmation=", nil)
      end

はい、ここです。
ここで sendメソッドを使って config.crypted_password_attribute_namenew_salt で encrypt した config.password_attribute_name をセットしていますね。
send("hoge=", fuga) で値をセットできます)

config.crypted_password_attribute_name
config.password_attribute_name なんていうまわりくどい指定になっているのは、
カラム名を変更できるようにするための記述です。
ではどうやって passwordcrypted_password のカラム名を変えるのかというと
bundle exec rails g sorcery:install で生成された config/initializers/sorcery.rb をみてみましょう。

config/initializers/sorcery.rb
...
    # Change *virtual* password attribute, the one which is used until an encrypted one is generated.
    # Default: `:password`
    #
    # user.password_attribute_name =
...
    # Change default crypted_password attribute.
    # Default: `:crypted_password`
    #
    # user.crypted_password_attribute_name =
...

というような記述がありますね。ここで passwordcrypted_password のカラム名が指定でき、それを考慮した上での記述が上記のようなものになっているのです。
Sorcery は Devise と違ってコード量も少ないので、こうやって追いやすいのが強みですね。
(まぁ Devise も ActiceRecord なんかに比べれば読もうと思えば読める程度の量ですが)

他の部分も気になることはぜひ gem の内部を読んでみましょう。

User モデルを操作するコントローラとビューを作成

UsersController と関連するビュー作っていきます。
appコンテナ のターミナルに戻って

bundle exec rails g controller users new create

すると、出力がこんな感じ

Running via Spring preloader in process 10637
      create  app/controllers/users_controller.rb
       route  get 'users/new'
get 'users/create'
      invoke  erb
      create    app/views/users
      create    app/views/users/new.html.erb
      create    app/views/users/create.html.erb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke  assets
      invoke    css
      create      app/assets/stylesheets/users.css

app/views/users/create.html.erb は今回使わないので消してしまってOKです。

rm app/views/users/create.html.erb

app/controllers/users_controller.rb をこう

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # GET user/new
  def new
    # ログイン済みならユーザ作成画面にはアクセスできない
    if logged_in?
      redirect_to root_path
    end

    @user = User.new
  end

  # POST user
  def create(user)
    @user = User.new(user.permit(:email, :password, :password_confirmation))

    if @user.save
      redirect_to root_path, notice: 'ユーザー登録に成功しました'
    else
      flash.now[:alert] = "ユーザー登録に失敗しました。再度お試しください"
      render :new
    end
  end
end

def create(user) という見慣れない書き方をしていますが
これが gem 'action_args' での書き方です。
アクションがどんなパラメータを必要としているから params[:hoge] とかより圧倒的にわかりやすいですね。

flash.now[:alert]render :new:alert を渡すために書いてます。

app/views/users/new.html.erb をこう

app/views/users/new.html.erb
<h1>サインアップ</h1>
<h2>Users#new</h2>
<p>Find me in app/views/users/new.html.erb</p>

<%= form_with model: @user, url: users_path, method: :post do |form| %>

  <div class="field">
    <%= form.label :email, :メールアドレス %><br />
    <%= form.text_field :email  %>
  </div>

  <div class="field">
    <%= form.label :password, :パスワード %><br />
    <%= form.password_field :password  %>
  </div>

  <div class="field">
    <%= form.label :password_confirmation, :パスワードを再度入力してください %><br />
    <%= form.password_field :password_confirmation  %>
  </div>

  <div class="actions">
    <%= form.submit :登録 %>
  </div>
<% end %>

<%= link_to "戻る", root_path %>

form_forform_tag は Rail5.1以降で非推奨になっているので
form_with を使いましょう。
また、 url: users_path とありますが
ルーティングはあとでまとめて設定します。

ログインを制御するコントローラーとビューを作成

UserSessionsController と関連するビュー作っていきます。
appコンテナ のターミナルに戻って

bundle exec rails g controller UserSessions new create

すると、出力がこんな感じ

Running via Spring preloader in process 10390
      create  app/controllers/user_sessions_controller.rb
       route  get 'user_sessions/new'
get 'user_sessions/create'
      invoke  erb
      create    app/views/user_sessions
      create    app/views/user_sessions/new.html.erb
      create    app/views/user_sessions/create.html.erb
      invoke  helper
      create    app/helpers/user_sessions_helper.rb
      invoke  assets
      invoke    css
      create      app/assets/stylesheets/user_sessions.css

app/views/user_sessions/create.html.erb は今回使わないので消してしまってOKです。

rm app/views/user_sessions/create.html.erb

app/controllers/user_sessions_controller.rb をこう

app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
  # GET user_sessions/new
  def new
  end

  # POST user_sessions
  def create(email:, password:)
    @user = login(email, password)

    if @user
      redirect_back_or_to(root_path)
    else
      flash.now[:alert] = "サインインに失敗しました。再度お試しください"
      render :new
    end
  end

  # GET user_sessions/destroy
  def destroy
    logout
    redirect_to root_path, notice: 'サインアウトしました'
  end
end

login()logout は サインインやサインインを行うための
Sorcery で提供しているヘルパーです。

def create(email:, password:) という見慣れない書き方をしていますが
これも gem 'action_args' での書き方です。
アクションがどんなパラメータを必要としているから params[:hoge] とかより圧倒的にわかりやすいですね。

app/views/user_sessions/new.html.erb をこう

app/views/user_sessions/new.html.erb
<h1>サインイン</h1>
<h2>UserSessions#new</h2>
<p>Find me in app/views/user_sessions/new.html.erb</p>

<%= form_with url: user_sessions_path, method: :post do |form| %>

  <div class="field">
    <%= form.label :email, :メールアドレス %><br />
    <%= form.text_field :email  %>
  </div>

  <div class="field">
    <%= form.label :password, :パスワード %><br />
    <%= form.password_field :password %>
  </div>

  <div class="actions">
    <%= form.submit :サインイン %>
  </div>

<% end %>

<%= link_to "戻る", root_path %>

ここも、 url: user_sessions_path とありますが
ルーティングはあとでまとめて設定します。

config/routes.rb を編集

では作成したコントローラーを config/routes.rb でルートの設定をしましょう。

config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html

  root to: "home#index"
  get 'home/index'

  resources :users, only: [:create]
  resources :user_sessions, only: [:create]

  get '/sign_up', to: 'users#new'
  get '/sign_in', to: 'user_sessions#new'
  get '/sign_out', to: 'user_sessions#destroy'
end

こんな感じです。
user_sessions#destroyrails g した時に宣言しませんでしたが
app/controllers/user_sessions_controller.rb を書く時に用意しました。
サインアウトするための処理です。

app/views/layouts/application.html.erb に導線を用意

サインアップするページへのリンクなどを用意しましょう。

app/views/layouts/application.html.erb をこんな感じに。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all' %>
  </head>

  <body>
    <div id="nav" style="margin-top:30px;">
      <% if logged_in? %>
        <strong><%= current_user.email %></strong> としてログイン中です |
        <%= link_to "サインアウト", sign_out_url, method: :get %>
      <% else %>
        <%= link_to "サインアップ", sign_up_url %> |
        <%= link_to "サインイン", sign_in_url %>
      <% end %>
    </div>
    <p id="notice"><%= flash[:notice] %></p>
    <p id="alert"><%= flash[:alert] %></p>

    <%= yield %>
  </body>
</html>

logged_in? はログイン中かどうかを判別するための Sorcery で提供しているヘルパーです。

動作確認してみる

ではappコンテナ のターミナルに戻って

bundle exec rails s -b "0.0.0.0"

して http://localhost:3000 にアクセスし、動作確認してみましょう。

サインアップしてみます。

サインアップしたアカウントでサインインしてみます。

サインアウトしてみましょう。

いい感じですね!!
スクリーンショットが下手なのは許してください😂

まとめ

今回は Sorcery を導入してみましたが
devise に比べると自分で色々準備しないといけない分、
コードを追いやすくていいですね。全体の把握も幾分か楽な気がします。

今回はSNS認証については行いませんでしたが参考リンクに載せておくので
興味のある人はやってみてください。

私の感触としてはこんな感じかな〜という印象です。
個人の感想レベルですが。

  • Twitter ログインと Emailログイン だけなどの Webサービスを作る
    • Sorcery が良さそう
  • Apple 認証や LINE 認証にも対応する必要がある
    • devise が良さそう

今回のリポジトリはこちらです。おつかれさまでした。

参考