#はじめに
本章では**認証システム(Authentification System)**を実装していきます。
認証システム とは?
ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組みのこと。
#8.1 セッション
HTTP
はステートレス(Stateless)なプロトコル
で、リクエスト前の情報を利用することができない独立したトランザクションとして扱われます。なので、ブラウザでページを移動したときにユーザーIDを保持しておく手段がHTTPプロトコル内にはありません。
なので、セッション(Session)
と呼ばれる半永続的な接続をコンピュータ間(Webブラウザ⇔Railsサーバーなど)に設定をします。
Railsでセッションを実装する方法で最も一般的なのはcookies
(ブラウザに保存されるテキストデータ)を使う方法です。ページを移動してもユーザーIDは破棄されません。
##8.1.1 Sessionsコントローラ
ログインとログアウトの要素をSessionコントローラの特定のRESTアクションにそれぞれ対応付けます。
Sessionリソースでは、login
ルーティングでGET
リクエストとPOST
リクエスト、logoutルーティングでDELETE
リクエストを扱います。
###演習 1
GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。
GETはページを取得して表示させるメソッドで、POSTはデータを作成するメソッド。
GET login_pathは/loginにアクセスしたときにsessions#newを実行する。
get '/login', to: 'sessions#new'
POST login_path`は/loginにアクセスしたときにsessions#createを実行する。
post '/login', to: 'sessions#create'
###演習 2
ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか? ヒント: パイプやgrepの使い方が分からない場合は 『開発基礎編: コマンドライン』の 「grepで検索する」を参考にしてみてください。
$ rails routes | grep sessions
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
##8.1.2 ログインフォーム
ログインフォームを整えます。ログインフォームで入力した情報に誤りがあったときは、ログインページをもう一度表示してエラーメッセージを出力します
セッションはActive Recordオブジェクトではないので、フラッシュを使ってエラーメッセージを表示させます。
セッションフォームは、Sessionモデルというものがなく、そのため@userのようなインスタンス変数に相当するものがありません。
なので、リソースのスコープ(ここではセッション)とそれに対応するURLを具体的に指定する必要があります。
form_with(url: login_path, scope: :session, local: true)
###演習 1
リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。
<form accept-charset="UTF-8" action="/login" method="post">
「/login」に「POSTメソッド」リクエストを送る。
post '/login', to: 'sessions#create'
「/login」にアクセスすると「sessions#create」が実行される。
Sessionsコントローラのcreateアクションが実行される
##8.1.3 ユーザーの検索と認証
- フォームが送信されたときの動作を順を追って理解します。
- ログインが失敗した場合に表示されるエラーメッセージを配置します。
- ログインに成功した場合に、パスワードとメールアドレスの組み合わせが有効かどうかを判定します。
params
の構造
ハッシュの中にハッシュがある構造になっています。
{ session: { password: "foobar", email: "user@example.com" } }
params[:session]
に{ password: "foobar", email: "user@example.com" }
が含まれていますので、アクセスの仕方は下記のとおりになります。
# params[:session]の中のemailを取得せよ
params[:session][:email]
create
アクションの中では、ユーザーの認証に必要なあらゆる情報をparams
ハッシュから簡単に取り出すことができます。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
# 送信されたemailを全て小文字にしてDBから取り出し、変数userに代入せよ
if user && user.authenticate(params[:session][:password])
# もし、ユーザーがデータベースにあり、かつ、認証に成功した場合ならtrue
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
# エラーメッセージを作成する
render 'new'
end
end
def destroy
end
end
authenticateメソッド
引数に渡された文字列(パスワード)をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較します。
###演習 1
Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を確かめてみてください。ヒント: 必ず論理値オブジェクトとなるように、4.2.2で紹介した!!のテクニックを使ってみましょう。例: !!(user && user.authenticate('foobar'))
>> user = nil
=> nil
>> user = User.first
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2021-02-13 18:10:29", updated_at: "2021-02-13 18:10:29", password_digest: [FILTERED]>
>> !!(user && user.authenticate('railstutorial'))
=> true
##8.1.4 フラッシュメッセージを表示する
flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
このままだとフラッシュメッセージが一度表示されると消えずに残ってしまう。
##8.1.5 フラッシュのテスト
ログインフォームの送信について簡単な統合テストを作成しましょう。
- ログイン用のパスを開く
- 新しいセッションのフォームが正しく表示されたことを確認する
- わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
- 新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
- 別のページ(Homeページなど) にいったん移動する
- 移動先のページでフラッシュメッセージが表示されていないことを確認する
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
# ログイン用のパスを開く
assert_template 'sessions/new'
# 新しいセッションのフォームが正しく表示されたことを確認する
post login_path, params: { session: { email: "", password: "" } }
# わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
assert_template 'sessions/new'
# 新しいセッションのフォームが再度表示されたことを確認する
assert_not flash.empty?
# フラッシュメッセージが追加されることを確認する
get root_path
# HOMEページにいったん移動する
assert flash.empty?
# 移動先のページでフラッシュメッセージが表示されていないことを確認する
end
end
8.1.4の間違った記述のままなのでエラーが出ます。
FAIL["test_login_with_invalid_information", #<Minitest::Reporters::Suite:0x000055c55ae2fef8 @name="UsersLoginTest">, 15.065257259999726]
test_login_with_invalid_information#UsersLoginTest (15.07s)
Expected false to be truthy.
test/integration/users_login_test.rb:12:in `block in <class:UsersLoginTest>'
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
flash
をflash.now
に置き換えるとテストはパスします。
###演習 1
8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
確認のみなので省略