Ruby on Rails チュートリアル
-
第4版 目次
- 第1章ゼロからデプロイまで
- 第2章Toyアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章基本的なログイン機構
- 第9章発展的なログイン機構
- 第10章ユーザーの更新・表示・削除
- 第11章アカウントの有効化
- 第12章パスワードの再設定
- 第13章ユーザーのマイクロポスト
- 第14章ユーザーをフォローする
|
||
第4版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第4版 目次
- 第1章ゼロからデプロイまで
- 第2章Toyアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章基本的なログイン機構
- 第9章発展的なログイン機構
- 第10章ユーザーの更新・表示・削除
- 第11章アカウントの有効化
- 第12章パスワードの再設定
- 第13章ユーザーのマイクロポスト
- 第14章ユーザーをフォローする
第8章基本的なログイン機構
第7章でWebサイトでの新規ユーザー登録が行えるようになりましたので、今度はユーザーがログインやログアウトを行えるようにしましょう。本章では、ログインの基本的な仕組みを実装していきます。ここでいうログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))です。この認証システムの基盤が出来上がったら、ログイン済みのユーザー (current user) だけがアクセスできるページや、扱える機能などを制御していきます。なお、このような制限や制御の仕組みを認可モデル (Authorization Model) と呼び、例えば本章で実装するログイン済みかどうかでヘッダー部分を切り替える、といった仕組みもこれにあたります。
この認証システムと認可モデルは、今後実装するサンプルアプリケーションの様々な機能の基盤となる仕組みです。例えば第10章では、ログインしたユーザーだけがユーザーの一覧ページに移動できるようにしたり、正当なユーザーだけが自分のプロフィール情報を編集できるようにしたり、管理者だけが他のユーザーをデータベースから削除できるようになります。また第13章では、本章で使えるようになるログイン済みユーザー (current user) を利用して、ユーザーのIDとマイクロポストを関連付ける時に必要になります。最後の第14章では、他のユーザーをフォローする機能や、自分のフィード一覧を実装するとき、「誰がログインしているのか」という情報が必須になります。
なお、第9章では、本章で構築した基本的なログイン機構を改善して、より発展的なログイン機構を実装します。例えば本章で構築する仕組みでは、ブラウザを閉じるとログインしたユーザー情報を強制的に忘れて (forget) しまいますが、第9章で改善した認証機構では、ユーザーが任意でブラウザにログイン情報を覚えさせる (remember me) 機能を実装します (具体的には [remember me] というチェックボックスをログインフォームに用意します)。結果として、この第8章と第9章を通して、1: ブラウザを閉じるとログインを破棄する (Session)、 2: ユーザーのログイン情報を自動で保存する (Cookie)、3: ユーザーがチェックボックスをオンにした場合のみログインを保存する (Remember me) 、という3つの一般的なログイン機構を実装することになります。
8.1 セッション
HTTPはステートレス (Stateless) なプロトコルです。文字通り「状態 (state)」が「ない (less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われます。HTTPは言ってみれば、リクエストが終わると何もかも忘れて次回最初からやり直す健忘症的なプロトコルであり、過去を捨てた旅から旅の流れ者的なプロトコルです (しかし、だからこそこのプロトコルは非常に頑丈なのです)。この本質的な特性のため、ブラウザのあるページから別のページに移動したときに、ユーザーのIDを保持しておく手段がHTTPプロトコル内「には」まったくありません。ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。セッションはHTTPプロトコルと階層が異なる (上の階層にある) ので、HTTPの特性とは別に (若干影響は受けるものの) 接続を確保できます。(訳注: 昔は離れた相手と一手ずつ葉書をやりとりしてのんびりと将棋を指す酔狂な人がときどきいましたが、将棋の対戦を1つのセッションと考えれば、その下の郵便システムはHTTP同様ステートレスであり、対戦者同士が盤の状態を保持していれば、郵便システムや郵便配達夫が対戦の進行や内容に一切かかわらなくてもゲームは成立します)。
Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。cookiesは、あるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できます。アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができます。本節および8.2では、その名もsession
というRailsのメソッドを使って一時セッションを作成します。この一時セッションは、ブラウザを閉じると自動的に終了します1。続く第9章では、Railsのcookies
メソッドを使った、もっと長続きするセッションの作り方について学びます。
セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと統一的に理解できて便利です。ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった具合です。ただしUsersリソースと異なるのは、UsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使う点です。ログインの仕組みの大半は、cookiesを使った認証メカニズムによって構築されています。本節と次の節では、セッション機能を作成する準備として、Sessionコントローラ、ログイン用のフォーム、両者に関連するコントローラのアクションを作成します。8.2では、セッションを操作するために必要なコードをいくつか追加し、ユーザーログインを完成させる予定です。
前章同様に、トピックブランチで作業してから、最後に更新をマージします。
$ git checkout -b basic-login
8.1.1 Sessionsコントローラ
ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付けることにします。ログインのフォームは、この節で扱うnew
アクションで処理します。create
アクションにPOST
リクエストを送信すると、実際にログインします (8.2)。destroy
アクションにDELETE
リクエストを送信すると、ログアウトします (8.3) (表 7.1のHTTPメソッドとRESTアクションの関連付けを思い出しましょう)。
まずは、Sessionsコントローラとnew
アクションを生成するところから始めてみましょう (リスト 8.1)。
$ rails generate controller Sessions new
(なお、rails generate
でnew
アクションを生成すると、それに対応するビューも生成されます。create
やdestroy
には対応するビューが必要ないので、無駄なビューを作成しないためにここではnewだけを指定しています。) 7.2のユーザー登録ページのときと同様に、図 8.1モックアップを元にセッション新規開始用のログインフォームを作成します。
Usersリソースのときは専用のresources
メソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにしました (リスト 7.3) が、Sessionリソースではフルセットはいらないので、「名前付きルーティング」だけを使います。この名前付きルーティングでは、GET
リクエストやPOST
リクエストをlogin
ルーティングで、DELETE
リクエストをlogout
ルーティングで扱います。このルーティングを反映したものをリスト 8.2に示します。なお、rails generate controller
で生成された不要なルートは、このリストから削除してあります。
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
リスト 8.2のルーティングと同様に、リスト 8.1のテストを更新し、新しいログイン用の名前付きルートを使うようにする必要があります。結果は、リスト 8.3のとおりです。
require 'test_helper'
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get login_path
assert_response :success
end
end
リスト 8.2で定義したルーティングのURLやアクション (表 7.1) は、ユーザー用のURLやアクション (表 8.1) とだいたい似ています。
HTTPリクエスト | URL | 名前付きルート | アクション名 | 用途 |
GET |
/login | login_path |
new |
新しいセッションのページ (ログイン) |
POST |
/login | login_path |
create |
新しいセッションの作成 (ログイン) |
DELETE |
/logout | logout_path |
destroy |
セッションの削除 (ログアウト) |
名前付きルーティングもだいぶ増えてきたので、ここまで追加した全ルーティングを表示できると便利そうです。このようなときはrails routes
コマンドを実行してみましょう。いつでも現状のルーティングを確認することができます。
$ rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
help GET /help(.:format) static_pages#help
about GET /about(.:format) static_pages#about
contact GET /contact(.:format) static_pages#contact
signup GET /signup(.:format) users#new
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
今はこのルーティングを完全に理解できる必要はありません。それでもこのリストを何となく眺めてみれば、アプリケーションでサポートされている全アクションがこのリストにあることに気付くと思います。
演習
GET login_path
とPOST login_path
との違いを説明できますか? 少し考えてみましょう。- ターミナルのパイプ機能を使って
rails routes
の実行結果とgrep
コマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか? ヒント: パイプやgrep
の使い方が分からない場合は Learn Enough Command Line to Be Dangerousの Section on Grep (英語) を参考にしてみてください。
8.1.2 ログインフォーム
コントローラとルーティングを定義したので、今度は新しいセッションで使うビュー、つまりログインフォームを整えましょう。図 8.1と図 7.11を比較してみると、ログインフォームとユーザー登録フォームにはほとんど違いがないことがわかります。違いは、4つあったフィールドが [Email] と [Password] の2つに減っていることだけです。
ログインフォームで入力した情報に誤りがあったときは、ログインページをもう一度表示してエラーメッセージを出力します (図 8.2)。7.3.3ではエラーメッセージの表示に専用のパーシャルを使いましたが、そのパーシャルではActive Recordによって自動生成されるメッセージを使っていたことを思い出しましょう。今回扱うセッションはActive Recordオブジェクトではないので、以前のようにActive Recordがよしなにエラーメッセージを表示してくれるということは期待できません。そこで今回は、フラッシュメッセージでエラーを表示します。
リスト 7.15のときは、次のようにユーザー登録フォームでform_for
ヘルパーを使い、ユーザーのインスタンス変数@user
を引数にとっていました。
<%= form_for(@user) do |f| %>
.
.
.
<% end %>
セッションフォームとユーザー登録フォームの最大の違いは、セッションにはSessionモデルというものがなく、そのため@user
のようなインスタンス変数に相当するものもない点です。したがって、新しいセッションフォームを作成するときには、form_for
ヘルパーに追加の情報を独自に渡さなければなりません。
form_for(@user)
Railsでは上のように書くだけで、「フォームのaction
は/usersというURLへのPOST
である」と自動的に判定しますが、セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります2。
form_for(:session, url: login_path)
適切なform_for
を使うことで、リスト 8.1のユーザー登録フォームを参考にして、リスト 7.15に示したようなモックアップに従ったログインフォームを簡単に作成できます (図 8.4)。
<% 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.4のコードを使うと、図 8.3のようにログインフォームが表示されます ([Log in] リンクがまだ効かないので、自分でブラウザのアドレスバーに「/login」とURLを直接入力してください。ログインリンクは8.2.3で動くようにします)。
生成されたHTMLフォームをリスト 8.5に示します。
<form accept-charset="UTF-8" action="/login" method="post">
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
<label for="session_email">Email</label>
<input class="form-control" id="session_email"
name="session[email]" type="text" />
<label for="session_password">Password</label>
<input id="session_password" name="session[password]"
type="password" />
<input class="btn btn-primary" name="commit" type="submit"
value="Log in" />
</form>
リスト 8.5とリスト 7.17を比較することで、フォーム送信後にparams
ハッシュに入る値が、メールアドレスとパスワードのフィールドにそれぞれ対応したparams[:session][:email]
とparams[:session][:password]
になることが推測できると思います。
8.1.3 ユーザーの検索と認証
ユーザー登録では最初にユーザーを作成しましたが、ログインでセッションを作成する場合に最初に行うのは、入力が無効な場合の処理です。最初に、フォームが送信されたときの動作を順を追って理解します。次に、ログインが失敗した場合に表示されるエラーメッセージを配置します (モックアップを図 8.2に示します)。次に、ログインに成功した場合 (8.2) に使う土台部分を作成します。ここではログインが送信されるたびに、パスワードとメールアドレスの組み合わせが有効かどうかを判定します。
それでは、最初に最小限のcreate
アクションをSessionsコントローラで定義し、空のnew
アクションとdestroy
アクションもついでに作成しておきましょう (リスト 8.6)。リスト 8.6のcreate
アクションの中では何も行われませんが、アクションを実行するとnew
ビューが出力されるのでこれで十分です。結果として、/sessions/new フォームから送信すると、図 8.4のようになります。
create
アクション (暫定版) app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
render 'new'
end
def destroy
end
end
図 8.4に表示されているデバッグ情報にご注目ください。8.1.2の終わりでも簡単に触れましたが、params
ハッシュでは、次のようにsession
キーの下にメールアドレスとパスワードがあります。
---
session:
email: 'user@example.com'
password: 'foobar'
commit: Log in
action: create
controller: sessions
ユーザー登録の場合 (図 7.15) と同様、これらのパラメータはリスト 4.13に示したようにネストした (入れ子になった) ハッシュになっていました。特に、params
は次のような入れ子ハッシュになっています。ハッシュの中にハッシュがある構造です。
{ session: { password: "foobar", email: "user@example.com" } }
つまり、次のようなハッシュがあり、
params[:session]
このハッシュの値にまたハッシュが含まれており、
{ password: "foobar", email: "user@example.com" }
結果として、次のようにデータにアクセスすることになります。
params[:session][:email]
実際、上のようにしてフォームから送信されたメールアドレスを取得でき、
params[:session][:password]
また、上のようすればフォームから送信されたパスワードを取得できます。
要するにcreate
アクションの中では、ユーザーの認証に必要なあらゆる情報をparams
ハッシュから簡単に取り出せるということです。そして、認証に必要なすべてのメソッドもここまでに学んであります (そうなるように本書は構成されています)。ここでは、Active Recordが提供するUser.find_by
メソッド (6.1.4) と、has_secure_password
が提供するauthenticate
メソッド (6.3.4) を使っています。ここで、authenticate
メソッドは認証に失敗したときにfalse
を返す (6.3.4) ことを思い出してください。以上の内容をまとめて、ユーザーのログイン部分を実装したものがリスト 8.7になります。
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.7) では、送信されたメールアドレスを使って、データベースからユーザーを取り出しています (6.2.5ではメールアドレスをすべて小文字で保存していたことを思い出しましょう。そこでここではdowncase
メソッドを使って、有効なメールアドレスが入力されたときに確実にマッチするようにしています)。次の行は少しわかりにくいかもしれませんが、Railsプログラミングでは定番の手法です。
user && user.authenticate(params[:session][:password])
&&
(論理積 (and)) は、取得したユーザーが有効かどうかを決定するために使います。Rubyではnil
とfalse
以外のすべてのオブジェクトは、真偽値ではtrue
になる (4.2.3) という性質を考慮すると、&&
の前後の値の組み合わせは表 8.2のようになります。表 8.2を見ると、入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ入力されたパスワードがそのユーザーのパスワードである場合のみ、if
文がtrue
になることがわかります。言葉でまとめると「ユーザーがデータベースにあり、かつ、認証に成功した場合にのみ」となります。
User | Password | a && b |
存在しない | 何でもよい | (nil && [オブジェクト]) == false |
有効なユーザー | 誤ったパスワード | (true && false) == false |
有効なユーザー | 正しいパスワード | (true && true) == true |
8.1.4 フラッシュメッセージを表示する
7.3.3では、ユーザー登録のエラーメッセージ表示にUserモデルのエラーメッセージをうまく利用したことを思い出しましょう。ユーザー登録の場合、エラーメッセージは特定のActive Recordオブジェクトに関連付けられていたのでその手が使えました。しかしセッションではActive Recordのモデルを使っていないため、その手が通用しません。そこで、ログインに失敗したときには代わりにフラッシュメッセージを表示することにします。最初のコードをリスト 8.8に示します (このコードはわざと少し間違えてあります)。
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
flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
render 'new'
end
end
def destroy
end
end
フラッシュメッセージはWebサイトのレイアウトに表示されるので (リスト 7.31)、flash[:danger]
で設定したメッセージは自動的に表示されます。Bootstrap CSSのおかげで適切なスタイルも与えられます (図 8.5)。
本文およびリスト 8.8のコメントで述べたように、このコードには誤りがあります。ページにはちゃんとエラーメッセージが表示されていますが、どこが問題なのでしょうか。実は上のコードのままでは、リクエストのフラッシュメッセージが一度表示されると消えずに残ってしまいます。リスト 7.29でリダイレクトを使ったときとは異なり、表示したテンプレートをrender
メソッドで強制的に再レンダリングしてもリクエストと見なされないため、リクエストのメッセージが消えません。例えばわざと無効な情報を入力して送信してエラーメッセージを表示してから、Homeページをクリックして移動すると、そこでもフラッシュメッセージが表示されたままになっています (図 8.6)。この問題は8.1.5で修正します。
8.1.5 フラッシュのテスト
フラッシュメッセージが消えない問題は、このアプリケーションの小さなバグです。コラム 3.3で解説したテストのガイドラインに従えば、これはまさに「エラーをキャッチするテストを先に書いて、そのエラーが解決するようにコードを書く」に該当する状況です。さっそく、ログインフォームの送信について簡単な統合テストを作成することから始めましょう。この統合テストは、そのままバグのドキュメントにもなり、今後の回帰バグ発生を防止する効能もあります。さらに、今後この統合テストを土台として、より本格的な統合テストを作成するときにも便利です。
アプリケーションのログインの挙動をテストするために、最初に統合テストを生成します。
$ rails generate integration_test users_login
invoke test_unit
create test/integration/users_login_test.rb
次に、図 8.5と図 8.6の手順をテストコードで再現する必要があります。基本的な流れを次に示します。
- ログイン用のパスを開く
- 新しいセッションのフォームが正しく表示されたことを確認する
- わざと無効な
params
ハッシュを使ってセッション用パスにPOSTする - 新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
- 別のページ (Homeページなど) にいったん移動する
- 移動先のページでフラッシュメッセージが表示されていないことを確認する
上のテスト手順の実装をリスト 8.9に示します。
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
リスト 8.9のテストを追加すると、このログインテストは redになるはずです。
$ rails test test/integration/users_login_test.rb
なお、上の例のように、rails test
の引数にテストファイルを与えると、そのテストファイルだけを実行することができます。
リスト 8.9の失敗するテストをパスさせるには、本編のコードでflash
をflash.now
に置き換えます。後者は、レンダリングが終わっているページで特別にフラッシュメッセージを表示することができます。flash
のメッセージとは異なり、flash.now
のメッセージはその後リクエストが発生したときに消滅します (リスト 8.9ではまさにその手順を再現しています)。置き換えの終わった正しいアプリケーションコードをリスト 8.11に示します。
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
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
続いて、ログインの統合テストを含む全テストスイートを実行してみると、 greenになることを確認できます。
$ rails test test/integration/users_login_test.rb
$ rails test
8.2 ログイン
無効な値の送信をログインフォームで正しく処理できるようになったので、次は、実際にログイン中の状態での有効な値の送信をフォームで正しく扱えるようにします。この節では、cookiesを使った一時セッションでユーザーをログインできるようにします。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使います。9.1では、ブラウザを閉じても保持されるセッションを追加します。
セッションを実装するには、様々なコントローラやビューでおびただしい数のメソッドを定義する必要があります。Rubyのモジュール機能を使うと、そうしたメソッドを一箇所にパッケージ化できることを4.2.5で学びました。ありがたいことに、Sessionsコントローラ (8.1.1) を生成した時点で既にセッション用ヘルパーモジュールも (密かに) 自動生成されています。さらに、Railsのセッション用ヘルパーはビューにも自動的に読み込まれます。Railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになります (リスト 8.13)3。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
設定が完了したら、いよいよユーザーログインのコードを書き始めましょう。
8.2.1 log_in
メソッド
Railsで事前定義済みのsession
メソッドを使って、単純なログインを行えるようにします (なお、これは8.1.1で生成したSessionsコントローラとは無関係ですのでご注意ください)。このsession
メソッドはハッシュのように扱えるので、次のように代入します。
session[:user_id] = user.id
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成されます。この後のページで、session[:user_id]
を使ってユーザーIDを元通りに取り出すことができます。一方、cookies
メソッド (9.1) とは対照的に、session
メソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了します。
同じログイン手法を様々な場所で使い回せるようにするために、Sessionsヘルパーにlog_in
という名前のメソッドを定義することにします (リスト 8.14)。
log_in
メソッド app/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
session
メソッドで作成した一時cookiesは自動的に暗号化され、リスト 8.14のコードは保護されます。そしてここが重要なのですが、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできないのです。ただし今述べたことは、session
メソッドで作成した「一時セッション」にしか該当しません。cookies
メソッドで作成した「永続的セッション」ではそこまで断言はできません。永続的なcookiesには、セッションハイジャックという攻撃を受ける可能性が常につきまといます。ユーザーのブラウザ上に保存される情報については、第9章でもう少し注意深く扱うことにします。
リスト 8.14でlog_in
というヘルパーメソッドを定義できたので、やっと、ユーザーログインを行ってセッションのcreate
アクションを完了し、ユーザーのプロフィールページにリダイレクトする準備ができました。作成したコードをリスト 8.15に示します4。
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
上のコードではリダイレクトを使っていますが、
redirect_to user
これは7.4.1で使ったメソッドと同じです。Railsでは上のコードを自動的に変換して、次のようなプロフィールページへのルーティングにしています。
user_url(user)
リスト 8.15でcreate
アクションを定義できたので、8.4で定義したログインフォームも正常に動作するようになったはずです。今はログインしても画面表示が何も変わらないので、ユーザーがログイン中かどうかは、ブラウザセッションを直接確認しない限りわかりません。このままでは困るので、ログインしていることがはっきりわかるようにします。そこで8.2.2では、セッションに含まれるIDを利用して、データベースから現在のユーザー名を取り出して画面で表示する予定です。8.2.3では、アプリケーションのレイアウト上のリンクを変更する予定です。このリンクをクリックすると、現在ログインしているユーザーのプロフィールが表示されます。
8.2.2 現在のユーザー
ユーザーIDを一時セッションの中に安全に置けるようになったので、今度はそのユーザーIDを別のページで取り出すことにしましょう。そのためには、current_user
メソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにします。current_user
メソッドの目的は、次のようなコードを書けるようにすることです。
<%= current_user.name %>
また、このようなコードで、ユーザーのプロフィールページに簡単にリダイレクトできるようにもしたいです。
redirect_to current_user
このとき現在のユーザーを検索する方法として思い付くのは、プロフィールページ (リスト 7.5) と同様に、次のfind
メソッドを使うことでしょう。
User.find(session[:user_id])
しかし6.1.4で既に経験済みのとおり、ユーザーIDが存在しない状態でfind
を使うと例外が発生してしまいます。findのこの動作は、プロフィールページでは適切でした。IDが無効の場合は例外を発生してくれなければ困るからです。しかし、「ユーザーがログインしていない」などの状況が考えられる今回のケースでは、session[:user_id]
の値はnil
になりえます。この状態を修正するために、create
メソッド内でメールアドレスの検索に使ったのと同じfind_by
メソッドを使うことにします。ただし今度はemail
ではなく、id
で検索します。
User.find_by(id: session[:user_id])
今度はIDが無効な場合 (=ユーザーが存在しない場合) にもメソッドは例外を発生せず、nil
を返します。
この手法を使って、current_user
を次のように定義し直します。
def current_user
User.find_by(id: session[:user_id])
end
こうすると、正常に動作はします。しかし少し残念なのは、current_user
メソッドが1リクエスト内で何度も呼び出されると、呼び出された回数と同じだけデータベースにも問い合わせされてしまう点です。そこでRubyの慣習に従って、User.find_by
の実行結果をインスタンス変数に保存することにします。こうすることで、データベースの読み出しは最初の一回だけになり、以後の呼び出しではインスタンス変数を返すようになります。地味なようですが、Railsを高速化させるための重要なテクニックです5。
if @current_user.nil?
@current_user = User.find_by(id: session[:user_id])
else
@current_user
end
or演算子「||
」(4.2.3) を使えれば、先ほどの「メモ化」コードが次のようにたった1行で書けます。
@current_user = @current_user || User.find_by(id: session[:user_id])
ここで重要なのは、Userオブジェクトそのものの論理値は常にtrueになることです。そのおかげで、@current_user
に何も代入されていないときだけfind_by
呼び出しが実行され、無駄なデータベースへの読み出しが行われなくなります。
上のコードはひとまず動作しますが、実はまだ「Ruby的に」正しいコードではありません。@current_user
への代入は、Rubyでは次のような短縮形で書くのが王道です。
@current_user ||= User.find_by(id: session[:user_id])
初めてこの記法を見た方は戸惑うかもしれません。しかしRubyコミュニティでは、この「||=」記法は広く普及しています (コラム 8.1)。
||=
」とは何か?この「||=
」(or equals) という代入演算子はRubyで広く使われているイディオムであり、Ruby開発者を志すならこの演算子に習熟することが重要です。or equalsという概念は一見不思議にみえますが、他のものになぞらえて考えれば難しくありません。
多くのコンピュータプログラムでは、次のような記法で変数の値を1つ増やすことができます。
x = x + 1
そして、Ruby (およびC、C++、Perl、Python、Javaなどの多くのプログラミング言語) では、上の演算を次のような短縮形で表記することもできます。
x += 1
他の演算子についても同様の短縮形が利用できます。
$ rails console >> x = 1 => 1 >> x += 1 => 2 >> x *= 3 => 6 >> x -= 8 => -2 >> x /= 2 => -1
いずれの場合も、●
という演算子があるときの「x = x ● y
」と「x ●= y
」の動作は同じです。
Rubyでは、「変数の値がnil
なら変数に代入するが、nilでなければ代入しない (変数の値を変えない)」という操作が非常によく使われます。4.2.3で説明したor演算子 ||
を使えば、次のように書くことができます。
>> @foo => nil >> @foo = @foo || "bar" => "bar" >> @foo = @foo || "baz" => "bar"
nil
の論理値はfalseになるので、@foo
への最初の代入「nil || "bar"
」の評価値は"bar"
になります。同様に、2つ目の代入「@foo || "baz"
」("bar" || "baz"
など) の評価値は"bar"
になります。Rubyでは、nil
とfalse
を除いて、あらゆるオブジェクトの論理値がtrue
になるように設計されています。さらにRubyでは、||
演算子をいくつも連続して式の中で使う場合、項を左から順に評価し、最初にtrueになった時点で処理を終えるように設計されています。なお、このように||
式を左から右に評価し、演算子の左の値が最初にtrueになった時点で処理を終了するという評価法を短絡評価 (short-circuit evaluation) と呼びます。論理積の&&
演算子も似たような設計になっていますが、項を左から評価して、最初にfalseになった時点で処理を終了する点が異なります。
上記の演算子をコンソールセッション上で実際に実行して比較してみると、@foo = @foo || "bar"
はx = x O y
に該当し、O
が||
に置き換わっただけであることがわかります。
x = x + 1 -> x += 1 x = x * 3 -> x *= 3 x = x - 8 -> x -= 8 x = x / 2 -> x /= 2 @foo = @foo || "bar" -> @foo ||= "bar"
これで「@foo = @foo || "bar"」は「@foo ||= "bar"」と等価であることが理解できます。この記法をcurrent_user
の文脈で使うと次のような簡潔なコードになります。
@current_user ||= User.find_by(id: session[:user_id])
お試しあれ。
(補足: 技術的には、「@foo || @foo = "bar"
」と書いた場合、Rubyの内部では実際にすべての項が評価されます (||
が左辺にある点に注意)。これは@foo
がnil
やfalse
でない場合に、無駄な代入を避ける必要があるためです。しかしこの式の動作では||=
記法の動作と同じにならず、説明上不都合なので、上の解説では@foo = @foo || "bar"
という式を用いて説明しました。
前述の簡潔な記法を current_user
メソッドに適用した結果をリスト 8.16に示します。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end
リスト 8.16のcurrent_user
メソッドが動作するようになったので、ユーザーがログインしているかどうかに応じてアプリケーションの動作を変更するための準備が整いました。
演習
- Railsコンソールを使って、
User.find_by(id: ...)
で対応するユーザーが検索に引っかからなかったとき、nil
を返すことを確認してみましょう。 - 先ほどと同様に、今度は
:user_id
キーを持つsession
ハッシュを作成してみましょう。リスト 8.17に記したステップに従って、||=
演算子がうまく動くことも確認してみましょう。
session
のシミュレーション
>> session = {}
>> session[:user_id] = nil
>> @current_user ||= User.find_by(id: session[:user_id])
<ココに何が表示されるか?>
>> session[:user_id]= User.first.id
>> @current_user ||= User.find_by(id: session[:user_id])
<ココに何が表示されるか?>
>> @current_user ||= User.find_by(id: session[:user_id])
<ココに何が表示されるか?>
8.2.3 レイアウトリンクを変更する
ログイン機能の最初の具体的な応用として、ユーザーがログインしているときとそうでないときでレイアウトを変更してみましょう。特に、図 8.7のモックアップ6に示したように、「ログアウト」リンク、「ユーザー設定」リンク、「ユーザー一覧」リンク、「プロフィール表示」リンクも追加します。図 8.7では、ログアウトのリンクとプロフィールのリンクは [Account] メニューの項目として表示されている点にご注目ください。リスト 8.19では、Bootstrapを使ってこのようなメニューを実現する方法を示します。
筆者なら即、この時点で上のメニューを対する統合テストを書くでしょう。コラム 3.3でも解説したように、Railsのテストツールを習熟するにつれ、何も指示されなくても筆者のようにこの時点でテストを書きたくなると思います。とはいうものの、今は無理は禁物です。このテストでもまたいくつか新しい概念を覚える必要があるので、テストの作成は8.2.4に回すことにします。
さて、レイアウトのリンクを変更する方法として考えられるのは、ERBコードの中でif-else文を使用し、条件に応じて表示するリンクを使い分けることです。
<% if logged_in? %>
# ログインユーザー用のリンク
<% else %>
# ログインしていないユーザー用のリンク
<% end %>
このコードを書くためには、論理値を返すlogged_in?
メソッドが必要なので、まずはそれを定義していきましょう。
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_user
がnil
ではないという状態を指します。これをチェックするには否定演算子 (4.2.3) が必要なので、!
を使っていきます。作成したlogged_in?
メソッドをリスト 8.18に示します。
logged_in?
ヘルパーメソッド app/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
end
リスト 8.18を追加したので、これでユーザーのログイン時にレイアウトを変えられるようにする準備が整いました。なお、新しく作るリンクは4つですが、そのうち次の2つのリンクについてはしばらく未実装のままとします (第10章で完成させます)。
<%= link_to "Users", '#' %>
<%= link_to "Settings", '#' %>
ログアウト用リンクでは、リスト 8.2で定義したログアウト用パスを使います。
<%= link_to "Log out", logout_path, method: :delete %>
上のコードでは、ログアウト用リンクの引数としてハッシュを渡している点にご注目ください。このハッシュでは、HTTPのDELETE
リクエストを使うように指示しています7。プロフィール用リンクについても同様に、次のように変更します。
<%= link_to "Profile", current_user %>
なお、上のコードは省略系で、このように書くこともできます。
<%= link_to "Profile", user_path(current_user) %>
しかしこの状況ではcurrent_user
を使う方が、Railsによってuser_path(current_user)
に変換され、プロフィールへのリンクが自動的に生成できるので便利です。次に、ユーザーがログインしていない場合は、リスト 8.2のログイン用パスを使って、次のようにログインフォームへのリンクを作成します。
<%= link_to "Log in", login_path %>
ここまでの結果をヘッダーのパーシャル部分に適用すると、リスト 8.19のようになります。
<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>
レイアウトに新しいリンクを追加したので、リスト 8.19にBootstrapのドロップダウンメニュー機能8を適用できる状態になりました。具体的には、Bootstrapに含まれるCSSのdropdown
クラスやdropdown-menu
などを使っています。これらのドロップダウン機能を有効にするため、Railsのapplication.js
ファイルを通して、Bootstrapに同梱されているJavaScriptライブラリを読み込むよう、アセットパイプラインに指示します (リスト 8.20)。(訳注: Rails 5.1 からjQueryもデフォルトで読み込まれなくなったので、jQueryも追加する必要があります。詳しくはRailsガイドのをRuby on Rails 5.1リリースノート参照してください。)
application.js
にBootstrapのJavaScriptライブラリを追加する app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .
この時点で、ログインパスにアクセスして有効なユーザー (ユーザー名がexample@railstutorial.org
、パスワードがfoobar
) としてログインできるようになっているので、これまでの3つのセクションのコードを効率よくテストできるようになります9。リスト 8.19やリスト 8.20のコードにより、図 8.8のようにドロップダウンメニューとログイン中ユーザー用のリンクが表示されることを確認してみましょう。
また、ブラウザを完全に閉じてしまうと、期待どおりアプリケーションのログイン情報が消去され、再びログインを要求されるようになったことも確認してみてください10。
8.2.4 レイアウトの変更をテストする
アプリケーションでのログイン成功を手動で確認したので、先に進む前に統合テストを書いてこの動作をテストで表現し、今後の回帰バグの発生をキャッチできるようにしましょう。リスト 8.9を元にテストを作成し、次の操作手順をテストで記述して確認できるようにします。
- ログイン用のパスを開く
- セッション用パスに有効な情報をpostする
- ログイン用リンクが表示されなくなったことを確認する
- ログアウト用リンクが表示されていることを確認する
- プロフィール用リンクが表示されていることを確認する
上の変更を確認するためには、テスト時に登録済みユーザーとしてログインしておく必要があります。当然ながら、データベースにそのためのユーザーが登録されていなければなりません。Railsでは、このようなテスト用データをfixture (フィクスチャ) で作成できます。このfixtureを使って、テストに必要なデータをtestデータベースに読み込んでおくことができます。6.2.5では、メールの一意性テスト (リスト 6.31) がパスするためにデフォルトのfixtureを削除する必要がありました。今度は自分で空のfixtureファイルを作成してデータを追加しましょう。
現時点のテストでは、ユーザーは1人いれば十分です。そのユーザーには有効な名前と有効なメールアドレスを設定しておきます。テスト中にそのユーザーとして自動ログインするために、そのユーザーの有効なパスワードも用意して、Sessionsコントローラのcreate
アクションに送信されたパスワードと比較できるようにする必要があります。図 6.8のデータモデルをもう一度見てみると、password_digest
属性をユーザーのfixtureに追加すればよいことが分かります。そのために、digest
メソッドを独自に定義することにします。
6.3.1で説明したように、has_secure_password
でbcryptパスワードが作成されるので、同じ方法でfixture用のパスワードを作成します。Railsのsecure_passwordのソースコードを調べてみると、次の部分でパスワードが生成されていることが分かります。
BCrypt::Password.create(string, cost: cost)
上のstring
はハッシュ化する文字列、cost
はコストパラメータと呼ばれる値です。コストパラメータでは、ハッシュを算出するための計算コストを指定します。コストパラメータの値を高くすれば、ハッシュからオリジナルのパスワードを計算で推測することが困難になりますので、本番環境ではセキュリティ上重要です。しかしテスト中はコストを高くする意味はないので、digest
メソッドの計算はなるべく軽くしておきたいです。この点についても、secure_password
のソースコードには次の行が参考になります。
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
少々込み入っていますが、コストパラメータをテスト中は最小にし、本番環境ではしっかりと計算する方法がわかれば十分です。なお「?
」〜「:
」という記法については、9.2で解説します。
このdigest
メソッドは、今後様々な場面で活用します。例えば9.1.1でもdigest
を再利用するので、このdigestメソッドはUserモデル (user.rb
) に置いておきましょう。この計算はユーザーごとに行う必要はないので、fixtureファイルなどでわざわざユーザーオブジェクトにアクセスする必然性はありません (つまり、インスタンスメソッドで定義する必要はありません)。そこで、digest
メソッドをUserクラス自身に配置して、クラスメソッドにすることにしましょう (クラスメソッドの作り方については4.4.1で簡単に説明しました)。作成したコードをリスト 8.21に示します。
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
リスト 8.21のdigest
メソッドができたので、有効なユーザーを表すfixtureを作成できるようになりました (リスト 8.22)11。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
上のコードにあるように、fixtureではERbを利用できる点にご注目ください。
<%= User.digest('password') %>
上のERbコードでテストユーザー用の有効なパスワードを作成できます。
has_secure_password
で必要となるpassword_digest
属性はこれで準備できましたが、ハッシュ化されていない生のパスワードも参照できると便利です。しかし残念なことに、fixtureではこのようなことはできません。さらに、リスト 8.22にpassword
属性を追加すると、そのようなカラムはデータベースに存在しないというエラーが発生します。実際、データベースにはそんなカラムはありません。この状況を切り抜けるため、テスト用のfixtureでは全員同じパスワード「password
」を使うことにします (これはfixtureでよく使われる手法です)。
有効なユーザー用のfixtureを作成できたので、テストでは次のようにfixtureのデータを参照できるようになります。
user = users(:michael)
上のusers
はfixtureのファイル名users.yml
を表し、:michael
というシンボルはリスト 8.22のユーザーを参照するためのキーを表します。
fixtureのユーザーにアクセスできるようになったので、レイアウトのリンクをテストできる状態になりました。レイアウトのリンクをテストするには、前述の操作手順をテストコードに書き換えます (リスト 8.23)。
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
上のコードのうち、次の行は
assert_redirected_to @user
リダイレクト先が正しいかどうかをチェックしています。
follow_redirect!
また上の行では、そのページに実際に移動します。リスト 8.23では、ログイン用リンクが表示されなくなったことも確認しています。このチェックは、ログインパスのリンクがページにないかどうかで判定しています。
assert_select "a[href=?]", login_path, count: 0
count: 0
というオプションをassert_select
に追加すると、渡したパターンに一致するリンクが0かどうかを確認するようになります。
アプリケーションのコードは既に動作するようになっているので、ここでテストを実行すると greenになるはずです。
$ rails test test/integration/users_login_test.rb
8.2.5 ユーザー登録時にログイン
以上で認証システムが動作するようになりましたが、今のままでは、登録の終わったユーザーがデフォルトではログインしていないので、ユーザーがとまどう可能性があります。ユーザー登録が終わってからユーザーに手動ログインを促すと、ユーザーに余分な手順を強いることになるので、ユーザー登録中にログインを済ませておくことにします。ユーザー登録中にログインするには、Usersコントローラのcreate
アクションにlog_in
を追加するだけで済みます (リスト 8.25)12。
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
リスト 8.25の動作をテストするために、リスト 7.33のテストに1行追加して、ユーザーがログイン中かどうかをチェックします。そのために、リスト 8.18で定義したlogged_in?
ヘルパーメソッドとは別に、is_logged_in?
ヘルパーメソッドを定義しておくと便利です。このヘルパーメソッドは、テストのセッションにユーザーがあればtrue
を返し、それ以外の場合はfalse
を返します (リスト 8.26)。残念ながらヘルパーメソッドはテストから呼び出せないので、リスト 8.18のようにcurrent_user
を呼び出せません。session
メソッドはテストでも利用できるので、これを代わりに使います。ここでは取り違えを防ぐため、logged_in?
の代わりにis_logged_in?
を使って、ヘルパーメソッド名がテストヘルパーとSessionヘルパーで同じにならないようにしておきます13。
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
# テストユーザーがログイン中の場合にtrueを返す
def is_logged_in?
!session[:user_id].nil?
end
end
リスト 8.26のコードを使うと、ユーザー登録の終わったユーザーがログイン状態になっているかどうかを確認できます (リスト 8.27)。
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
演習
- リスト 8.25の
log_in
の行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。 - 現在使っているテキストエディタの機能を使って、リスト 8.25をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については Test Editor Tutorial の Commenting Out (英語) などを参照してみてください。
8.3 ログアウト
8.1で解説したように、このアプリケーションで扱う認証モデルでは、ユーザーが明示的にログアウトするまではログイン状態を保てなくてはなりません。この節では、そのために必要なログアウト機能を追加することにします。ログアウト用リンクはリスト 8.19で既に作成済みなので、ユーザーセッションを破棄するための有効なアクションをコントローラで作成するだけで済みます。
これまで、SessionsコントローラのアクションはRESTfulルールに従っていました。new
でログインページを表示し、create
でログインを完了するといった具合です。セッションを破棄するdestroy
アクションも、引き続き同じ要領で作成することにします。ただし、ログインの場合 (リスト 8.15とリスト 8.25) と異なり、ログアウト処理は1か所で行えるので、destroy
アクションに直接ログアウト処理を書くことにします。9.3でも説明しますが、この設計 (および若干のリファクタリング) のおかげで認証メカニズムのテストが行い易くなります。
ログアウトの処理では、リスト 8.14のlog_in
メソッドの実行結果を取り消します。つまり、セッションからユーザーIDを削除します14。そのためには、次のようにdelete
メソッドを実行します。
session.delete(:user_id)
上のコードで、現在のユーザーをnil
に設定できます。今回はログイン済みでない場合、即座にルートURLにリダイレクトするようにしているので、このコードが特に問題になることはありません15。次に、log_in
および関連メソッドのときと同様に、Sessionヘルパーモジュールに配置するlog_out
メソッドとしてリスト 8.29のように定義してみましょう。
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
アクションでも同様に使っていきます (リスト 8.30)。
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
ログアウト機能をテストするために、リスト 8.23のユーザーログインのテストに手順を若干追加します。ログイン後、delete
メソッドでDELETE
リクエストをログアウト用パスに発行し (表 8.1参照)、ユーザーがログアウトしてルートURLにリダイレクトされたことを確認します。ログイン用リンクが再度表示されること、ログアウト用リンクとプロフィール用リンクが非表示になることも確認します。手順を追加したテストをリスト 8.31に示します。
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
テストでis_logged_in?
ヘルパーメソッドを利用できるようにしてあったおかげで、有効な情報をセッション用パスにpostした直後にassert is_logged_in?
で簡単にテストできました。
セッションのdestroy
アクションの定義とテストも完成したので、ついにサンプルアプリケーションの基本となる「ユーザー登録・ログイン・ログアウト」の機能すべてが完成しました。またこの時点で、テストスイートは greenになるはずです。
$ rails test
8.4 最後に
本章では、サンプルアプリケーションの基本的なログイン機構 (認証システム) を実装しました。次の章では、このログイン機構をさらに改善して、セッションより長くログイン情報を維持する方法について学んでいきます。
それでは次の章に進む前に、今回の変更をmasterブランチにマージしておきましょう。
$ rails test
$ git add -A
$ git commit -m "Implement basic login"
$ git checkout master
$ git merge basic-login
マージ後、リモートのリポジトリにpushします。
$ rails test
$ git push
最後に、いつものようにHerokuにデプロイしましょう。
$ git push heroku
8.4.1 本章のまとめ
- Railsの
session
メソッドを使うと、あるページから別のページに移動するときの状態を保持できる。一時的な状態の保存にはcookiesも使える - ログインフォームでは、ユーザーがログインするための新しいセッションが作成できる
flash.now
メソッドを使うと、描画済みのページにもフラッシュメッセージを表示できる- テスト駆動開発は、回帰バグを防ぐときに便利
session
メソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる- ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
- 統合テストでは、ルーティング、データベースの更新、レイアウトの変更が正しく行われているかを確認できる
- ブラウザによっては、「中断した時点から再開」などのオプション機能でセッションを復旧できるものもあります。このような動作はブラウザ依存で、かつブラウザ側でしか行えないので、Railsサーバーではこうしたセッション復旧機能を実現することはできません。 ↑
- 注:
form_for
の代わりにform_tag
を使うこともでき、Railsではこの方が慣用的な方法です。しかし、ユーザー登録フォームではform_for
を使うのが一般的であり、並列構造を強調するためにもform_for
を使いました。 ↑ - 筆者はこのテクニックが好きです。というのも、このテクニックはRails流と言うより、Ruby単体のモジュールを追加する方法と基本的に同じだからです。なお、Rails 4で導入されたconcernsという機能でも、似たようなテクニックが使われています。concernsについて詳しく学びたい場合は「how to use concerns in Rails」で検索してみてください。 ↑
- リスト 8.13でモジュールを読み込んでいるので、Sessionコントローラで
log_in
メソッドが使えます。 ↑ - このように、メソッド呼び出しでの変数代入を記憶して次回以降の呼び出しで使い回す手法をメモ化 (memoization) と呼びます。memorizationのスペルミスではなく、それをもじったmemoization (rがない) という造語であることにご注意ください。 ↑
- 画像の引用元: https://www.flickr.com/photos/elevy/14730820387 2016-06-03. Copyright © 2014 by Elias Levy (改変不可の Creative Commons Attribution 2.0 Generic ライセンス) ↑
- Webブラウザは実際には
DELETE
リクエストを発行できないので、RailsではJavaScriptを使ってこのリクエストを「偽造」しています。 ↑ - 詳しくはBootstrapコンポーネント一覧ページ (英語) を参照してください。 ↑
- 環境によっては、この時点で一度Webサーバーを再起動する必要があります (コラム 1.1)。 ↑
- クラウドIDEをご利用の場合は、別のブラウザでログイン動作を確認することをおすすめします。そうでないと、クラウドIDEを開いているブラウザも一緒に閉じるはめになってしまいます。 ↑
- fixtureでは空白スペースの数は重要です。タブを使わずに、リスト 8.22と同じ空白スペースになるように注意してください。 ↑
- Sessionsコントローラがあることで、Usersコントローラで
log_in
メソッドを使えるようになります。そのために必要なモジュールはリスト 8.13で対応しています。 ↑ - 一例として、かつて筆者が作成したテストスイートでは、Sessionsヘルパーから
log_in
メソッドをうっかり削除してしまったにもかかわらず、テストが greenのまま変わらなかったことがありました。原因は、テストで使っていたヘルパーメソッドの名前が、うかつにもSessionsヘルパーメソッド名と同じだったことです。そのため、アプリケーションが壊れていてもテストがパスしてしまいました。テストヘルパーメソッド名を、Sessionヘルパーメソッド名log_in_as
(リスト 9.24) とは異なるis_logged_in?
で定義することで、この問題を回避できます。 ↑ - ブラウザによっては、「ログイン状態を保存する」などでセッションを自動復元する機能がサポートされていることがあります。この機能は開発の邪魔になるので、ログアウトする前にこの機能を必ずオフにしておいてください。 ↑
- インスタンス変数
@current_user
をnil
にする必要があるのは、@current_user
がdestroy
アクションより前に作成され (作成されていない場合)、かつ、リダイレクトを直接発行しなかった場合だけです。今回はリダイレクトを直接発行しているので、不要です。現実にこのような条件が発生する可能性はかなり低く、このアプリケーションでもこのような条件を作り出さないように開発しているので、本来はnilに設定する必要はないのですが、ここではセキュリティ上の死角を万が一にでも作り出さないためにあえてnilに設定しています。 ↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!