Rails / Deviceでないユーザー登録・ログインを実装する

Shunsuke Sawada

Railsでサインアップやログインを実装する時どうしてますか?
だいたいどのアプリ書くときも必要になってくるので、結構つまらない作業だったりする。
個人的には自分でつくるのが好みですが、plataformatec/devise 使ってる人が圧倒的に多い印象。

Deviceていろんな機能がある、けど、結構使わない。
のでもっとシンプルなGemを試してみました。

NoamB/sorcery

今回は、下記の機能を実装します。

だいたいこんなもんなんだよね。欲しいのは。
あとはSNS認証とかかな。

インストール

Gemfile
1
gem "sorcery"

bundle install を忘れずに。

1
$ rails generate sorcery:install remember_me reset_password user_activation brute_force_protection

このコマンドでマイグレーションファイルが作成されるので rake db:migrate する。

  
あとであの機能も欲しかった!という場合は下記のように追加する。

1
$ rails generate sorcery:install remember_me --only-submodules

  
設定ファイル
config/initializers/sorcery.rb

ruby
1
2
3
4
5
6
7
8
9
10
Rails.application.config.sorcery.submodules = [:remember_me, :user_activation, :reset_password, :brute_force_protection]

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.user_activation_mailer = UserMailer
    user.reset_password_mailer = UserMailer
    user.consecutive_login_retries_amount_limit = 5
  end
  config.user_class = "User"
end

  
routes.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Rails.application.routes.draw do

  root 'users#new'

  resources :users, except: [:index] do
    member do
      get :activate
    end
  end
  resources :sessions, only: [:new, :create, :destroy]
  resources :password_resets, only: [:create, :edit, :update]

  get 'signup'  => 'users#new',        as: 'signup'
  get 'login'   => 'sessions#new',     as: 'login'
  get 'logout'  => 'sessions#destroy', as: 'logout'
  get 'password_resets/create'
  get 'password_resets/edit'
  get 'password_resets/update'
end

  
これは直接関係ないけど、メールの設定。
開発環境のメールテストに Mailtrap.io — Fake smtp testing server. Dummy smtp email testing 使ってます。

config/environments/development.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
Rails.application.configure do
  # Mailtrap
  config.action_mailer.default_url_options = { :host => 'dev.phonenumber.jp:3000' }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    :user_name => 'xxxxxxx',
    :password => 'xxxxxxxx',
    :address => 'mailtrap.io',
    :domain => 'mailtrap.io',
    :port => '2525',
    :authentication => :cram_md5
  }
end

  

実装(モデル)

models/user.rb

ruby
1
2
3
4
5
6
7
8
9
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :email, uniqueness: true
  validates :password, length: { minimum: 6 }, if: -> { new_record? || changes["password"] }
  validates :password, confirmation: true, if: -> { new_record? || changes["password"] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes["password"] }
end

実装(コントローラー)

controllers/users_controller.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class UsersController < ApplicationController

  skip_before_filter :require_login, only: [:index, :new, :create, :activate]

  def new
    @user = User.new
  end

  def create
    @user = User.new(strong_params)
    if @user.save
      redirect_to login_path, notice: "Signed up!"
    else
      render :new
    end
  end

  def activate
    if @user = User.load_from_activation_token(params[:id])
      @user.activate!
      redirect_to login_path, notice: "User was successfully activated."
    else
      not_authenticated
    end
  end

  def show
    @user = User.find(params[:id])
  end

  private

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

  
controllers/sessions_controller.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SessionsController < ApplicationController

  def create
    user = login(params[:email], params[:password], params[:remember_me])
    if user
      redirect_back_or_to user_path(user), notice: "Logged in!"
    else
      invalid_user = User.find_by_email(params[:email])
      message = (invalid_user.present? && invalid_user.locked?) ? "This account is locked" : "Email or password was invalid"
      flash.now[:alert] = message
      render :new
    end
  end

  def destroy
    logout
    redirect_to login_path, notice: "Logged out!"
  end

end

  
controllers/password_resets_controller.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class PasswordResetsController < ApplicationController

  skip_before_filter :require_login

  def create
    @user = User.find_by_email(params[:email])
    @user.deliver_reset_password_instructions! if @user
    # Tell the user instructions have been sent whether or not email was found.
    # This is to not leak information to attackers about which emails exist in the system.
    redirect_to login_url, notice: 'Instructions have been sent to your email.'
  end

  def edit
    @token = params[:id]
    @user = User.load_from_reset_password_token(params[:id])

    if @user.blank?
      not_authenticated
      return
    end
  end

  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(params[:id])
    if @user.blank?
      not_authenticated
      return
    end
    # it makes the password confirmation validation work
    @user.password_confirmation = params[:user][:password_confirmation]
    # it clears the temporary token and updates the password
    if @user.change_password!(params[:user][:password])
      redirect_to login_path, notice: 'Password was successfully updated.'
    else
      render :edit
    end
  end
