docker-compose下でrails newして Rails6.1 + Sorcery を試す( Sorcery の仕組み少し解説)
devise は試してみたので、
今度は 同じ認証認可gem である Sorcery を試してみましょう
TL;DR
大体以下記事や Sorcery のチュートリアルと同じです
- SorceryでRailsアプリケーションに認証機能を実装する | Boys Be Engineer 非エンジニアよ、エンジニアになれ
- 【Rails】Sorceryでログイン機能を実装する | Qiita
- Simple Password Authentication | Sorcery/sorcery Wiki
今回やること、やらないこと
- やること
- 基本的なEmail&passwordログインでの Sorcery の導入
- Sorcery を使ったときの User モデルの解説
- やらないこと
- パスワードリセットなど、 Sorcery のサブモジュール導入
- SNS認証
docker-compose の準備, bundle init して rails new する
今までは過去記事と同じ部分もなるべく転記するようにしてたのですが
今回はさすがに全く同じなので過去記事をご参照ください。
http://localhost:3000/ でいつものアレが出ましたでしょうか。
OKですね。Ctrl+c でサーバを閉じましょう。
ここまでの手順を全部同じで実施します。
(というか私自身が記事をみながら実施しました笑)
- docker-compose 下で rails new して Rails6.1+deviseを試す | 北山淳也 | zenn
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'
も入れちゃいましょう。
# どこかに追加
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
を実施したタイミングで変わります。
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 モデルはこんな感じです。
class User < ApplicationRecord
authenticates_with_sorcery!
end
Sorcery チュートリアルにのっとってバリデーションを追加します。
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文字
-
password
とpassword_confirmation
を比較して一致か確認 -
password_confirmation
が空でない -
email
が空でない、一意
ですね。
- Simple Password Authentication | Sorcery/sorcery Wiki
- Railsバリデーションまとめ | Qiita
待って、 :password と :crypted_password ?
と、ここで不思議に思ったあなたは真面目ですね。
そうです、 :password や :crypted_password なんてカラムは users に存在しません。
users のカラムは
- crypted_password
- salt
でしたね。
ではなぜ :password などという指定が出てくるのでしょうか?
これは、Sorcery がよしなにやってくれてるんですね。
具体的には app/models/user.rb の authenticates_with_sorcery!
がキモで、
雑に言ってしまえばここのコードを読めばわかります。
- sorcery/lib/sorcery/model.rb | Sorcery / sorcery | GitHub
もうちょっと詳しく解説すると、上記 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_password
で encrypt_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_name
に new_salt
で encrypt した config.password_attribute_name
をセットしていますね。
(send("hoge=", fuga)
で値をセットできます)
- 【Ruby on Rails】sendメソッドのいろんな書き方 | Qiita
config.crypted_password_attribute_name
や
config.password_attribute_name
なんていうまわりくどい指定になっているのは、
カラム名を変更できるようにするための記述です。
ではどうやって password
や crypted_password
のカラム名を変えるのかというと
bundle exec rails g sorcery:install
で生成された 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 =
...
というような記述がありますね。ここで password
や crypted_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 をこう
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 をこう
<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_for
や form_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 をこう
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 をこう
<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 でルートの設定をしましょう。
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#destroy
は rails g
した時に宣言しませんでしたが
app/controllers/user_sessions_controller.rb を書く時に用意しました。
サインアウトするための処理です。
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 が良さそう
今回のリポジトリはこちらです。おつかれさまでした。
参考
- SorceryでRailsアプリケーションに認証機能を実装する | Boys Be Engineer 非エンジニアよ、エンジニアになれ
- 【Rails】Sorceryでログイン機能を実装する | Qiita
- Simple Password Authentication | Sorcery/sorcery Wiki
- 【Rails】SorceryでTwitter認証 | Qiita
- 【Sorcery】Sorceryで使えるようになるメソッドとその活用例 | Qiita
- gem sorcery使ってみた | とまとの成長日記
- RailsのsorceryでBrute force対策を行う | rochefort's blog
- rails deviseの代わりにSorceryを使って認証機能をしてみたよ │ 文系エンジニア大学生の技術ブログ
- #283 Authentication with Sorcery | RailsCasts
- パスワードリセット機能を実装する | Ruby on Rails Learning Diary
- [Rails] そうだ、コントローラで引数を取ろう [ActionArgs] | Qiita
- ActionArgsが素晴らしい件 #Rails | Islands in the byte stream
- 名前付きルートの書き方が分からないとき | Qiita
- 【Rails 5】(新) form_with と (旧) form_tag, form_for の違い | Qiita
- noticeやalertの設定方法の違い | Qiita
Discussion
とても分かりやすかったです。
Deviseは全体の把握がかなり難しいのでその点Sorceryは良さそうですね。
@catnose さん
そうですね、Sorcery は提供されるものがシンプルで使いやすいので
その辺りはすごくいいなと思いました。安心して使えるというか。