まさ@ブログ書き込み中

まさ@ブログ書き込み中

まさの旅、英語、プログラミング、プライベートについて、色々記録しています。

永続cookiesでガチセッションするRailsチュートリアル9章

 

こんばんは、まさです。

 

前回の記事ではRailssessionメソッドをつかった一時セッションについてまとめました。

masa-world.hateblo.jp

 

身も蓋もないことをいうと、第8章はセッション自体の仕組みというより、その周りのフォームやら、テストやら、Railsならではの実装の仕方に対して学ぶことが多かったのですが、今回まとめる第9章は全体的に未知の領域でした。なので、結構理解するのに苦労しました・・・。

 

なので気を引き締めてまとめていきたいと思います!

 

 

Remember me機能を作るわけ

前回実装した一時セッションで、アプリケーションが最低限機能するための実装はできたと言っても過言ではありません。一時セッションのおかげで、一度ログインしてしまえば、どのページでも「ユーザーがまさである」という状態で操作できるようになったからです。

 

ただし、一度ブラウザを閉じてしまうと、そのセッションは消えてしまうのでした。でも、忘れられるのって悲しくないですか?

 

そこでRemember me機能という、ログイン時に「Remember me」と書かれたチェックボックスにチェックを入れることでブラウザを閉じた後でもユーザー情報が保持されるようになる仕組みを作りたいと思います。

 

何でそんなのいちいち作るかって?一つは、現在のWebアプリケーションでそれが当たり前になっているし、何と言っても便利だからです。ただもう一つの理由は一つ目の理由よりずっと大切です

 

ワンピースでDr.ヒルルクは言いました。

f:id:masaincebu:20170831143536j:plain

f:id:masaincebu:20170831143618j:plain

 

泣ける。

 

そこでみんな気がつくわけです。

 

Remember me機能は何としてでも作らねばならないと。

 

 

Remember meの実装の方針

本章によると

セッションの永続化の第一歩として記憶トークン (remember token) を生成し、cookiesメソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用します。

という方針でやっていくそう。ちなみに

トークン」とは、パスワードの平文と同じような秘匿されるべき情報を指します。パスワードとトークンとの一般的な違いは、パスワードは使用者が自身で作成・管理する情報であるのに対し、トークンはコンピューターなどが生成した情報である点です。

だそうです。

 

永続的なセッションを実現するためにcookiesを使うということでしたが、cookiesは一時セッションとは違って盗み出される可能性があります。これをセッションハイジャックというそうです。

 

それらを考慮した上で永続的セッションを作成する際には以下の点に沿う必要があります。

  1. 記憶トークンにはランダムな文字列を生成して用いる。

  2. ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。

  3. トークンはハッシュ値に変換してからデータベースに保存する。

  4. ブラウザのcookiesに保存するユーザーIDは暗号化しておく。

  5. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

 

ぶっちゃけ僕は本章のこのパートを読んでいる時点では何がしたいのか僕はわかっていなかったので理解に苦労したので、先に僕なりの理解をここに書いておきます。

 

まとめると、どうやら永続的セッションは

  • アプリ(具体的にはサーバー)側がユーザーに記憶トークン(ランダムな文字列)を暗号化したもの(記憶ダイジェスト)を持たせる
  • cookiesには暗号化したユーザーIDと記憶トークンを(永続的に)持たせておく
  • ユーザーがサイトに訪れた際にcookiesがアプリケーション側に暗号化したIDと記憶トークンを渡し、認証が行われる

という背景があって実現されるみたいですね。

 

 

Rememberする

さて、これらの方針を頭に入れながらRemember me機能を実装していきます。ここから長旅になりますが、できるだけわかりやすく書いていきますのでついてきてください。

 

まずは、ユーザーを記憶する仕組みについて書いていきます。

 

マイグレーションファイルを作成する

「ユーザーに記憶トークンを暗号化したものを持たせる」ということは「データベースに記憶ダイジェストを保存する」ということと同義ですので、新しくマイグレーションファイルを作ります。

$ rails generate migration add_remember_digest_to_users remember_digest:string

 

ランダムなトークンをつくる

記憶トークンとして使う「ランダムな文字列」をどうやって実現するかというと、ここではSecureRandomモジュールにあるurlsafe_base64メソッドを使います。

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

 

app/models/user.rbには以下のようなメソッドを定義します。

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

 

user.rememberメソッドでユーザーを記憶トークンと関連づける

さきほど述べたとおり、記憶トークンを暗号化したものをユーザーが持っている必要があるので、user.rememberメソッドを定義します。

 

マイグレーションファイルを作成した際にremember_digestカラムは作成しましたが、remember_token(記憶トークン)カラムは作成していません。なので、いつものようにいきなりuser.remember_tokenとはできないのでattr_accessorメソッドでremember_tokenを宣言し、Userインスタンスに値を持たせる際には暗号化しremember_digestとします。

app/models/user.rb

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

 

User.digestメソッドは第8章でテスト時に登録済みユーザーとしてログインしなければならなかった時、fixture(テスト用データ)にユーザーを作成する際に使った暗号化メソッドです。

 

cookiesで永続セッションを作成

実装の方針について説明した際にも言及したとおり、永続セッションを作成するためにはユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存すればいいのです。

 

cookiesはメソッドですが、ハッシュのように扱えて、1つのvalue(値)と1つのexpires(有効期限)からできています。

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

 

20年をexpiresとして指定することが多かったため、いまでは上のコードは次のコードで代替されているようです。

cookies.permanent[:remember_token] = remember_token

 

さて、ユーザーIDをcookiesに保存するには

cookies.permanent.signed[:user_id] = user.id

と書けばOKです。

