railsチュートリアル 第九章


はじめに

railsチュートリアルで理解しにくいところや、詰まったところを書いていく記事になります。
なので、手順を示す記事とはなっていません。

Remember me 機能

[remember me] 機能 を利用すると、ユーザーが明示的にログアウトを実行しない限り、ログイン状態を維持することができるようになる。

この機能を利用するには、記憶トークン (remember token)を生成し、cookiesメソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用することが必要となる。

(トークンとは、パスワードと似たようなものであるが、違いとしてはトークンの場合はコンピュータが作成した情報である)

永続的セッションを作成するにあたって、以下のようなことを留意点とする。

・記憶トークンにはランダムな文字列を生成して用いる。
・ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。
・トークンはハッシュ値に変換してからデータベースに保存する。
・ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
・永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

rememberダイジェストを追加

最初の手順として、マイグレーションファイルを生成し、remember_digest属性(string)Userモデルに追加する。

また、remember_digestはユーザーが直接生み出すことはないので、インデックスを追加する必要がない。

記憶トークンの作成

記憶トークンには基本的に長くてランダムな文字列が求められるので今回はSecureRandomモジュールにある urlsafe_base64メソッド を使用する。

このメソッドは、A–Z、a–z、0–9、"-"、"_"のいずれかの文字 (64種類) からなる長さ22のランダムな文字列を返す。
例:

$ rails console
>> SecureRandom.urlsafe_base64
=> "q5lt38hQDc_959PVoo6b7A"

ユーザーを記憶するには、記憶トークンを作成して、そのトークンをダイジェストに変換したものをデータベースに保存すればいい。

ここで、トークン生成用のメソッドを追加する。

app/models/user.rb
# ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

次に有効なトークンとそれに関連するダイジェストを作成する。
具体的には、最初に User.new_token で記憶トークンを作成し、続いて User.digest を適用した結果で記憶ダイジェストを更新する。

app/models/user.rb
attr_accessor :remember_token
.
.
.
 # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

rememberメソッド の1行目の代入の箇所では、self というキーワードを使わないと、Rubyによってremember_tokenという名前のローカル変数が作成されてしまう。
selfキーワードを与えると、この代入によってユーザーのremember_token属性が期待どおりに設定される。
rememberメソッドの2行目では、update_attributeメソッド を使って記憶ダイジェストを更新している。

ログイン状態の保持

user.rememberメソッド が動作するようになったので、ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができた。
これを実行するには cookiesメソッド を使う。

以下に手順を示す。

app/models/user.rb
.
.
.

 # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

rememberヘルパーメソッド を追加して、log_in と連携させる。

app/controllers/sessions_controller.rb
 def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      #追加
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
 end

実際のSessionsヘルパーの動作は、rememberメソッド定義のuser.rememberを呼び出すまで遅延され、そこで記憶トークンを生成してトークンのダイジェストをデータベースに保存される。

続いて上と同様に、cookiesメソッドでユーザーIDと記憶トークンの永続cookiesを作成する。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # ユーザーのセッションを永続的にする
  #追加
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 現在ログインしているユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

これにより、current_userヘルパー を追加修正する必要がある。

記憶トークンcookieに対応するユーザーを返す↓

app/helpers/sessions_helper.rb
# 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

ユーザーを忘れる

ユーザーがログアウトできるように、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義する。

具体的には、記憶ダイジェストをnilで更新するメソッドを定義することとなる。

forgetヘルパーメソッド を追加して、log_outヘルパーメソッドから呼び出す。

app/models/user.rb
.
.
.
# ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end
app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

[Remember me] チェックボックス

ログインフォームに チェックボックス を追加する。
チェックボックスは、他のラベル、テキストフィールド、パスワードフィールド、送信ボタンと同様にヘルパーメソッドで作成できる。

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>
      #追加
      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

2つのCSSクラス checkbox inline を使い、Bootstrapではこれらをチェックボックスとテキスト「Remember me on this computer」として同じ行に配置することができる。

最後に、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにする。

ログインフォームから送信されたparamsハッシュには既にチェックボックスの値が含まれていて、オンの時は1、オフの時は0になるので、三項演算子を使うことで、コンパクトに処理を書くことができる。

app/controllers/sessions_controller.rb
def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      #追加
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
end

ちなみに三項演算子を使わない場合は追加部分は以下のようなコードとなる。

if params[:session][:remember_me] == '1'
  remember(user)
else
  forget(user)
end

おわりに

普段、当たり前のようにお世話になっている機能がここまで難しとは、、という感じです、、、
徐々に理解していこうと思います!

次↓
https://qiita.com/jonnyjonnyj1397/items/dda1a3b33bf9a1c0f982