Ruby on Rails チュートリアル
-
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
|
||
第2版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
第8章サインイン、サインアウト
第7章でWebサイトでの新規ユーザー登録が行えるようになりましたので、今度はユーザーがサインインとサインアウトを行えるようにしましょう。これにより、サインインの状態と現在のユーザーidに応じて動作を変更できるようになります。たとえば、この章では、サイトのヘッダー部分にサインイン/サインアウトのリンクとプロファイルへのリンクを表示するようにします。第10章では、サインインしたユーザーidを使用して、そのユーザーに関連付けられたマイクロポストを作成します。また第11章では、現在のユーザーが他のユーザーをフォローして、マイクロポストのフィードを受け取ることができるようにします。
ユーザーがサインインすることでセキュリティモデルも実装され、サインインしているユーザーidに基づいて、特定のページへのアクセスを制限することもできます。第9章でも説明しますが、たとえばサインインしたユーザーのみがユーザー情報編集ページにアクセスできるようにします。サインインシステムを使用して、管理者ユーザーに特別な権限を与えることもできます。たとえば (第9章で導入する) データベースからユーザーを削除する機能があります。
コアとなる認証機能を実装した後は、少々回り道して、振舞駆動開発 (8.3) で有名なCucumberというツールを試してみます。特に、2つの開発手法を比較するために、RSpecによる結合テストの組み合わせをCucumberで再実装します。
前章同様に、トピックブランチで作業してから、最後に更新をマージします。
$ git checkout -b sign-in-out
8.1セッション、サインインの失敗
セッションとは、2つのコンピュータの間、たとえばクライアント側のブラウザとサーバーで動作しているRailsとの間の、半永続的な接続のことです。ここでは、”サインイン” の共通パターンを実装するためにセッションを使用します。ここで、Webの世界にはセッションの振る舞いを表現するためのいくつかの異なったモデルがあります。ブラウザを閉じるとセッションを終了する「忘却モデル」、[パスワードを保存する] チェックボックスを使用してセッションを継続する「継続モデル」、ユーザーが明示的にサインアウトするまでセッションを継続する「永続モデル」などです1。ここでは、最後の永続モデルを採用することにします。ユーザーがサインインすると、ユーザーが明示的にサインアウトするまでサインインの状態を永続させます (8.2.1では、“永続” が実際にはどのぐらいの時間であるかをお見せします)。
セッションは、RESTfulなリソースとして作成しておくと便利です。たとえばサインインページをnewセッションで、サインインをcreateセッションで、サインアウトをdestroyセッションでそれぞれ扱います。Usersリソースのように (Usersモデルを経由して) データベースをバックエンドに持つリソースとは異なり、このSessionsリソースではcookiesを使用します。cookiesとは、ブラウザに保存される小さなテキストデータです。サインイン関連の作業の大半は、このcookiesをベースにして認証システムを構築することになります。この節と次の節では、セッション機能を作成する準備として、Sessionコントローラ、サインイン用のフォーム、そしてこれらに関連するコントローラのアクションを作成します。次の8.2では、cookies管理のために必要なコードを追加して、ユーザーがサインインするための仕組みを完成させます。
8.1.1 Sessionコントローラ
サインインとサインアウトの要素は、Sessionsコントローラの特定のRESTアクションにそれぞれ対応します。サインインのフォームは、この節のnew
アクションで処理されます。実際のサインイン処理は、create
アクションにPOSTリクエストが送信されたときに処理されます (8.1および8.2) 。サインアウトは、destroy
アクションにDELETEリクエストを送信することで処理されます(8.2.6)。(表7.1のHTTPメソッドとRESTアクションの関連付けを思い出してください。) それでは最初に、Sessionsコントローラと認証システムをテストする結合テストを作成します。
$ rails generate controller Sessions --no-test-framework
$ rails generate integration_test authentication_pages
7.2で作成したユーザー登録ページのモデルを元に、新しいセッションを確立するためのフォームを図8.1のモックアップに従って作成します。
このサインインページは、signin_path
によって仮に定義されるURLでアクセス可能になります。リスト8.1のように、最初は最小限のテストから始めます (リスト7.6のユーザー登録ページのコードと似ているので、比較してみてください)。
new
アクションとビューをテストする。spec/requests/authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
subject { page }
describe "signin page" do
before { visit signin_path }
it { should have_content('Sign in') }
it { should have_title('Sign in') }
end
end
以下のテストは、この時点では失敗するはずです。
$ bundle exec rspec spec/
リスト8.1のテストがパスするためには、サインインページ用にカスタマイズした名前付きルートを使用して、Sessionsリソースのルーティングを定義する必要があります。このサインインページはSessionコントローラのnew
アクションと対応しています。Usersリソースの場合と同様に、resources
メソッドを使用して通常のRESTfulなルーティングを設定することができます。
resources :sessions, only: [:new, :create, :destroy]
セッションを編集したりユーザーに表示したりする必要がないので、resources
メソッドに:only
オプションを追加し、new
、create
、destroy
の3つのアクションのみを有効にします。サインインとサインアウト用のルーティングを含む完全なコードをリスト8.2に示します。
config/routes.rb
SampleApp::Application.routes.draw do
resources :users
resources :sessions, only: [:new, :create, :destroy]
root 'static_pages#home'
match '/signup', to: 'users#new', via: 'get'
match '/signin', to: 'sessions#new', via: 'get'
match '/signout', to: 'sessions#destroy', via: 'delete'
.
.
.
end
注: サインアウトのルーティングにあるvia: ’delete’
は、このアクションが HTTPのDELETEリクエストによって呼び出されることを示しています。
リスト8.2で定義されたリソースは、表8.1に示したように、表7.1とよく似たURLとアクションを提供します。注:サインインとサインアウトのアクションのルーティングはカスタムで設定しますが、セッションの生成アクションへのルーティングはデフォルトを使います (i.e., [resource name]_path
).
HTTPリクエスト | URL | 名前付きルート | アクション | 用途 |
---|---|---|---|---|
GET | /signin | signin_path | new | 新しいセッション用 (サインイン) |
POST | /sessions | sessions_path | create | 新しいセッションを作成する |
DELETE | /signout | signout_path | destroy | セッションを削除する (サインアウト) |
次に、リスト8.1のテストがパスするようにするために、リスト8.3に示したようにnew
アクションをSessionsコントローラに追加します (後で使えるようcreate
アクションとdestroy
アクションも定義しておきます) 。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
end
def destroy
end
end
最後に、サインインページを新規に定義します。このページは新規セッション用なので、今から作成するサインインページをapp/views/sessions/new.html.erb
に置くことに注目してください。リスト8.4に示したように、今の時点ではタイトルと一番上のヘッダ部分のみを定義します。
app/views/sessions/new.html.erb
<% provide(:title, "Sign in") %>
<h1>Sign in</h1>
これにより、リスト8.1のテストが青信号 (成功) になります。これで実際のサインインフォームを作成する準備ができました。
$ bundle exec rspec spec/
8.1.2サインインをテストする
図8.1と図7.11を比較すると、サインインフォーム (新規セッションフォーム) の外観は、4つあったフィールドがメールアドレスとパスワードの2つになったこと以外は、ユーザー登録フォームによく似ていることに気付くと思います。ユーザー登録フォームのときと同様に、サインインフォームでもCapybaraを使ってフォームに値を入力し、ボタンをクリックするテストを行うことができます。
テストを作成していると、私たちはアプリケーションをさまざまな側面から設計することを強いられます。これはテスト駆動開発の素晴らしい副次的効果のひとつです。図8.2のモックアップで示されているように、最初はサインインの入力に誤りがあった場合のテストから作成します。
図 8.2に示されているように、サインインで入力した情報に誤りがあったときは、サインインページをもう一度表示して、エラーメッセージを出力します。ここではエラーをフラッシュメッセージとして表示するので、以下のようにテストできます。
it { should have_selector('div.alert.alert-error', text: 'Invalid') }
上のコードでは、リスト7.32で導入したCapybaraのhave_selector
メソッドを使用しています (第7章の演習)。have_selector
メソッドは、特定のセレクタ要素 (HTMLタグなど) があるかどうかをチェックします。なお、Capybara 2.0では画面に表示される要素しかチェックできません。セレクタ要素(つまりタグ)は以下のように指定します。
div.alert.alert-error
上のコードはdiv
タグがあるかどうかをチェックします。上のコードのドットはCSSのクラスを意味することを思い出してください (5.1.2) 。つまりこのテストでは、"alert"
クラスと"alert-error"
クラスのdiv
タグを対象にしていることがわかります。また、エラーメッセージに"Invalid"
という単語が含まれていることもテストします。これらを合わせると、次のフォームの要素を探しだしてテストが行われます。
<div class="alert alert-error">Invalid...</div>
リスト 8.5のコードは、タイトルとフラッシュメッセージをまとめてテストします。ただしこのテストは、実はある重要な点を欠いています。これは8.1.5で改善します。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "signin" do
before { visit signin_path }
describe "with invalid information" do
before { click_button "Sign in" }
it { should have_title('Sign in') }
it { should have_selector('div.alert.alert-error', text: 'Invalid') }
end
end
end
サインインに失敗した時のテストができたので、次はサインインに成功した場合のテストを作成しましょう。ユーザープロファイルページが表示されること (ページタイトルがユーザー名になっていること)、サイトのナビゲーションに次の3つの変更が加えられていることを確認するよう、テストを変更します。
- プロファイルページヘのリンクの表示
- [Sign out] リンクの表示
- [Sign in] リンクの非表示
([Setting] リンクについては9.1で、[Users] リンクについては9.3でそれぞれテストします) 。これらの変更を加えたモックアップを図8.3に示しました2。サインアウトとプロファイルページヘのリンクは、[Account] メニューにドロップダウンで表示されます。Bootstrapを使用してドロップダウンメニューを作成する方法については8.2.4で説明します。
サインインに成功したときのテストコードをリスト8.6に示しました。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "signin" do
before { visit signin_path }
.
.
.
describe "with valid information" do
let(:user) { FactoryGirl.create(:user) }
before do
fill_in "Email", with: user.email.upcase
fill_in "Password", with: user.password
click_button "Sign in"
end
it { should have_title(user.name) }
it { should have_link('Profile', href: user_path(user)) }
it { should have_link('Sign out', href: signout_path) }
it { should_not have_link('Sign in', href: signin_path) }
end
end
end
上のコードでは、Capybaraのhave_link
メソッドが導入されています。このメソッドは、リンクテキストを引数にとります。オプションとして次のように:href
パラメータを加えると、
it { should have_link('Profile', href: user_path(user)) }
アンカータグa
にhref
(URL) 属性を追加することもできます (この例では、ユーザープロファイルへのリンク)。上のテストでは、upcase
メソッドを使用してユーザーのメールアドレスを大文字に変換することで、大文字小文字を区別しないデータベースが使用されている場合であってもユーザーを確実に検索できるように配慮してあることに注目してください。
8.1.3サインインのフォーム
テストの準備が完了したので、いよいよサインインフォームの開発に取りかかりましょう。リスト7.17のときは、以下のようにユーザー登録フォームでform_for
ヘルパーを使用し、ユーザーのインスタンス変数 @user
を引数にとっていました。
<%= form_for(@user) do |f| %>
.
.
.
<% end %>
このユーザー登録フォームとサインインフォームの主な違いは、サインインフォームにはSessionモデルがないために@user
変数のような存在がないことです。 これはつまり、新しいセッションフォームを作成するときにform_for
ヘルパーに追加の情報を渡す必要があるということです。
form_for(@user)
Railsは上の場合、フォームのaction
は/usersというURLへのPOSTであると自動的に判定しますが、セッションの場合はリソースの名前とそれに対応するURLを指定する必要があります。
form_for(:session, url: sessions_path)
(注: form_for
の代わりにform_tag
を使うこともでき、Railsではこの方が慣用的な方法です。しかし、ユーザー登録フォームではform_forを使用する方が一般的であり、並列構造を強調するためにもform_forを使用しました。form_tag
を使ったフォームについては(8.5)の演習に回します。)
適切なform_for
を使用することで、リスト7.17のユーザー登録フォームを参考にして、リスト8.7に示したようなモックアップに従ったサインインフォームを簡単に作成できます (図8.1)。
app/views/sessions/new.html.erb
<% provide(:title, "Sign in") %>
<h1>Sign in</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(:session, url: sessions_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit "Sign in", class: "btn btn-large btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
ユーザーにとって便利なように、ユーザー登録ページのリンクを追加してあることに注目してください。リスト 8.7のコードで生成されるサインインフォームを図 8.4に示します。
これまではRailsが生成したHTMLをブラウザでたびたび確認していた方も多いと思いますが、やがてその習慣は忘れ去られ、ヘルパーが正常に動作していることを信頼するようになる日が来ることでしょう。今はいつものように(リスト 8.8)を見てみましょう。
<form accept-charset="UTF-8" action="/sessions" method="post">
<div>
<label for="session_email">Email</label>
<input id="session_email" name="session[email]" type="text" />
</div>
<div>
<label for="session_password">Password</label>
<input id="session_password" name="session[password]"
type="password" />
</div>
<input class="btn btn-large btn-primary" name="commit" type="submit"
value="Sign in" />
</form>
リスト8.8とリスト7.20を比較することで、フォーム送信後にparams
ハッシュに入る値が、メールアドレスとパスワードのフィールドにそれぞれ対応したparams[:session][:email]
とparams[:session][:password]
になることを推測できることでしょう。
8.1.4確認フォームを送信する
ユーザーを作成する (ユーザー登録) 場合と同様、セッションを作成する場合 (サインイン) で最初にやることは、正しくない入力の取り扱いです。ユーザー登録のテストは既に作成済みです (リスト 8.5)。そしてサインインのアプリケーションコードは、いくつかの点においてユーザー登録のコードとわずかに異なります。最初に、フォームが送信されたとき何が起こるかを考えましょう。次に、サインインが失敗した場合に表示されるエラーメッセージを配置します (モックアップを図8.2に示しました)。次に、サインインが送信されるたびに、パスワードとメールアドレスの組み合わせが正しいかどうかを判定し、サインインに成功した場合に使用する土台 (8.2) を作成します。
最初に、Sessionsコントローラに最小限のcreate
アクションを定義します (リスト8.9) 。このアクションは何も実行しませんが、new
ビューを表示します。/sessions/newフォームを表示し、フィールドを空白のまま [Sign in] を押して送信すると図8.5のように表示されます。
create
アクションの試作バージョン。app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
render 'new'
end
.
.
.
end
図8.5 に表示されているデバッグ情報に注目してください。8.1.3 の終わりにもヒントがありましたが、params
ハッシュはメールアドレスとパスワードを以下のように:session
キーの下に持ちます。
---
session:
email: ''
password: ''
commit: Sign in
action: create
controller: sessions
ユーザー登録の場合 (図7.15) と同様、これらのパラメータはリスト4.6に示したようにネストした (入れ子になった) ハッシュになっています。特に、params
は以下のような入れ子ハッシュになっています。
{ session: { password: "", email: "" } }
これはつまり、
params[:session]
上はそれ自体がハッシュであり、以下の要素を含んでいます。
{ password: "", email: "" }
その結果、
params[:session][:email]
上はフォームから送信されたメールアドレスであり、
params[:session][:password]
上はフォームから送信されたパスワードです。
言い換えれば、create
アクションのparams
ハッシュには、ユーザーの認証に必要な情報がすべて含まれているということになります。ここまでに、私たちはすでに必要なメソッドを手にしています。 Active RecordがUser.find_by_email
を提供し (6.1.4)、 has_secure_password
がauthenticate
メソッドを提供しています (6.3.3)。認証に失敗したとき、authenticate
の返り値はfalse
になることを思い出してください。ユーザーのサインイン方法の方針をまとめると以下のようになります。
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
else
# エラーメッセージを表示し、サインインフォームを再描画する。
end
end
最初の行は、送信されたメールアドレスを使用して、データベースからユーザーを取り出しています。(6.2.5ではメールアドレスはすべて小文字で保存されていたことを思い出してください。従って、ここではdowncase
メソッドを使用して、正しいメールアドレスが入力されたときに確実にマッチするようにしています)。次の行は少しわかりにくいかもしれませんが、Railsプログラミングでは定番の手法です。
user && user.authenticate(params[:session][:password])
&&
(論理積) は、取得したユーザーが有効かどうかを決定するのに使用します。nil
とfalse
以外のすべてのオブジェクトは、真偽値ではtrue
になる (4.2.3) というRubyの性質を考慮すると、 組み合わせは表8.2のようになります。 表 8.2を見ると、入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ入力されたパスワードがそのユーザーのパスワードである場合のみ、if
文がtrue
になることがわかります。
ユーザー | パスワード | a && b |
---|---|---|
存在しない | 何でもよい | nil && [anything] == false |
有効なユーザー | 誤ったパスワード | true && false == false |
有効なユーザー | 正しいパスワード | true && true == true |
8.1.5フラッシュメッセージを表示する
7.3.2では、Userモデルのエラーメッセージを使用してユーザー登録のエラーメッセージを表示したことを思い出してください。これらのエラーメッセージは、特定のActive Recordオブジェクトに関連付けられていますが、セッションはActive Recordのモデルではないためにこの方法はここでは使用できません。代わりに、サインインに失敗したときにフラッシュメッセージを表示することにします。最初のコードをリスト8.10に示します (ここには少しだけ間違いがあります)。
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])
# ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
else
flash[:error] = 'Invalid email/password combination' # 誤りあり!
render 'new'
end
end
def destroy
end
end
フラッシュメッセージはWebサイトのレイアウトに表示される (リスト7.27) ので、flash[:error]
のメッセージは自動的に表示されます。Bootstrap CSSのおかげで適切なスタイルも与えられます (図8.6)。
残念ながら、本文およびリスト8.10のコメントで述べたように、このコードには誤りがあります。ページの表示には問題がないようですが、どこがおかしいのでしょうか。実は、あるリクエストに対するフラッシュメッセージの内容がすぐに消えずに残ってしまうという問題があります。リスト7.28で使用したリダイレクトのときとは異なり、render
で表示したテンプレートを再描画してもリクエストと見なされません。このため、フラッシュメッセージはひとつのリクエストに対して期待よりも長い間消えずに表示され続けてしまいます。たとえば、無効な情報を送信するとサインインページにフラッシュメッセージが設定されて表示されます (図8.6)。ここでHomeページなどの別のページへのリンクをクリックすると、これはフォームが送信されてから最初のリクエストであるため、フラッシュメッセージが移動先のページでも表示されたままになってしまいます (図8.7)。
フラッシュメッセージの残留問題はこのアプリケーションのバグです。この問題を修正する前に、この問題をキャッチするテストを書くのが正しいやり方です。特に、現在のサインイン失敗テストではこの問題がキャッチされずにパスしてしまいます。
$ bundle exec rspec spec/requests/authentication_pages_spec.rb \
> -e "signin with invalid information"
当然ながら、既知のバグが未修正の状態であれば、このテストはパスするべきではありません。この問題をキャッチする、失敗するテストを追加しましょう。幸い、結合テストはフラッシュメッセージ残留などの多くの問題解決において大活躍します。期待される動作は以下のテストで正確に表現されています。
describe "after visiting another page" do
before { click_link "Home" }
it { should_not have_selector('div.alert.alert-error') }
end
このテストは、無効なサインインデータを送信し、次にWebサイトのレイアウトにあるHomeリンクを開き、フラッシュメッセージが表示されていないことを確認します。フラッシュテストの部分を更新したテストコードをリスト8.11に示します。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "signin" do
before { visit signin_path }
describe "with invalid information" do
before { click_button "Sign in" }
it { should have_title('Sign in') }
it { should have_selector('div.alert.alert-error', text: 'Invalid') }
describe "after visiting another page" do
before { click_link "Home" }
it { should_not have_selector('div.alert.alert-error') }
end
end
.
.
.
end
end
再度テストを実行すると、期待どおり失敗します。
$ bundle exec rspec spec/requests/authentication_pages_spec.rb \
> -e "signin with invalid information"
失敗するテストがパスするようになるために、flash
の代わりにflash.now
を使用します。これもフラッシュメッセージをページに表示するために設計されたメソッドですが、flash
の場合とは異なり、他のリクエストが発生したらすぐにメッセージを消します。正しいアプリケーションコードをリスト8.12に示します。
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])
# ユーザーをサインインさせ、ユーザーページ (show) にリダイレクトする。
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
これで、ユーザー情報が無効な場合のテストスイートが緑色 (成功) になりました。
$ bundle exec rspec spec/requests/authentication_pages_spec.rb \
> -e "with invalid information"
8.2サインイン成功
サインイン失敗をテストできるようにしたので、次は実際にユーザーをサインインさせましょう。この節で必要なRubyのプログラミングは、本書の中ではこれまでで最も難易度が高くなっています。どうか最後まであきらめずにがんばってください。ここからの力仕事に備えておきましょう。幸い、はじめの一歩は簡単です。Sessionsコントローラのcreate
アクションはすぐできあがります。残念ながら、少々ズルもしています。
サインインの実装 (リスト8.12) はシンプルです。サインインに成功すると、sign_in
関数を使用して実際にユーザーをサインインさせ、プロファイルページにリダイレクトします (リスト8.13)。なぜこれがズルなのかというと、何とsign_in
はこの時点では存在していないのです。この節の残りは、この関数を完成させることに費やされます。
create
アクションが完成したところ (まだ動きません)。app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_to user
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
8.2.1[このアカウント設定を保存する]
これよりサインインモデルの実装を開始します。具体的には、サインイン状態を「永続化」し、ユーザーが明示的にサインアウトしたときにのみセッションを終了します。サインイン関数そのものは伝統的なMVC (model-view-controller)) に帰着します。特に、いくつかのサインイン関数についてはコントローラとビューのどちらからも使用できるようにする必要があります。4.2.5を思い出してみてください。Rubyには、いくつもの関数をパッケージ化し、さまざまな場所でインクルードできるようにするモジュールという機能が提供されています。これを認証機能の実装に使用しましょう。認証のためにまったく新しいモジュールを作ることも可能ですが、Sessionsコントローラには既にSessionsHelper
というモジュールが備わっています。さらに、ヘルパーはRailsのビューに自動的にインクルードされるので、Applicationコントローラにこのモジュールをインクルードするだけで、Sessionsヘルパー機能をコントローラ内で使用できるようになります (リスト8.14)。
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
デフォルトでは、すべてのヘルパーはビューで使用できますが、コントローラでは使用可能になっていません。Sessionsヘルパーはビューとコントローラの両方でメソッドが必要となるので、コントローラでは上のように明示的にインクルードする必要があります。
HTTPはステートレスなプロトコルであり、そのままでは状態が保存されないので、Webアプリケーションのサインインは、ページからページの移動を追跡するという方法で実装する必要があります。ユーザーのサインイン状態を保持するひとつの方法は、伝統的なRailsセッション (特殊なsession
関数を使用) を使って、ユーザーIDに等しい「記憶トークン (remember token)」を保持することです。
session[:remember_token] = user.id
このsession
オブジェクトは、ユーザーidをcookiesに保持することで、ページ移動後にもユーザーidを参照できるようにしています。cookiesはブラウザが閉じられると無効になります。アプリケーションは、ページごとに以下の呼び出しを行います。
User.find(session[:remember_token])
これだけで、ユーザーを取り出すことができます。Railsでは、セッションがセキュアになるように扱っていますので、悪意のあるユーザーがidをなりすまそうとしても、Railsはセッションごとに生成される特別のセッションidによって不一致を検出します。
私たちのアプリケーション設計では、永続的なセッションを採用します。つまり、ブラウザを閉じた後にもサインイン状態を保持するということであり、サインインしたユーザーに対して何らかの恒久的な識別子を使用する必要があります。これを実現するために、ユーザーごとに一意かつ安全な記憶トークンを生成し、ブラウザを閉じても無効にならない恒久的なcookiesとして登録します。
記憶トークンはユーザーに関連付けられる必要があります。また、今後使用するときのために保持しておく必要もあります。そこで、図8.8に示すUserモデルの属性に記憶トークンの保存場所を追加しましょう。
最初に、Userモデルのspecに若干の追加を行います (リスト8.15)。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
it { should respond_to(:password_confirmation) }
it { should respond_to(:remember_token) }
it { should respond_to(:authenticate) }
.
.
.
end
コマンドラインで以下のように記憶トークンを生成することで、上のテストがパスするようになります。
$ rails generate migration add_remember_token_to_users
次に、上で生成されたマイグレーションにリスト8.16のコードを記入します。ユーザーを記憶トークンで検索できるように、インデックス (コラム 6.2) をremember_token
カラムに追加していることに注目してください。
remember_token
をusers
テーブルに追加したマイグレーション。db/migrate/[ts]_add_remember_token_to_users.rb
class AddRememberTokenToUsers < ActiveRecord::Migration
def change
add_column :users, :remember_token, :string
add_index :users, :remember_token
end
end
次に、いつものように開発データベースとテストデータベースを更新します。
$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
この時点で、Userモデルのspecはパスするはずです。
$ bundle exec rspec spec/models/user_spec.rb
ここで、記憶トークンに何を使用するかを決める必要があります。有力な候補としてさまざまなものが考えられますが、基本的には一意性を確保できる、長くてランダムな文字列でさえあればどんなものでも良いでしょう。Ruby標準ライブラリのSecureRandom
モジュールにあるurlsafe_base64
メソッドならこの用途にぴったりです3。このメソッドは、A–Z、a–z、0–9、“-”、“_”のいずれかの文字 (64種類) からなる長さ16のランダムな文字列を返します (そのためbase64と呼ばれています)。生成された2つの記憶トークンが偶然一致 (衝突) する確率は、64のマイナス16乗、2のマイナス96乗 (10のマイナス29乗) にほぼ等しく、トークンが衝突することはまずありません。
ここでのねらいは、ブラウザにこのbase64トークンを保存しておき、データベースにはトークンを暗号化したものを保存することです。そして、cookiesからこのトークンを読みだして暗号化し、データベース上にある暗号化された記憶トークンと一致するものがあるかどうかを検索することにより、ユーザーを自動的にサインインさせることができます。暗号化したトークンだけをデータベースに保存する理由は、万が一データベースが不正アクセスを受けるようなことがあっても、攻撃者が記憶トークンを使用してサインインできないようにするためです。記憶トークンをさらにセキュアにするためには、新しいセッションを作成するたびにトークンを更新するのがよい方法です。この方法なら、攻撃者が盗んだcookiesを使用して本物のユーザーになりすましてサインインしようとする (セッションハイジャック) ことがあっても、ユーザーが次回サインインするときにはトークンが期限切れになります。(セッションハイジャックは、セキュリティ上の注意を呼びかけるためにこれを実演するFiresheepアプリケーションによって広く知られるようになりました。Firesheepを使用すると、公共Wi-Fiネットワーク経由で接続したときに多くの有名Webサイトの記憶トークンが丸見えになっていることがわかります。この問題を解決するには、7.4.4で説明したようにWebサイト全体をSSLで暗号化することです。)
サンプルアプリケーションでは、新規作成されたユーザーをその場でサインインさせ、その副作用として記憶トークンがついでに作成されますが、この動作に依存すべきではありません。安全を確保するために、すべてのユーザーが必ず最初から有効な記憶トークンを持つようにする必要があります。これを確実に行うには、コールバックを使用して記憶トークンを生成します。このテクニックは、6.2.5でemailの一意性の文脈で紹介しました。あのときはbefore_save
というコールバックを使用しましたが、ここではそれによく似たbefore_create
というコールバックを使用して、ユーザー新規作成時に記憶トークンを設定することにします4。
記憶トークンをテストするために、最初にテストユーザーを保存し (これまでは作成されても保存はされていませんでした)、次にユーザーのremember_token
属性が空欄でないことを確認します。これにより、必要が生じたときにランダム文字列を変更するのに十分な柔軟性が得られます。このテストをリスト8.17に示します。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
describe "remember token" do
before { @user.save }
its(:remember_token) { should_not be_blank }
end
end
上のリスト8.17で導入したits
メソッドは、it
と似ていますが、itが指すテストのsubject (ここでは@user) そのものではなく、引数として与えられたその属性 (この場合は:remember_token) に対してテストを行うときに使用します。
its(:remember_token) { should_not be_blank }
つまり、上のコードは以下と等価です。
it { expect(@user.remember_token).not_to be_blank }
このアプリケーションコードでは、新しい要素をいくつか導入します。最初に、記憶トークンを作成するために以下のようにコールバックメソッドを追加します。
before_create :create_remember_token
上のコードはメソッド参照と呼ばれるもので、こうすることでRailsはcreate_remember_token
というメソッドを探し、ユーザーを保存する前に実行するようになります (リスト6.20では、before_save
に明示的にブロックを渡していましたが、メソッド参照の方が一般にお勧めできます) 。次に、このメソッド自身はUserモデルの内部でしか使用しないので、外部のユーザーがアクセスできるようにする必要はありません。7.3.2でも触れたように、メソッド定義に以下のようにprivate
キーワードを与えるのがRuby流です。
private
def create_remember_token
# トークンを作成する。
end
private
キーワード以降で定義されたメソッドはすべて隠蔽されます。
$ rails console
>> User.first.create_remember_token
このため、上をコンソールで実行するとNoMethodError
例外が発生します。
最後に、create_remember_token
メソッドは、ユーザーの属性のひとつに「要素代入 (assignment)」する必要があります。この文脈では、remember_token
の前にself
キーワードを追加する必要があります。
def User.new_remember_token
SecureRandom.urlsafe_base64
end
def User.encrypt(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = User.encrypt(User.new_remember_token)
end
上のコードにself
というキーワードがないと、要素代入によってremember_token
という名前のローカル変数が作成されてしまうので、注意が必要です (ここで本来必要なのはローカル変数ではなく、インスタンス変数です)。この動作は、Rubyにおけるオブジェクト内部への要素代入の仕様によるものです。self
キーワードを与えることで、要素代入は正しくそのユーザーのremember_token
属性を設定するようになり、その結果ユーザーが保存されるときに他の属性と一緒にこの属性もデータベースに保存されます (リスト6.20の他のbefore_save
コールバックでも、email
ではなくself.email
と記述していた理由が、これでおわかりいただけたと思います)。
SHA1を使用して記憶トークンを暗号化すると、6.3.1でユーザーのパスワード暗号化に使用していたBcryptアルゴリズムよりも動作がはるかに高速である点に注目してください。8.2.2でも説明しますが、トークンの暗号化はユーザーがページを開くたびに行うので、暗号化アルゴリズムが高速であることは重要です。SHA1はBcryptと比べるとそれほどセキュアではありませんが、元となるトークンが既に16桁のランダムな文字列なので、これで十分です。このようなランダムな文字列をさらにSHA1でhexdigestしたものは、本質的にクラック不可能です (to_sメソッドを呼び出しているのは、nil
トークンを扱えるようにするためです。ブラウザでnilトークンが発生することはあってはなりませんが、テスト中に発生することはありえるためです)。
encrypt
メソッドとnew_remember_token
メソッドは、ユーザーのインスタンスに対して動作する必要がないので、インスタンスではなくUser
クラスに属します (クラスメソッド)。また、これらのメソッドはprivate
行より上にあるのでpublicになっていますが、これは8.2.3でUserモデルの外で使用する必要があるためです。
以上の実装をまとめたUserモデルをリスト8.18に示します。
before_create
コールバックを使用して remember_token
属性を作成する。app/models/user.rb
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
before_create :create_remember_token
.
.
.
def User.new_remember_token
SecureRandom.urlsafe_base64
end
def User.encrypt(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = User.encrypt(User.new_remember_token)
end
end
ちなみに、private
キーワード以降のコードは、強調のためcreate_remember_token
のインデントを1段深くしてあります (経験上、こうしておくことをお勧めします)。
SecureRandom.urlsafe_base64
は決して空欄にはならなくなったので、Userモデルのテストはパスするはずです。
$ bundle exec rspec spec/models/user_spec.rb
8.2.2正しいsign_inメソッド
いよいよ、最初のサインイン要素であるsign_in
関数自身の実装に取りかかりましょう。既に述べたように、これから作る認証メソッドは、記憶トークンをブラウザのcookiesに保存し、ユーザーが別のページに移動する (8.2.3で実装予定) ときにそのトークンを使用してデータベース内のユーザーのレコードを見つけます。このコードをリスト8.19に示します。なお、このコードにはcurrent_user
というメソッドがありますが、これは8.2.3で実装します。
sign_in
関数。app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
end
ここでは以下の手順で進めます。1) トークンを新規作成する。2) 暗号化されていないトークンをブラウザのcookiesに保存する。3) 暗号化したトークンをデータベースに保存する。4) 与えられたユーザーを現在のユーザーに設定する (8.2.3)。上のコードで、update_attribute
を使用してトークンを保存している点に注目してください。6.1.5でも簡単に説明しましたが、このメソッドを使用すれば検証をすり抜けて単一の属性を更新することができます。ページの移動ではユーザーのパスワードやパスワード確認は与えられないので、このメソッドを使用する必要があります。
リスト8.19ではRailsが提供するcookies
ユーティリティも使用しています。これを使用することで、ブラウザのcookiesをハッシュのように扱うことができます。cookiesの各要素は、それ自体が2つの要素 (value
とオプションのexpires
日時) のハッシュになっています。たとえば以下のように、cookiesに20年後に期限切れになる記憶トークンに等しい値を保存することで、ユーザーのサインインを実装できます。
cookies[:remember_token] = { value: remember_token,
expires: 20.years.from_now.utc }
(上のコードではRailsの便利なtimeヘルパーを使用しています。コラム 8.1を参照してください。)
Rubyは組み込みクラスを含むあらゆるクラスにメソッドを追加できることを4.4.2で学びました。あのときは、palindrome?メソッドをStringクラスに追加しました (ついでに"deified"も回文になっていることを発見しました)。また、Railsが実はblank?メソッドをObjectクラスに追加していることも判明しました (これにより、"".blank?、" ".blank?、nil.blank?はいずれもtrueになります)。リスト8.19のコードでは、cookiesが20年後に期限切れになる (20.years.from_now) ように指定していますが、これはRailsのtimeヘルパーを使用した格好の例題になります。timeヘルパーはRailsによって、数値関連の基底クラスであるFixnumクラスに追加されます。
$ rails console >> 1.year.from_now => Sun, 13 Mar 2011 03:38:55 UTC +00:00 >> 10.weeks.ago => Sat, 02 Jan 2010 03:39:14 UTC +00:00
Railsは以下のようなヘルパーも追加しています。
>> 1.kilobyte => 1024 >> 5.megabytes => 5242880
上のヘルパーは、ファイルのアップロードに5.megabytesなどの制限を与えるのに便利です。
メソッドを組み込みクラスに追加できる柔軟性の高さのおかげで、純粋なRubyを極めて自然に拡張することができます (もちろん注意して使う必要はありますが)。実際、Railsのエレガントな仕様の多くは、背後にあるRubyの高い拡張性によって実現されているのです。
上のように20年で期限切れになるcookies設定はよく使われるようになり、今ではRailsにも特殊なpermanent
という専用のメソッドが追加されたほどです。このメソッドを使用すると、コードは以下のようにシンプルになります。
cookies.permanent[:remember_token] = remember_token
上のコードでは、permanent
メソッドによって自動的に期限が20.years.from_now
に設定されます。
cookiesを設定後、移動先のページで以下のようなコードを使用してユーザーを取り出すことができます。
User.find_by(remember_token: remember_token)
もちろん、このcookies
は本物のハッシュではなく、実際にはcookies
に割り当てを行ったときにブラウザ上のテキストの断片を保存しているだけです。そうしたアプリケーションの実装の詳細を気にしなくてよい点に、Railsの美しさの一端が垣間見えます。
8.2.3現在のユーザー
後で使うために記憶トークンをcookiesに保存する方法の説明が終わりましたので、今度は移動先のページでユーザーを取り出す方法について学びましょう。sign_in
関数のコードをもういちどよく見てみてください。
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
end
現時点では、上のコードのうち、以下のコードだけが動作していません。
self.current_user = user
この行の目的は、コントローラからもビューからもアクセスできるcurrent_user
を作成することです。これにより、以下のような構文を使用できるようになります。
<%= current_user.name %>
以下も使用できるようになります。
redirect_to current_user
リスト8.18のときに説明したのと同じ理由で、ここにもself
が必要です。self
がないと、Rubyは単にcurrent_user
というローカル変数を作成してしまいます。
current_user
のコードを書く上で、以下の行については注意が必要です。
self.current_user = user
上は「要素代入 (assignment)」であることに注意してください。このcurrent_user=は別途定義が必要です。リスト8.20に示したように、Rubyにはこのような要素代入関数を定義する特殊な文法があります。
current_user
への要素代入を定義する。app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
.
.
.
end
def current_user=(user)
@current_user = user
end
end
上のコードで使用されている特殊な文法は混乱しやすいので注意してください。普通のプログラミング言語では、定義するメソッドの名前に等号を使用することはできませんが、Rubyではメソッド名末尾の等号には特殊な意味があり、上のコードはcurrent_user
への要素代入を扱うように設計されたcurrent_user=
というメソッドを単に定義します。つまり、以下のコードは
self.current_user = ...
自動的に以下のコードに置き換えられます。
current_user=(...)
そして最終的にcurrent_user=
というメソッドが呼び出されます。その引数は要素代入の右側にひとつ置かれます (ここではサインインするユーザー)。この一行メソッドは、単に@current_user
インスタンス変数を設定し、後に使用するためにユーザーを効率よく保存します。
通常のRubyでは、もうひとつのメソッドとしてcurrent_user
を定義することができます。リスト8.21に示したように、このメソッドは@current_user
の値を返すようになっています。
current_user
の定義 module SessionsHelper
def sign_in(user)
.
.
.
end
def current_user=(user)
@current_user = user
end
def current_user
@current_user # 役に立たない。この行は使用しないこと。
end
end
もしかすると、これを使用して4.4.5のattr_accessor
を効率よく置き換えることができるのではないでしょうか5。それをしないのは、このコードを使用すると今までの苦労が台無しになってしまうからです。リスト8.21のコードを使用すると、ユーザーが別のページに移動した瞬間にユーザーのサインイン情報がきれいさっぱり消滅してしまいます。セッションは終了し、ユーザーは自動的にサインアウトしてしまいます。この問題を回避するには、リスト8.22に示したように、リスト8.19のコードで作成した記憶トークンに対応するユーザーを検索します。データベース上の記憶トークンは暗号化されているので、cookiesから取り出した記憶トークンは、データベース上の記憶トークンを検索する前に暗号化する必要がある点に注意してください。これを行うには、リスト8.18で定義したUser.encrypt
を使用します。
remember_token
を使用して現在のユーザーを検索する。app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
def current_user=(user)
@current_user = user
end
def current_user
remember_token = User.encrypt(cookies[:remember_token])
@current_user ||= User.find_by(remember_token: remember_token)
end
end
リスト8.22では、Rubyでは定番の (しかし最初はまごつきやすい) ||=
(“or equals”) 代入演算子を使用しています (コラム 8.2)。この代入演算子は、@current_user
が未定義の場合にのみ、@current_user
インスタンス変数に記憶トークンを設定します6。
@current_user ||= User.find_by(remember_token: remember_token)
あるユーザーに対してcurrent_user
が初めて呼び出される場合はfind_by
メソッドを呼び出しますが、以後の呼び出しではデータベースにアクセスせずに@current_user
を返します7。このコードは、ひとつのユーザーリクエストに対してcurrent_user
が何度も使用される場合にのみ有用です。いずれの場合も、ユーザーがWebサイトにアクセスするとfind_by
は最低1回は呼び出されます。
||=という記法は非常にRuby的であり、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 -= 7 => -1
いずれの場合も、●という演算子があるときの「x = x ● y」と「x ●= y」の動作は同じです。
Rubyでは、「変数の値がnilなら変数に代入するが、nilでなければ代入しない (変数の値を変えない)」という操作が非常によく使われます。4.2.3で説明したor演算子||を使用すれば、以下のように書くことができます。
>> @user => nil >> @user = @user || "the user" => "the user" >> @user = @user || "another user" => "the user"
nilの論理値は偽 (false) です。初めて代入するときは「nil || "the user"」となり、これは「"the user"」と評価されます。同様に、2度目の代入では「"the user" || "another user"」となり、これも「"the user"」と評価されます。あらゆる文字列の論理値は真 (true) なので、 ||を使用する一連の式は、いずれも1番目の式が評価された時点で終了します (なお、||式を左から右に評価し、最初の左の値がtrueであればその時点で終了するというやり方を短絡評価 (short-circuit evaluation)と呼びます)。
上述した多くの演算子をコンソールセッション上で実行して比較してみると、@user = @user || valueは「x = x O y」のパターンに該当し、Oが||に置き換わっただけのものであることがわかります。ということは、この演算は以下と同じであることが推測できます。
>> @user ||= "the user" => "the user"
お試しあれ。
8.2.4レイアウトリンクを変更する
サインイン/サインアウトが動作するようになり、実用的なアプリケーションらしくなってきました。今度は、サインインの状態に合わせてレイアウト上のリンクが変わるようにしましょう。特に、図8.3のモックアップで示したように、ユーザーがサインインしているときとサインアウトしているときでリンクを変更し、すべてのユーザーを表示するリンクとユーザー設定用のリンクも追加し (第9章で完成する予定)、さらに現在のユーザーのプロファイルのリンクも追加します。これらを実装することで、リスト8.6のテストがパスするようになります。つまり、本章開始以来初めてテストスイート全体が緑色 (成功) になるということです。
サイトレイアウトのリンクを変更するには、埋め込みRubyの内側でif-else分岐構造を使用します。
<% if signed_in? %>
# サインインしているユーザー用のリンク
<% else %>
#サインインしていないユーザー用のリンク
<% end %>
この種のコードでは、signed_in?
論理値が必要になりますので、これから定義しましょう。
ユーザーがサインインしている状態は、セッションに現在のユーザーがいる (current_user
がnil
でない) ことで表されます。これを表現するには否定の演算子が必要なので、!
("bang" と読みます) を使用します。リスト8.23に示すとおり、現在の文脈ではcurrent_user
がnil
でない場合にユーザーがサインインしています。
signed_in?
ヘルパーメソッド。app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
def signed_in?
!current_user.nil?
end
.
.
.
end
signed_in?
メソッドを手作りしてあるので、レイアウトのリンクはすぐに作成できます。なお、新しく作るリンクは4つですが、そのうち以下の2つのリンクは未実装です (第9章で完成の予定)。
<%= link_to "Users", '#' %>
<%= link_to "Settings", '#' %>
サインアウトのリンクには、リスト8.2で定義したサインアウトのパスを使用します。
<%= link_to "Sign out", signout_path, method: "delete" %>
(上のコードでは、サインアウトのリンクがハッシュ引数を渡していることに注目してください。このハッシュ引数は、HTTPのDELETEリクエストを使用して送信される必要があることを示しています8)。
<%= link_to "Profile", current_user %>
なお、上のコードは以下のように書くこともできます。
<%= link_to "Profile", user_path(current_user) %>
Railsでは、このユーザーへの直接リンクが許されるので、この場合current_user
はuser_path(current_user)
に自動的に変換されます。
新しいリンクをレイアウトに追加するときに、Bootstrapの機能を使用してドロップダウンメニューを実現しましょう。詳細については「Bootstrapコンポーネント (英語)」を参照してください。完全なコードをリスト8.24に示します。このコードでは、Bootstrapのドロップダウンメニューに関連するCSSのidとクラスが与えられていることに注目してください。
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="navbar-inner">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav pull-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if signed_in? %>
<li><%= link_to "Users", '#' %></li>
<li id="fat-menu" 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 "Sign out", signout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Sign in", signin_path %></li>
<% end %>
</ul>
</nav>
</div>
</div>
</header>
ドロップダウンメニューを実現するには、BootstrapのJavaScriptライブラリが必要です。このライブラリは、RailsのAsset Pipelineを使用してインクルードすることができます。そのためには、リスト8.25に示すようにアプリケーションのJavaScriptファイルを編集します。
application.js
に追加する。app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .
上のコードは、Sprocketsライブラリを使用してBootstrap JavaScriptをインクルードします。5.1.2でbootstrap-sass gemを導入してあるので、これでBootstrapが利用できるようになります。
リスト8.24のコードを使用することで、すべてのテストにパスするはずです。
$ bundle exec rspec spec/
以上の変更を行ったことで、図8.9に示したように、リスト8.24で定義したドロップダウンメニューがサインインしたユーザーに表示されるようになりました。
この時点で、ブラウザで実際にサインインできることを確認し、続いてブラウザを閉じてから再度サンプルアプリケーションを表示するとサインインしたままになっていることを確認してください。その気になれば、ブラウザのcookiesをブラウザで調べて結果を直接確認することもできます (図8.10)。
8.2.5ユーザー登録と同時にサインインする
認証機能の基本的な部分はできましたが、ユーザーが登録を行った後、そのユーザーがデフォルトではサインインしておらず、このままではユーザーが混乱する可能性があります。そこで、サインアウトの機能を実装する前にその部分をもう少し作り込みましょう。最初に、認証のテストを追加します (リスト8.26)。ここには、7.6の演習にあるリスト7.32でも使った “after saving the user” describe
ブロックが含まれています。このリストに対応する演習を行なっていなかった方は、この時点で追加する必要があります。
spec/requests/user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
.
.
.
describe "with valid information" do
.
.
.
describe "after saving the user" do
before { click_button submit }
let(:user) { User.find_by(email: 'user@example.com') }
it { should have_link('Sign out') }
it { should have_title(user.name) }
it { should have_selector('div.alert.alert-success', text: 'Welcome') }
end
end
end
end
上のコードでは、ユーザー登録後にサインインしていることを確認するために、サインアウト用のリンクが表示されているかどうかをテストしています。
8.2でsign_in
メソッドを作成してあるので、ユーザーをデータベースに保存した直後にsign_in @user
を追加するだけで、ユーザーが実際にサインインしてテストにパスするようになります (リスト8.27)。
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
sign_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
.
.
.
end
8.2.6サインアウトする
8.1で説明したとおり、このアプリケーションで使用する認証モデルでは、ユーザーは明示的にサインアウトするまでサインインしたままになります。この節では、そのサインアウト機能を追加します。
Sessionsコントローラのアクションは、これまでもRESTful慣例に従ってサインインページにはnew
を使用し、サインインの完了にはcreate
を使用しました。今回も同様に慣例に従い、セッションの削除 (サインアウト) にはdestroy
を使用します。このことをテストするには、[Sign out] リンクをクリックして、そこにサインイン用のリンクが再び現れるかどうかを確認します (リスト8.28)。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
.
.
.
describe "signin" do
.
.
.
describe "with valid information" do
.
.
.
describe "followed by signout" do
before { click_link "Sign out" }
it { should have_link('Sign in') }
end
end
end
end
サインインがsign_in
関数に依存しているのと同様に、サインアウトでもsign_out
関数を単に実行します (リスト8.29)。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def destroy
sign_out
redirect_to root_url
end
end
他の認証用機能と同様に、sign_out
もSessionsヘルパーモジュールの中に置きます。実装は極めてシンプルです。現在のユーザーをnil
にし、delete
メソッドを使用して、cookiesにあるセッションの記憶トークンを削除します (リスト8.30)。(destroy
アクション内ですぐにリダイレクトされるので、現在のユーザーをnil
にする必要は必ずしもありません。ただし、リダイレクトなしでsign_out
を使用したい状況が今後発生するかもしれないので、そのときに役立つでしょう。)
sign_out
メソッド。app/helpers/sessions_helper.rb
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
.
.
.
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
end
これでユーザー登録/サインイン/サインアウトがすべて揃いました。テストスイートはパスするはずです。
$ bundle exec rspec spec/
本書のテストスイートは認証システムをほぼカバーしていますが、すべてをカバーしているわけではありません。この点をご了承ください。たとえば、[このアカウント設定を保存する] の cookies (remember me) が有効になっているか、その後も保持されているかどうかのテストは含まれていません。このテストは作成可能ですが、著者の経験上、cookiesの値を直接調べる方法はRailsの実装に左右されやすい傾向があり、次のバージョンのRailsではまた変わるかもしれません。その場合、アプリケーションコードは正常に動作してもテストが正常に動作しなくなります。このような理由で、本書のテストコードでは、コアとなるアプリケーションコードをテストする際に高度な機能 (ユーザーがサインインできること、ページ移動後もサインインしていること、サインアウトできること) に重点を置き、重要性の低い機能は必ずしも含んでいません。
8.3Cucumberの紹介 (オプション)
サンプルアプリケーションの認証システムの基礎部分が無事完成したので、この機会にCucumberを使用したサインインのテスト方法をご紹介します。Cucumberは振舞駆動開発用のツールとして有名で、Rubyコミュニティに多くの愛用者がいます。この節の内容は必須ではありませんので、スキップしても問題ありません。
Cucumberを使用すると、アプリケーションの振る舞いをテキストベースの「ストーリー」で定義することができます。多くのRailsプログラマーは、Cucumberは顧客と共同作業するときに便利であることを知っています。Cucumberのストーリーは、専門知識のない人でも読むことができ、ストーリーを顧客と共有したり、場合によっては顧客がストーリーを作成することすらできるためです。もちろん、テスティングのフレームワークが純粋なRubyでないという点は残念でもあり、著者にとってはテキストベースのストーリーはいささか冗長な面もあると思われます。それでもCucumberはRubyのテスティングツールキットとして確固たる地位を占めており、著者としては低レベルの実装を気にすることなく高度な振る舞いを記述できる点が特に気に入っています。
本書ではRSpecとCapybaraをテスティングのメインに据えているので、この節のCucumberに関する説明は完全ではなく、表面的で物足りないことでしょう。この節の目的はCucumberのおいしさ (間違いなくシャキシャキして汁気たっぷりです) を知ってもらうための、いわば試食です。もし気に入っていただけたら、テスティングツールに関する完結した書籍がいくつもあります。(おすすめは「The RSpec Book 」(David Chelimsky著)、「 Rails 3 in Action 」(Ryan Bigg、Yehuda Katz著)、 「The Cucumber Book」(Matt Wynne、Aslak Hellesøy著) です)。(訳注: 日本語で読めるCucumberの書籍としては「はじめる!Cucumber」(諸橋 恭介著) があります。)
8.3.1インストールと設定
Cucumberをインストールするには、最初にcucumber-rails gemとdatabase_cleanerというユーティリティgemをGemfile
の:test
グループに追加します (リスト8.31)。
Gemfile
に追加する。 .
.
.
group :test do
.
.
.
gem 'cucumber-rails', '1.4.0', :require => false
gem 'database_cleaner', github: 'bmabey/database_cleaner'
end
.
.
.
次に、いつものように以下を実行します。
$ bundle install
アプリケーションでCucumberを使用するための設定を行うために、次は必要なサポート用ファイルとディレクトリを生成します。
$ rails generate cucumber:install
上のコマンドにより、Cucumber関連のファイルが置かれるfeatures/
ディレクトリが作成されます。
8.3.2フィーチャーとステップ
Cucumberでは、Gherkin (キュウリ属の植物: ガーキン) と呼ばれるテキストベースの言語を使用して、アプリケーションに期待される振る舞いを記述します。Gherkinで書かれたテストは、ちゃんと書かれたRSpecの例と同じぐらい読みやすくできています。どちらもテキストベースであり、自然な英語に近くRubyコードよりも読みやすいためです。
ここでは、リスト8.5とリスト8.6のサインインのテスト例のサブセットをCucumberで実装してみましょう。最初に、features/
ディレクトリ内にsigning_in.feature
というファイルを作成します。
Cucumberのフィーチャーファイルは、以下のようにその機能の簡単な説明から始まります。
Feature: Signing in
次に個別のシナリオを追加します。たとえば、サインイン失敗をテストするには、以下のようなシナリオを作成します。
Scenario: Unsuccessful signin
Given a user visits the signin page
When he submits invalid signin information
Then he should see an error message
同様に、サインイン成功をテストするために以下を使用できます。
Scenario: Successful signin
Given a user visits the signin page
And the user has an account
When the user submits valid signin information
Then he should see his profile page
And he should see a signout link
これらをまとめたものをリスト8.32に示します。
features/signing_in.feature
Feature: Signing in
Scenario: Unsuccessful signin
Given a user visits the signin page
When he submits invalid signin information
Then he should see an error message
Scenario: Successful signin
Given a user visits the signin page
And the user has an account
When the user submits valid signin information
Then he should see his profile page
And he should see a signout link
これらのフィーチャーを実行するには、cucumber
実行ファイルを以下のように実行します。
$ bundle exec cucumber features/
上のCucumberのコマンドを、下のRSpecのコマンドと比較してみてください。
$ bundle exec rspec spec/
ここからおわかりだと思いますが、CucumberはRSpecと同様Rakeタスクから呼び出すこともできます。
$ bundle exec rake cucumber
(rake cucumber:ok
と書くこともできます)。
まだテキストを書いただけなので、当然ながらこのままではCucumberのシナリオはテストにパスしません。テストスイートが緑色 (成功) になるためには、テキストファイルをRubyコードにマップするステップファイルを作成します。ステップファイルはfeatures/step_definitions
ディレクトリに置きます。ファイル名はここではauthentication_steps.rb
とします。
Feature
行とScenario
行は説明のためのものですが、それ以外の行はRubyに対応付けられる必要があります。たとえば、フィーチャーファイルにある以下のコードは、
Given a user visits the signin page
以下のステップ定義によって扱われます。
Given /^a user visits the signin page$/ do
visit signin_path
end
フィーチャーファイルの中ではGiven
は単なる文字列ですが、ステップファイルの中ではGiven
はメソッドであり、正規表現とブロックを引数に取ります。この正規表現はシナリオの中の行とマッチし、次のブロックの内容は、そのステップを実装するために必要なRubyのコードです。この場合、“a user visits the signin page”という記述は以下のコードによって実装されます。
visit signin_path
上のコードはどこかで見たことがあると思ったら、それもそのはず、Capybaraです。CapybaraはデフォルトでCucumberのステップファイルに含まれます。次の2行もわかりやすいと思います。
When he submits invalid signin information
Then he should see an error message
上のフィーチャーファイルのコードは、ステップファイルでは以下のように扱われます。
When /^he submits invalid signin information$/ do
click_button "Sign in"
end
Then /^he should see an error message$/ do
expect(page).to have_selector('div.alert.alert-error')
end
最初のステップでもCapybaraが使用されていますが、その次のステップではCapybaraのpage
オブジェクトとRSpecが併用されています。見てのとおり、RSpecとCapybaraで行えるテストは、すべてCucumberでも行えます。
残りのステップも同様に進められます。最終的なステップ定義ファイルをリスト8.33に示します。ステップを追加したら、以下を実行します。
$ bundle exec cucumber features/
テストにパスするまでこれを繰り返します。
features/step_definitions/authentication_steps.rb
Given /^a user visits the signin page$/ do
visit signin_path
end
When /^he submits invalid signin information$/ do
click_button "Sign in"
end
Then /^he should see an error message$/ do
expect(page).to have_selector('div.alert.alert-error')
end
Given /^the user has an account$/ do
@user = User.create(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
When /^the user submits valid signin information$/ do
fill_in "Email", with: @user.email
fill_in "Password", with: @user.password
click_button "Sign in"
end
Then /^he should see his profile page$/ do
expect(page).to have_title(@user.name)
end
Then /^he should see a signout link$/ do
expect(page).to have_link('Sign out', href: signout_path)
end
リスト8.33のコードにより、以下のCucumberのテストはパスするはずです。
$ bundle exec cucumber features/
8.3.3対比: RSpecのカスタムマッチャー
簡単なCucumberのシナリオをいくつか紹介したので、それらと同等のRSpecの例と比較してみましょう。まず、リスト8.32のCucumberフィーチャーと、それに対応するリスト8.33のステップ定義をもう一度見てみましょう。次に、以下のRSpecリクエストspec (結合テスト) を見てみましょう。
describe "Authentication" do
subject { page }
describe "signin" do
before { visit signin_path }
describe "with invalid information" do
before { click_button "Sign in" }
it { should have_title('Sign in') }
it { should have_selector('div.alert.alert-error', text: 'Invalid') }
end
describe "with valid information" do
let(:user) { FactoryGirl.create(:user) }
before do
fill_in "Email", with: user.email.upcase
fill_in "Password", with: user.password
click_button "Sign in"
end
it { should have_title(user.name) }
it { should have_link('Profile', href: user_path(user)) }
it { should have_link('Sign out', href: signout_path) }
it { should_not have_link('Sign in', href: signin_path) }
end
end
end
Cucumberと結合テストでそれぞれどのように実装されているかがおわかりいただけると思います。Cucumberのフィーチャーは非常に読みやすいのですが、その代わり実装されているコードから完全に切り離されていて、両刃の剣です。著者にとって、Cucumberは読みやすいが書くのは面倒、結合テストはプログラマーにとってはそれほど読みやすくない代わりに書くのははるかに楽、という印象です。
Cucumberではフィーチャーとステップが分離されていることにより、抽象度の高い記述が可能であるという効果があります。以下のフィーチャーは、エラーメッセージが表示されるはずであるということを記述しています。
Then he should see an error message
そして以下のステップファイルでは、このテストを実装しています。
Then /^he should see an error message$/ do
expect(page).to have_selector('div.alert.alert-error', text: 'Invalid')
end
これが特に便利なのは、実装に依存するのが2番目の要素であるステップファイルだけである点です。そのため、たとえばエラーメッセージを表示するためのCSSを変更しても、フィーチャーファイルは変更不要です。
同様に、以下のようなコードを何度も書くのは多くの人にとって苦痛だと思います。
should have_selector('div.alert.alert-error', text: 'Invalid')
本当に行いたいのは、そのページでエラーメッセージが表示されることを示すことのはずです。しかも、上の記法は実装に密着しているので、実装が変更されたらそれらもすべて変更が必要になります。純粋なRSpecでは、カスタムマッチャーを使用してこの問題を解決することができます。カスタムマッチャーを使用すると、上のコードを以下のように簡潔に記述することができます。
should have_error_message('Invalid')
このマッチャーは、5.3.4でfull_title
テストヘルパーを置いたのと同じユーティリティファイル内で定義することができます。コード自体は以下のようになります。
RSpec::Matchers.define :have_error_message do |message|
match do |page|
expect(page).to have_selector('div.alert.alert-error', text: message)
end
end
同様に、よく使われる操作をヘルパーメソッドとして定義することもできます。
def valid_signin(user)
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Sign in"
end
作成したサポートコードをリスト8.34に示します (5.6の演習のリスト5.38とリスト5.39の結果を含みます)。 この方法は、Cucumberのステップ定義よりも柔軟であることがわかってきました。特に、マッチャーやshouldヘルパーがvalid_signin(user)
のように引数を自然に取ることができます。ステップ定義は正規表現マッチャーによって繰り返すことができますが、この手法は一般に厄介なものになりやすいという印象です。
spec/support/utilities.rb
include ApplicationHelper
def valid_signin(user)
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Sign in"
end
RSpec::Matchers.define :have_error_message do |message|
match do |page|
expect(page).to have_selector('div.alert.alert-error', text: message)
end
end
リスト8.34を使用することで、以下のように書くことができます。
it { should have_error_message('Invalid') }
以下も同様です。
describe "with valid information" do
let(:user) { FactoryGirl.create(:user) }
before { valid_signin(user) }
.
.
.
テストとサイトの実装を結びつける方法の例は他にも多数あります。現在のテストスイート全般にわたって、カスタムマッチャーとカスタムメソッドを作成してテストと実装の詳細を切り離す作業については、演習に回すことにします (8.5)。
8.4最後に
本章では多くの分野をカバーし、約束どおり、かつて未成熟だったアプリケーションを、ユーザー登録とログインをフル装備したWebサイトに変身させました。認証機能を完成させるためには、サインインの状態とユーザーidに基いてページのアクセスに制限を与える必要もあります。ページごとのアクセス制限機能の実装については、ユーザー情報を編集する機能と、ユーザーをシステムから削除できる権限を管理者に与える機能を実装するときに同時に行う予定です。このアクセス制限は、第9章の主な目標でもあります。
次の章に進む前に、変更をmasterブランチにマージしておきましょう。
$ git add .
$ git commit -m "Finish sign in"
$ git checkout master
$ git merge sign-in-out
次にリモートのGitHubリポジトリとHerokuの本番サーバーにプッシュします。
$ git push
$ git push heroku
$ heroku run rake db:migrate
8.5演習
form_for
の代わりにform_tag
を使用して、サインインフォームをリファクタリングしてください。テストスイートが以前と同様にパスすることも確認してください。ヒント: RailsCastの「Rails 3.1における認証 (英語)」を参照してください。特に、params
ハッシュの構造の変更に注目してください。- 8.3.3の例に従い、ユーザー用リクエストspecと認証リクエストspec (
spec/requests
ディレクトリの下にあるファイル) を見直し、spec/support/utilities.rb
でユーティリティ関数を定義して、テストを実装から切り離してください。課外活動: サポートコードを独立したファイルとモジュールに再編成し、specヘルパーファイルでそれらのモジュールを適切にインクルードしてすべて動作するようにしてください。
- 他に、一定時間が経過するとセッションを期限切れにするモデルもあります。このモデルは、インターネットバンキングや金融取引口座などの重要な情報を扱うWebサイトに向いています。↑
- 画像はhttps://www.flickr.com/photos/hermanusbackpackers/3343254977/からの引用です。↑
- このメソッドは、RailsCastの「remember me」の記事を元に選びました。↑
- Active Recordでサポートされるコールバックの種類の詳細については、Railsガイドの「Active Record コールバック」を参照してください。↑
- 実は、この2つは完全に同等です。
attr_accessor
は、単にゲッターメソッドやセッターメソッドを自動的に作成する便利な方法でしかありません。↑ - 通常、これは初期値が
nil
である変数への代入を意味しますが、false
値も||=
演算子によって上書きされることに注意してください。↑ - これは、コラム 6.3で説明したメモ化 (memoization) の例になっています。↑
- Webブラウザは実際にはDELETEリクエストを発行できないので、RailsではJavaScriptを使用してこのリクエストを偽造します。↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!