permanentは永続化のためのメソッドで、signedはcookiesを暗号化するためのメソッドです。

 

cookiesの記憶トークンをユーザーの記憶ダイジェストに認証させる仕組みをつくる

僕がまとめた永続的セッションの流れのうちの「ユーザーがサイトに訪れた際にcookiesがアプリケーション側に暗号化したIDと記憶トークンを渡し、認証が行われる」の部分を実装していきます。

 

つまり「cookies[:remember_token]がremember_digestと一致することを確認」するわけです。

 

これには、パスワードをハッシュ化する際に利用したbcryptを利用します。

app/models/user.rb

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

 

 紛らわしいですが、authenticated?メソッド内にあるremember_tokenは先ほど定義したアクセサ(user.remember_tokenとして使われるときのもの)ではなく、引数として渡されたメソッド内のローカル変数を指していて、remember_digestはself.remember_digestとして使われるものと同じで、Active Recordによって取得されています。

 

実際にユーザーを記憶する

ログインしたユーザーを記憶する処理の準備が整ったあとは、実際にログイン後にユーザーを記憶させるようにコードを書いていきます。

 

その前に、皆さんに少し意識してほしいことがあります。この記事に限って、僕は引用したコードのいくつかに「どのファイルにこのコードを書いているか」を赤文字で明示していることに気づいた方もいるかもしれません。

 

実は、これは後の実装の際に起きうる混乱を避けるために書いたものです。なぜなら、Remember me機能の実装にはrememberメソッドが二回出てくる上、複数のファイルをまたいでコードを書き込むからです。

 

いまのところ、僕が紹介したコードはapp/models/user.rbでの更新にとどまっています。つまり、Userクラスに関係するメソッドを定義してきたということです。

 

実際にユーザーを記憶するためのコードを書くファイルは第8章で作成したsessions_controller.rbです。そこに、rememberヘルパーメソッドを定義します。

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

 

このrememberヘルパーメソッドはsessionsコントローラーのヘルパーメソッドであり、先ほど定義したuser.rememberメソッドとは違う点に注意してください。実際に、user.rememberメソッドとは違って引数としてuserを取っています。

 app/helpers/sessions_helper.rb

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

 

 

ここまでこれば、ユーザーをcookiesによって記憶する流れが見えてきました。

  1. ユーザーがログインするときにsessions_controller.rbcreateアクションによりログイン認証後にrememberヘルパーメソッドにユーザーが渡される
  2. rememberヘルパーメソッドの中にはUserモデルのインスタンスメソッドuser.rememberが呼び出されており、user.rememberメソッドはユーザーインスタンスに記憶ダイジェストを持たせる
  3. その後、rememberヘルパーメソッドではcookiesに永続化と暗号化されたユーザーIDと、永続化された(ユーザーの)記憶トークンが入れられる

 というわけですな。

 

永続的セッション(cookiesを利用したログイン)に合わせた微調整

ここでちょっとした微調整を行います。それはcurrent_userヘルパーメソッドについてです。現在current_userメソッドは

@current_user ||= User.find_by(id: session[:user_id])

と、一時セッションを利用して現在のユーザーを特定する方法しか取っていません。

 

確かに、sessions_controller.rbにあるようにログインすればlog_inヘルパーメソッドでsession[:user_id] = user.idとして保存されるので問題ありませんが、一度ブラウザを閉じて、再び開いたときにはcookiesを利用してユーザーを特定するので、このままではcurrent_userメソッドは正常に動作しません。

 

具体的な対策としては、以下のように一時セッションがあるかどうかでcurrent_userの特定方法を変えます。

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

 

 

Forgetする

さて、ここまで散々ユーザーを記憶するための機能を実装してきましたが、ログアウトした際にはそのセッションを終了させなければなりません。

 

人も同じです。ずっと過去の人を引きずっていては、前に進めません。ときには、綺麗さっぱり忘れることが大切なときもあります

 

f:id:masaincebu:20170831165338j:plain

 

 

forgetするためには、rememberとは逆のことをすればいいのです(意味深)。

 

具体的には、user.rememberを取り消すためにuser.forgetメソッドを定義します。このメソッドで、記憶ダイジェストをnilにしてしまいます(user.rememberでは記憶ダイジェストをユーザーに保持させました)。

app/models/user.rb

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end

 

次にforgetヘルパーメソッドで永久的セッションを破棄させます(rememberヘルパーメソッドではcookiesにユーザーIDや記憶トークンを代入していました)。

app/helpers/sessions_helper.rb

  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

 

最後に、forgetヘルパーメソッドをユーザーを引数としてログアウトの際に呼び出せばOK。

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

 

 

Rememberチェックボックス

本章では「2つの目立たないバグ」という節があり、上の実装では細かいバグに対応できないと指摘し対策法までまとめてくださっていますが、本記事ではそれについては触れずに最後にRememberチェックボックスの作成についてまとめて終わりたいと思います。

 

まず、以下のコードをログインフォームに追加します。

app/views/sessions/new.html.erb

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

 

次に以下のようにparamsから:remember_meの値を参照し、それによって呼び出すメソッドを変えます。

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

 

ちなみに、上のコードは

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

と書き換えることができます。

 

これは三項演算子と呼ばれるもので、本章によると以下のように書かれています。

論理値? ? 何かをする : 別のことをする

 

以上をもって、大まかなRemember me機能の実装は完了です。

 

 

感想

9章はこれまでのRailsチュートリアルの中で一、二位を争う強敵でした。

二日前からスキマ時間を見つけてはちょこっと取り組んでみましたが、そんな生半可なスタンスでは理解できませんでしたね・・・。

 

でも今日、このブログを通してしっかり理解できたのでよかったです!

 

ではまた。