rails5 cookiesでremember me機能を実装したい~railstutorial解説~


業務的なところで、ログイン機能を実装することになり、改めて復習をしてみるついでに、remember me機能について解説してみた。
また、remember me機能はrailstutorialを読んでた際に非常に苦労した覚えが以前あったので、inputを整理するために書いた。
何か認識として間違えていることなどあったら、コメントや、一番下の自分のtwitterからDMを頂けるとありがたいです。

参照→https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-remember_me

#cookiesを使うメリット
railsにはあらかじめsession機能があり、ハッシュsessionにuser_idとして受け取り、DBに格納する事で、ログイン機能を実現している。だが、あくまでこのsessionはサーバー側にuser_idをパラメーターとして受け取り、DBに予め保存されているUser_idとの認証を行い、ログイン機能として実現されているので、ブラウザ側を閉じてしまうと、これまでsessionに格納されていたUser_idは、消去されて、ログイン状態を保持する事が出来ないようになっている。これでは、User側に何度も認証の作業を行わせる事になる。cookiesではこのようなジレンマを解決してくれる。ただ、一つ問題として挙げられるのは、sessionでは、自動的にUser_idは暗号化され、セキュアーになっているのだが、cookiesでは、User_idを暗号化して、保存させなければならない。非常にアンセキュアーなので、ここでは、GemであるBcryptを使って、暗号化を測る事にしている。

#cookiesに関連した必要な要件の洗い出し
前提→cookiesには、User_idと紐づいた記憶トークンを発行してそのトークンをcookiesとDBに保存させる。
1.記憶トークンをランダムに生成した文字列として発行する。
2.生成したトークンをDBに保存する際は、User_idと関連づけて、ハッシュ化して保存する。
3.cookiesにトークンを保存する時は、有効期限を設定する。
4.cookiesにトークンを保存する時はハッシュ化して保存する。
5.ユーザーIDが保存されたcookiesを受け取ったら、そのユーザーIDを使って、DBから引っ張ってきて、DBでハッシュ化されているトークンとcookiesに保存されているトークンを照合する。

#機能実装

まずは、DBの設定を行います。記憶トークンをハッシュ化して保存するためのカラムを作成します。

```
rails g migration add_columns_remember_digest_to_users
```

その後、生成したマイグレーションファイルで、カラムを追加する処理を書きます。

```
db/migrate/[timestamp]_add_remember_digest_to_users.rb
 
 class AddRememberDigestToUsers < ActiveRecord::Migration[5.0]
  def change
   add_columns :users,:remember_digest,:string
  end
 end
 
```

DBの設定を以上にして、1~5を実装していきます。

```
app/models/user.rb

 class User < ApplicationRecord
  before_save { self.email = email.downcase }
   validates :name,  presence: true, length: { maximum: 50 }
   VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
   validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
   has_secure_password
   validates :password, presence: true, length: { minimum: 6 }
  #以上はmodelのvalidate

  #引数として渡された値をハッシュ化する
  def User.digest(string)
   cost=ActiveModel::SecurePassword.min_cost? 
   Bcrypto::Engine::MIN_COST:BCrypto::Engine.cost
   BCrypto::Password.create(string, cost: cost)
  end

#ランダムなトークンを返す
  def User.new_token
   SecureRandom::ulsafe_base64
  end
 end
```
上記のUser.new_tokenでSecureRandomはrailsで予め実装されている機能で、ulsafe_base64というメソッドでランダムな文字列を返してくれます。
これにより、1をクリアしました。

では、次に2を実装します。この2番の機能がremember機能となります。つまり、DBにUser_idと関連づけた
記憶トークンを保存する事で、ブラウザがトークンと関連づいているUser_idを保持しているという事を認識して、ブラウザを閉じた後も、User_idを記憶してくれますので、、ログインの認証手続き後もログイン状態が保持できる仕組みを実現できます。

```
app/models/user.rb

 User < ApplicationRecord
  attr_accessor:remember_token
 ・
 ・
 ・
  def remember
   self.remember_token = User.new_token
   update_attribute(:remember_digest,User.digest(remember_token))
  end
 end
```
rememberメソッド定義時に、使われているselfは、rememberメソッドを呼び出した際のレシーバーを参照する事ができるようになります。これにより、User.rememberという感じで呼び出す事ができて、呼び出した際には、User_idと関連付けつつ、発行したトークンを暗号化してDBに保存され、remember機能が出来ちゃうというわけです。
ちなみにupdate_attributeは、userモデルの情報をアップデートします。
attr_accessor:remember_tokenとする事で、remember_tokenという変数をクラスの外部、内部から参照したり、更新する事ができます。これにより、rememberメソッド定義時にも、remember_tokenを更新する事ができるようになります。
　　　　　　　　　　　       
次に、3.4.5の流れつまり、ログインをブラウザが認識させるためにcookiesにハッシュ化したUser_idを保存していきます。
ここから分かりづらくなるので、予め実装の流れを説明していきます。

・User_idをハッシュ化してcookiesに保存するメソッドをヘルパーメソッドのファイルで定義して、コントローラー側で呼び出します。その際に、先ほど定義したrememberメソッドも使う事により、一気に、User_idをトークンに紐づけて、ハッシュ化しDBに保存して、かつ、cookiesにUser_idを保存する事でcookiesにそのUser_idと紐づいているトークンとUser_idを保存するまでを書く事で完成します。

・有効期限を指定する処理も書きます。

・app/model/user.rbでcookiesのトークンとDBのトークンを照合する処理を書きます。

では、書きます。

```
 app/helpers/session_helper.rb
  
 module SessionHelper
   def log_in(user)
     session[user_id]=user.id
   end

   def remember(user)
      user.remember #Userクラス(User.rb)で定義したrememberメソッド
　　　 cookies.permanent.signed[user_id]=user.id
      cookies.permanent[:remember_token]=user.remember_token
   end
  
   def current_user
     @current_user||=User.find_by(id:session[user_id])
   end

  def logged_in?
     !current_user.nil?
  end
 
  def log_out
    session.delete(:user_id)
    @current_user=nil
  end
 end
```

remember(user)では、前もって説明した通り、引数をuserモデルで取る、rememberメソッドを定義しています。これは、先ほどUserクラスで定義した、レシーバーをuserモデルで取る、rememberメソッドを使って、ヘルパーメソッドでトークンを発行から、Userと関連つけて、DBにトークンを暗号化して保存するまでの処理を1行でできるようにしました。また、その後の、cookies.permanent.singedでuser_idをcookiesに保存していますが、これは有効期限を定めるためのpermanentと、user_idを暗号化するためのsingedです。user_idと同じように、トークンもremember_tokenとして保存しています。ここで、先ほどのattr_accessorが生きてきます。別のモジュール内でも、remember_tokenを参照する事ができるからです。

では、remember機能が完成しましたので、これをcontrollerで呼び出して見ましょう。

```
app/controller/session_controller.rb

class SessionController < ApplicationController
 def new
 end

 def create
   user=User.find_by(email:params[:session][:email].downcase)
   if user & user.authenticate(params[:session][:password])
     log_in user
     remember user
     redirected_to user
   else
     flash.now[:danger] = "invalid email/password combination"
     render "new"
  end

 def destroy
  log_out
  redirected_to root_url
 end
end
```

これでremember me機能が完成しました。非常に長くなりそうなので、とりあえず今日は、ここまでで。
次は、rspecについて書いていきたいと思います。

twitter →https://twitter.com/FujisawaRyohei

是非Followのほどよろしくお願いします。