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に保存します。
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)
これを認証メソッドとして盛り込んであげる。
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メソッド)と関連付けてあげる。
module SessionsHelper
...
# ユーザーのセッションを永続的にする
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
...
end
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]からユーザーを取り出して、対応する永続セッションにログインする必要があります。
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にしてあげるだけ。
class User < ApplicationRecord
attr_accessor :remember_token
...
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
...
end
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]チェックボックスの実装。
- チェックあり➞ログインを保持
- チェックなし➞ログインを保持しない
上記のような設定にする。
<% 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になるそう。
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でご指摘いただけると幸いです!
コメントを残す