8.1 セッション
HTTPはステートレス (Stateless) なプロトコルです。文字通り「状態 (state)」が「ない (less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われます。
ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。
Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
8.1.1 Sessionsコントローラ
Sessionsコントローラを生成する
rails generate controller Sessions new
config/routes.rb
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
end
#セッションルールによって提供されるルーティング
HTTPリクエスト URL 名前付きルート アクション名 用途
GET /login login_path new 新しいセッションのページ (ログイン)
POST /login login_path create 新しいセッションの作成 (ログイン)
DELETE /logout logout_path destroy セッションの削除 (ログアウト)
演習
1:GET login_pathとPOST login_pathとの違いを説明できますか?
少し考えてみましょう。
get '/login', to: 'sessions#new' post '/login', to: 'sessions#create'
GET login_path: “/login”へアクセスされた際に”sessions#new”アクションを実行
POST login_path: “sessions#create”アクションの情報を”/login”へ送信。
Getは/loginのリクエストが来た際にsessionsコントローラのnewアクションをするという意味でPostはsessionsコントローラのcreateアクションを/loginに送信する
2:ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。
同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。
現在、いくつのSessionsリソースがあるでしょうか?
rails routes | grep users#
rails routes | grep sessions# 3つ
form_for(@user)
Railsでは上のように書くだけで、
「フォームのactionは/usersというURLへのPOSTである」と自動的に判定しますが、
セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります。
form_for(:session, url: login_path)
8.1.2 ログインフォーム
演習
リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか?
ヒント:表 8.1とリスト 8.5の1行目に注目してください。
action=”/login” method=”post”から/loginにPostする場合
sessionsコントローラのcreateアクションが実行されるため
8.1.3 ユーザーの検索と認証
app/controllers/sessions_controller.rb
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
User Password a && b
存在しない 何でもよい (nil && [オブジェクト]) == false
有効なユーザー 誤ったパスワード (true && false) == false
有効なユーザー 正しいパスワード (true && true) == true
演習
1:Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめて
みましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を
確かめてみて下さい。
ヒント:!!(user && user.authenticate(‘foobar’))
user = nil >> !!(user && user.authenticate('password00')) => false user = User.first >> !!(user && user.authenticate('password00')) => true
8.1.5 フラッシュのテスト
rails generate integration_test users_login
1:ログイン用のパスを開く get login_path
2:新しいセッションのフォームが正しく表示されたことを確認する
assert_template ‘sessions/new’
3:わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
post login_path, params: { session: { email: “”,password: “”,} }
4:新しいセッションのフォームが再度表示され、フラッシュメッセージが
追加されることを確認する
assert_template ‘sessions/new’
5:別のページ (Homeページなど) にいったん移動する
get root_path
6:移動先のページでフラッシュメッセージが表示されていないことを確認する
assert flash.empty?
flash.nowに置き換えます。後者は、レンダリングが終わっているページで特別にフラッシュメッセージを表示することができます。
test/integration/users_login_test.rb
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
rails test test/integration/users_login_test.rb
8.2 ログイン
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHeler
end
8.2.1 log_in
メソッド
log_in
メソッドapp/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
app/controllers/sessions_controller.rb
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
演習
1:有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか?
ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです!
- ページの一番下にある「詳細設定」をクリック。
- 「コンテンツの設定」をクリック。
- 「Cookie」をクリック。
- 「すべての Cookie とサイトデータを表示」をクリック。
- ドメイン別にcookieが表示されます。
2:先ほどの演習課題と同様に、Expires
の値について調べてみてください。
1と同じ手順で見ることができます。
8.2.2 現在のユーザー
current_user
メソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにします。
app/helpers/sessions_helper.rb
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
演習
1:Railsコンソールを使って、User.find_by(id: …)で対応するユーザー
が検索に引っかからなかったとき、nilを返すことを確認してみましょう。
user = User.find_by(id: 10) User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 10], ["LIMIT", 1]] => nil
2:先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成
してみましょう。
リスト 8.17に記したステップに従って、||=演算子がうまく
動くことも確認してみましょう
session = {} => {} >> session[:user_id] = nil => nil >> @current_user ||= User.find_by(id: session[:user_id]) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ? [["LIMIT", 1]] => nil >> session[:user_id]= User.first.id User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => 1 >> @current_user ||= User.find_by(id: session[:user_id]) User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2020-04-04 01:52:48", updated_at: "2020-04-04 01:52:48", password_digest: "$2a$10$X1rTgXx2saP84XHMnyVr5cM6iOKD...">
8.2.3 レイアウトリンクを変更する
レイアウトのリンクを変更する方法として考えられるのは、
ERBコードの中でif-else文を使用し、
条件に応じて表示するリンクを使い分けることです。
<% if logged_in? %> # ログインユーザー用のリンク <% else %> # ログインしていないユーザー用のリンク <% end %>
このコードを書くためには、論理値を返すlogged_in?メソッドが必要なので、まずはそれを定義していきましょう。
logged_in?
ヘルパーメソッドapp/helpers/sessions_helper.rb
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
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
dropdown
クラスやdropdown-menu
などを使っています。これらのドロップダウン機能を有効にするため、Railsのapplication.js
ファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示しますapplication.js
にBootstrapのJavaScriptライブラリを追加するapp/assets/javascripts/application.js
//= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .
演習
1:ブラウザのcookieインスペクタ機能を使って、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか?
cookieを削除して確認するだけ
2:もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。
注意: もしブラウザの [閉じたときの状態に戻す] 機能をオンにしていると、セッション情報も復元される可能性があります。もしその機能をオンにしている場合、忘れずにオフにしておきましょう
動作確認するだけ
8.2.4 レイアウトの変更をテストする
- ログイン用のパスを開く
- セッション用パスに有効な情報をpostする
- ログイン用リンクが表示されなくなったことを確認する
- ログアウト用リンクが表示されていることを確認する
- プロフィール用リンクが表示されていることを確認する
app/models/user.rb
class User < ApplicationRecord
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
# 渡された文字列のハッシュ値を返す
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
end
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "login with valid information" do
get login_path
post login_path, params: { session: { email: @user.email,
password: 'password' } }
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
end
end
rails test test/integration/users_login_test.rb
演習
1:試しにSessionヘルパーのlogged_in?
メソッドから!
を削除してみて、リスト 8.23が redになることを確認してみましょう。
!を削除してTest
2:先ほど削除した部分 (!
) を元に戻して、テストが greenに戻ることを確認してみましょう。
もとに戻してTest
8.2.5 ユーザー登録時にログイン
ユーザー登録中にログインするには、Usersコントローラのcreate
アクションにlog_in
を追加するだけで済みます。
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
is_logged_in?ヘルパーメソッドを定義しておくと便利です。
このヘルパーメソッドは、テストのセッションにユーザーがあればtrueを返し、それ以外の場合はfalseを返します。
残念ながらヘルパーメソッドはテストから呼び出せない。
sessionメソッドはテストでも利用できるので、これを代わりに使います。
ここでは取り違えを防ぐため、logged_in?の代わりにis_logged_in?を
使って、ヘルパーメソッド名がテストヘルパーとSessionヘルパーで同じにならないようにしておきます。
テスト中のログインステータスを論理値で返すメソッドtest/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
# テストユーザーがログイン中の場合にtrueを返す
def is_logged_in?
!session[:user_id].nil?
end
end
ユーザー登録後のログインのテスト green
test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
end
これで、テストを実行すると greenになるはずです。
rails test
演習
1:リスト 8.25のlog_in
の行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。
RED
2:現在使っているテキストエディタの機能を使って、app/controllers/users_controller.rb
をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。
ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については『開発基礎編: テキストエディタ』の 「コメントアウト機能」などを参照してみてください。
コメントアウトして動作確認するだけ
8.3 ログアウト
ログアウトの処理では、app/helpers/sessions_helper.rb
のlog_in
メソッドの実行結果を取り消します。つまり、セッションからユーザーIDを削除します。そのためには、次のようにdelete
メソッドを実行します。
log_out
メソッドapp/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
.
.
.
# 現在のユーザーをログアウトする
def log_out
session.delete(:user_id)
@current_user = nil
end
end
log_out
メソッドは、Sessionsコントローラのdestroy
アクションでも同様に使っていきます。
app/controllers/sessions_controller.rb
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
log_out
redirect_to root_url
end
end
test/integration/users_login_test.rb
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
.
.
.
test "login with valid information followed by logout" do
get login_path
post login_path, params: { session: { email: @user.email,
password: 'password' } }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
follow_redirect!
assert_select "a[href=?]", login_path
assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
end
rails test
演習
1:ブラウザから [Log out] リンクをクリックし、どんな変化が起こるか確認してみましょう。また、test/integration/users_login_test.rb
で定義した3つのステップを実行してみて、うまく動いているかどうか確認してみましょう。
動作確認するだけ
2:cookiesの内容を調べてみて、ログアウト後にはsessionが正常に削除されていることを確認してみましょう。
cookiesの中身をログアウトして見るだけ