end

  

実装(メーラー)

mailers/user_mailer.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class UserMailer < ActionMailer::Base

  default from: 'yourapp.com <[email protected]>', template_path: "mailers/user_mailer"
  add_template_helper(ApplicationHelper)

  layout 'mailer'

  def activation_needed_email(user)
    @user = user
    @url  = "#{root_url}users/#{user.activation_token}/activate"
    mail to: user.email,
         subject: "Welcome to My Awesome Site"
  end

  def activation_success_email(user)
    @user = user
    @url  = "#{root_url}login"
    mail to: user.email,
         subject: "Your account is now activated"
  end

  def reset_password_email(user)
    @user = User.find(user.id)
    @url  = edit_password_reset_url(@user.reset_password_token)
    mail :to => user.email,
         :subject => "Your password has been reset"
  end

end

  

実装(ビュー)

サインアップとログイン

views/users/new.html.haml

haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
= form_for @user do |f|
  = render 'shared/error_messages', object: f.object

  .field
    = f.label :email
    = f.text_field :email
  .field
    = f.label :password
    = f.password_field :password
  .field
    = f.label :password_confirmation
    = f.password_field :password_confirmation
  .actions
    = f.submit

  
views/users/show.html.haml

haml
1
2
3
= @user.email
%br
= link_to "log out", logout_path

  
views/sessions/new.html.haml

haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
= render 'shared/flash_message'

- if logged_in?
  = current_user.email

= form_tag sessions_path do
  .field
    = label_tag :email
    = text_field_tag :email, params[:email]
  .field
    = label_tag :password
    = password_field_tag :password
  .field
    = check_box_tag :remember_me, 1, params[:remember_me]
    = label_tag :remember_me
  .actions
    = submit_tag "Log in"

%h5
  Forgot Password?
= render 'password_resets/forgot_password_form'

  
パスワードリセット
  
views/password_resets/edit.html.haml

haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
%h1
  Choose a new password

= form_for @user, url: password_reset_path(@token), html: { method: :put } do |f|
  = render 'shared/error_messages', object: f.object

  .field
    = f.label :email
    = @user.email
  .field
    = f.label :password
    = f.password_field :password
  .field
    = f.label :password_confirmation
    = f.password_field :password_confirmation
  .actions
    = f.submit

  
views/password_resets/_forgot_password_form.html.haml

haml
1
2
3
4
5
= form_tag password_resets_path, method: :post do
  .field
    = label_tag :email
    = text_field_tag :email
    = submit_tag "Reset my password!"

  
共通部分
  
views/shared/_error_messages.html.haml

haml
1
2
3
4
5
- if object.errors.present?
  .panel.warning
    %ul
      - object.errors.full_messages.each do |msg|
        %li.text-red= msg

  
views/shared/_flash_message.html.haml

haml
1
2
3
4
- if flash.present?
  - flash.each do |key, value|
    .panel{class: key}
      = value

 
メーラーのビュー
  
views/mailers/user_mailer/activation_needed_email.html.haml

haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%h3
  Welcome!

= @user.email

%p
  You have successfully signed up to example.com,
  your username is:
  %br
  = @user.email

%p
  To login to the site, just follow this link:
  %br
  = link_to @url, @url

%p
  Thanks for joining and have a great day!

  
views/mailers/user_mailer/activation_success_email.html.haml

haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Congratz,
= @user.email

%p
  You have successfully activated your example.com account,
  your username is:
  = @user.email

%p
  To login to the site, just follow this link:
  %br
  = link_to @url, @url

%p
  Thanks for joining and have a great day!

  
views/mailers/user_mailer/reset_password_email.html.haml

haml
1
2
3
4
5
6
7
8
9
10
11
Hello,
= @user.email

%p
  You have requested to reset your password.
  %br
  To choose a new password, just follow this link:
  %br
  = link_to @url, @url
  %br
  Have a great day!

  
ふぅ。以上です。
Deviceよりも書くコードは多いかもしれないけど、とても理解しやすいです。

それではー。

参考

#283 Authentication with Sorcery - RailsCasts

81
Shunsuke Sawada

おすすめの記事

acts-as-taggable-on タグを表示させる順番を決めたい
Railsを4.2にバージョンアップしたら、Vagrantのローカル開発環境にアクセスできなくなった問題
Railsのバリデーションエラー後にレイアウトが崩れるとき