#基本的なログイン機構
■第8章
基本的なログイン機構を構築していく。
##8.1 セッション
HTTPは状態のないプロトコル。HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われる。
ログインするとcreateでセッションを作成して保存。
ログアウトするとdestroyでセッションを破棄。
###8.1.1 Sessionsコントローラ
ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。
セッションコントローラの作成。
$ rails generate controller Sessions new
リソースを追加して標準的なRESTfulアクションをgetできるようにする。
以下のコードを追記。
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
Sessionsコントローラのテストで名前付きルートを使うようにする。
require 'test_helper'
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get login_path
assert_response :success
end
end
###8.1.2 ログインフォーム
新しいセッションフォームを作成するときには、form_for
ヘルパーに追加の情報を独自に渡さなければならない。
ログインフォームのコード
<% 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.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
###8.1.3ユーザーの検索と認証
ログインでセッションを作成する場合、入力が無効な場合の処理を最初に行う。
最小限のcreate
アクションをSessionsコントローラで定義し、空のnew
アクションとdestroy
アクションも作成する。
class SessionsController < ApplicationController
def new
end
def create
render 'new'
end
def destroy
end
end
ユーザー認証に必要なあらゆる情報をparams
ハッシュから簡単に取り出せる。
Active Recordが提供する
User.find_by
メソッドでデータベースからユーザーを探し、has_secure_password
が提供する
authenticate
メソッドでパスワードをチェックする。authenticate
メソッドは認証に失敗したときにfalseを返す。
最終的にはこんな感じに。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
# エラーメッセージを作成する
render 'new'
end
end
def destroy
end
end
###8.1.4 フラッシュメッセージを表示する
セッションではActive Recordのモデルを使っていないので、ログインに失敗したときには代わりにフラッシュメッセージを表示する。
ここで書いたコードは誤りで、フラッシュメッセージが消えない状態になるので、一旦次へ。
###8.1.5 フラッシュのテスト
統合テストを作成する。
$ rails generate integration_test users_login
以下の流れでテストコードを実装していく
・ログイン用のパスを開く
・新しいセッションのフォームが正しく表示されたことを確認する
・わざと無効なparams
ハッシュを使ってセッション用パスにPOSTする
・新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
・別のページ(Homeページなど)にいったん移動する
・移動先のページでフラッシュメッセージが表示されていないことを確認する
フラッシュメッセージの残留をキャッチするテスト。
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
assert_template 'sessions/new'
post login_path, params: { session: { email: "", password: "" } }
assert_template 'sessions/new'
assert_not flash.empty?
get root_path
assert flash.empty?
end
end
このままだとテスト失敗になるので、テストをパスさせるに、本編のコードでflash
をflash.now
に置き換える。後者はその後リクエストが発生したときに消滅する。
##8.2 ログイン
cookiesを使った一時セッションでユーザーをログインできるようにする。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。
ほんとはセッションの実装はめちゃくちゃ大変だけど、Rubyのモジュール機能使えば楽にできる。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
###8.2.1 log_in
メソッド
同じログイン手法を様々な場所で使いまわせるようにするために、
sessions
ヘルパーにlog_in
という名前のメソッドを定義。
ログインメソッド
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
上から3行目のコードで、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。
class SessionsController < 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
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
create
アクションを完了し、ユーザーのプロフィールページにリダイレクトする準備ができた。
###8.2.2 現在のユーザー
ユーザーIDを一時セッションの中に安全に置けるようになったので、今度は、ユーザーIDを別のページで取り出すことにする。
current_user
メソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。
ユーザーIDが存在しない状態でfind
を使うと例外が発生するので、find_by
メソッドを使う。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
end
current_user
メソッドが動作するようになったので、ユーザーがログインしているかどうかに応じてアプリケーションの動作を変更するための準備ができた。
###8.2.3 レイアウトリンクを変更する
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_user
がnil
ではないという状態を指すので、否定演算子!
を使ってチェックをしていく。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
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
end
###8.2.4 レイアウトの変更をテストする
以下の手順でテストを作成する。
・ログイン用のパスを書く
・セッション用パスに有効な情報をpostする
・ログイン用リンクが表示されなくなったことを確認する
・ログアウト用リンクが表示されていることを確認する
・プロフィール用リンクが表示されていることを確認する
上の変更の確認のため、テスト時に登録済みユーザーとしてログインしておく必要がある。
テスト用データをfixture (フィクスチャ) で作成できる。現時点のテストでは、ユーザーは1人いれば十分。
fixture向けのdigestメソッドを追加する。
以下のコードを追加。
# 渡された文字列のハッシュ値を返す
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
end
digest
メソッドができたので、有効なユーザーを表すfixture
を作成できるようになった。
ユーザーのログインで使うfixture。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
このコードでテストユーザー用の有効なパスワードが作成できる。
<%= User.digest('password') %>
fixtureは生のパスワードを参照できないので、テスト用のfixtureでは全員同じパスワード「password」にする。
assert_redirected_to @user
上のコードはリダイレクト先が正しいかどうかをチェックしている。
テストでも問題ないことが確認できた。
###8.2.5 ユーザー登録時にログイン
ユーザー登録中にログインを済ませておくことにする。Usersコントローラのcreate
アクションにlog_in
を追加するだけで済むらしい。
##8.3 ログアウト
ログアウト機能を追加していく。ログアウト用リンクは作成済なので、ユーザーセッションを破棄するための有効なアクションをコントローラで作成する済む。
log_out
メソッド。以下を追記。
# 現在のユーザーをログアウトする
def log_out
session.delete(:user_id)
@current_user = nil
end
2行目のコードで、現在のユーザーをnil
に設定できる。
ここで定義したlog_out
メソッドは、Sessionsコントローラのdestroy
アクションでも同様に使っていく。
以下を追記。
# 現在のユーザーをログアウトする
def destroy
log_out
redirect_to root_url
end
テストの内容も弄って、無事テストGREENに。
##感想
ログイン・ログアウト機能の実装はバックエンドよりの作業なのかな?
HTTPだったりcookiesの話は応用情報の時にもちょっと触ったので懐かしいですね。
フワフワしたまま進んでいっていますが、頑張ります。