[Day9 Rails Tutorial]第九章完|難しすぎ初めてこんなに詰まった[9.1〜9.4]

Day9です!

今日もがんばっていきましょー!

読んでいただいている方、いつもありがとうございます!

初心者なもので、たまに間違った内容もあるかと思います。

その際にはコメントやTwitterでご指摘いただけると幸いです!

 

[Day9]第九章9.1〜9.4作業ログ

今日は永続クッキーを使ったログイン機能の実装

昨日のDay8でも書いたけど、ログイン情報の記憶の仕方は2種類あって、

  • 一時クッキー:ブラウザ閉じると消える
  • 永続クッキー:ブラウザ閉じても消えない

今日は後者の実装!

cookiesメソッドを使っていきます。

 

cookiesメソッドは便利だけど、危険性もある

sessionメソッドで保存した情報は、あん税制が自動的に保てれる一方で、cookiesメソッドによって保存された情報はそうではない。

セキュリティの心配がある。

だからこんな感じのセキュリティ対策をする必要あり。

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

 

DBにremember_digestカラムを追加

ミグレーションファイルの作成

rails generate migration add_remember_digest_to_users remember_digest:string

変更をデータベースに反映

rails db:migrate

 

ユーザーを記憶するための記憶トークンを作成

ユーザーを記憶するためにremember_tokenというトークンを作ってあげます。

はじめにremember_tokenはランダムな文字列を生成する便利なメソッドを用いて作ります。

それがSecureRandomモジュールにあるurlsafe_base64メソッド。

これを使ってあげると、64文字の中からランダムに22文字の文字列を生成してくれます。

さらにこの記憶トークンをdigestメソッドでハッシュ化してremember_digestに保存します。

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

 

cookiesメソッドによるログイン状態の保持

次はさっき作成した、remember_tokenとcookiesメソッドを使って、ログイン状態を保持できるようにします。

具体的には、前章のsessionみたいに、cookiesもハッシュで扱える。

個別のcookiesは1つのvalueをオプションの有効期限からできている。次のような構造。

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

20年で有効期限が切れるのは一般化されていて、便利なメソッドが用意されている。もっと簡単にかけて、次。

cookies.permanent[:remember_token] = remember_token

次にsessionでやったときと同様に、user_idをセットする。

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

ここでsignedメソッドは署名付きcookieを使うためのもの。ブラウザにcookieを保存する前に安全に暗号化してくれる。

 

ユーザーの認証には2つの要素

  • User.find_by(id: cookies.signed[:user_id])でユーザー確認
  • bcryptを使ってcookies[:remember_token]がremember_digestと一致することを確認

前者はこれで完成なのでOK。ちなみにsignedメソッドはここでは自動的に暗号化を戻してくれている。

後者について詳しく見ていきます。

secure_passwordのソースコードを参考に、こんなコードを使ってみる。

BCrypt::Password.new(remember_digest).is_password?(remember_token)

これを認証メソッドとして盛り込んであげる。

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

あとはヘルパーメソッドを作成して、signup(sessions_controllerのcreateメソッド)と関連付けてあげる。

sessions_helper.rb
module SessionsHelper
  ...
  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
  ...
end
sessions_controller.rb
class SessionsController < ApplicationController
  ...
  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
  ...
end

 

またcurrent_userも永続できるように設定

永続セッションの場合は、session[:user_id]が存在すれば一時セッションからユーザーを取り出し、それ以外の場合はcookies[:user_id]からユーザーを取り出して、対応する永続セッションにログインする必要があります。

sessions_helper.rb
module SessionsHelper
  ...
  # 記憶トークン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
  ...
end

これでcookiesメソッドによるログイン保持ができた。

 

ログアウトは簡単で値をnilに更新するだけ

ログアウトは簡単です。remember_digestとuser_id、remember_tokenをnilにしてあげるだけ。

user.rb
class User < ApplicationRecord
attr_accessor :remember_token
...
  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
  ...
end
sessions_helper.rb
module SessionsHelper
  ...
  # 永続的セッションを破棄する
  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]チェックボックスの実装

次は[remember me]チェックボックスの実装。

  • チェックあり➞ログインを保持
  • チェックなし➞ログインを保持しない

上記のような設定にする。

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 :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>
       ...
  </div>
</div>

実はここからはすごく簡単で、params[:session][:remember_me]の値はチェックありだと1、チャックなしだと0になるそう。

sessions_controller.rb
class SessionsController < ApplicationController
  ...
  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
  ...
end

 

わからないので飛ばした範囲

9.3からは全然わからなかったので、諦めた。

頭が全然回らない。。

基本的に必要なところはコピペして実装した。

リスト9.27、リスト9.28、9.3.2節は飛ばした。

9.3.2節のsessions_helper_test.rbは一度実装したらエラーが出たから、ちょっともしかしたらどこか機能がおかしいかも。。

結局わからなかったからスルーしました。

 

メモ

rails サーバーを閉じる前にコマンドライン閉じちゃった時の対処法

今回チュートリアルの中で、誤ってrails サーバーを閉じる前に、コマンドラインを消しちゃいました。

すると、rails sを実行しても、

A server is already running. Check C:/rails_book/devise/tmp/pids/server.pid.

ってでてきてサーバーが起動できません。

サーバーのWebページを開くことはできるんですが、サーバーを立て直してチェックすることができなくなります。

対処法を調べたところ、無事解決しました。

参考:A server is already running 対処方法【Rails】

ここから、このパスにあるファイルを削除してみてとのこと。

Railsプロジェクトフォルダ]\tmp\pids\server.pid

こがサーバーを使用しているときのidらしいです。

しかし、問題は治らず。でも別のエラーメッセージが出てきました。

Address already in use - bind(2) for "127.0.0.1" port 8080 (Errno::EADDRINUSE)

こちらも以下の参考URLを参考に解決できました。

参考:ローカルサーバを停止せずにターミナルを閉じてしまいました。

まずは$ lsof -i :8080を実行。

結果はこれ。

COMMAND   PID     USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
ruby    16343 ec2-user   11u  IPv4  44080      0t0  TCP localhost:webcache (LISTEN)

ここでkill 16343を実行してあげると、ローカルサーバーを閉じる事ができました。

 

[Day9]まとめ

  • 学習範囲:9.1〜9.4
  • 学習時間:4時間20分
  • 総学習時間:41時間40分
  • 反省点:分からないところが分からない問題が発生。これは一番ダメな状態なので、なんとかしよう。でも無理をしすぎてやる気なくすよりは前に進んじゃったほうがいい!
  • 備考:9.1.4節、リスト9.27、リスト9.28、9.3.2節は飛ばした。

読んでいただいている方、いつもありがとうございます!

初心者なもので、たまに間違った内容もあるかと思います。

その際にはコメントやTwitterでご指摘いただけると幸いです!

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です