#目次
1.前回
2.概要
3.内容
4.用語のまとめ
5.感想
6.おわりに
#1. 前回
前章を貼り付ける
#2. 概要
今章の概要を記載
#3. 内容
基本的なログイン機構
7章では新規ユーザー登録。本章ではログイン・ログアウト機能。
認証システム(Authentification System)を使う。
ログイン済みユーザー(current user)を扱うことを認可モデル(Authorizzation Model)
セッション
HTTPはステートレス(Stateless)なプロトコル。
cookieはユーザーのブラウザに保存される小さなテキストデータ。
この章ではsessionメソッドを使い、一時セッションを作成する。
次章ではcookiesメソッドを使う。
セッションをRESTfulなりソースとしてモデリングできると、
ログインページではnewで新しいセッションを出力。
ログインするとcreateでセッションを実際に作成して保存。
ログアウトするとdestroyでセッションを破棄する。
UsersリソースではバックエンドでUsersモデルを介してデータベース上の永続的データにアクセスするのに対し、
Sessionリソースでは変わりにcookiesを保存場所として使う点である。
ログインの大半はcookiesを使った認証メカニズムとして構築される。
####Sessionsコントローラ
ログイン・ログアウトをsessionコントローラーの特定のRESTアクションにそれぞれ対応付ける。
ログインフォームはnewアクションで処理。
createアクションにPOSTリクエストを送信すると、実際にログインする。
destroyアクションにDELETEリクエストを送信するとログアウトする。
#Sessionsコントローラを生成する
$ rails generate controller Sessions new
ログインフォームのモックアップ
Usersリソースのときは専用のresourcesメソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないため、「名前付きルーティング」だけを使う。
GET,POSTリクエストをloginルーティングで、DELETEリクエストをlogoutルーティングで扱う。
# リソースを追加して標準的なRESTfulアクションをgetできるようにする
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
# Sessionsコントローラのテストで名前付きルートを使うようにする
require 'test_helper'
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get login_path
assert_response :success
end
end
HTTPリクエスト URL 名前付きルート アクション名 用途
GET /login login_path new 新しいセッションのページ(ログイン)
POST /login login_path create 新しいセッションの作成(ログイン)
DELETE /logout logout_path destroy セッションの削除(ログアウト)
演習
- GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。
GET login_path:GETリクエストでログイン用フォームを受け取る
POST login_path:ログインフォームで入力した値をPOSTリクエストで送信する。
- ターミナルでrails routes | grep sessionsと入力する。
ubuntu:~/environment/sample_app (basic-login) $ rails routes | grep sessions
sessions_new GET /sessions/new(.:format) sessions#new
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
ubuntu:~/environment/sample_app (basic-login) $
ログインフォーム
コントローラーとルーティングを定義したので
新しいsessionで使うビュー、ログインフォームを整える。
ログインフォームはEmailとPasswordの2つである。
ログイン失敗時はもう一度ログインページを表示してエラー文をフラッシュメッセージで表示させる。
モックアップに従ったログインフォームを作成。
app/views/sessions/new.html.erb
# ログインフォームのコード
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(url: login_path, scope: :session, local: true) 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>
ユーザーがすぐクリックできるよう、ユーザー登録ページのリンクを追加。今の状態はLoginをクリックしても画面遷移しないため、アドレスバーに/loginと入力する。
# 生成したログインフォームのHTML
<form accept-charset="UTF-8" action="/login" method="post">
<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="email" />
<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>
フォーム送信後、paramsハッシュに入る値が、メールアドレスとパスワードのフィールドにそれぞれ対応したparams[:session][:email]とparams[:session][:password]になる。
演習
- リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。
login_pathは2種類ある。リスト8.5の1行目で
methodにpostが指定されており、 postのlogin_pathはcreateアクションになるため。 この1行目がgetであれば、newアクションになる。ユーザーの検索と認証
ユーザー登録でユーザーの作成をしたが、ログインセッションで作成する場合に最初に行うのは、入力が無効な場合の処理です。
ログイン失敗時の表示エラーを配置させる。次に成功した場合の土台を作成する。
最初に最小限のcreateアクションをSessionコントローラで定義、空のnewアクションとdestroyアクションも作成する。
リスト8.6のcreateアクション内では何も行われないが、アクションを実行するとnewビューが出力される。
/sessions/newフォームから送信すると下記の画像になる。
# Sessionsコントローラのcreateアクション(暫定版)
class SessionsController < ApplicationController
def new
end
def create
render 'new'
end
def destroy
end
end
上記に表示されているでデバック情報を見る。
paramsハッシュでは次のようにsessionキーの下にメールアドレスとパスワードがある。
session:
email: 'user@example.com'
password: 'foobar'
commit: Log in
action: create
controller: sessions
ユーザー登録の場合、ネストしたハッシュになっていた。
特にparamsは次のような入れ子ハッシュになって、ハッシュの中にハッシュがある構造である。
{ session: { password: "foobar", email: "user@example.com" } }
つまり、次のようなハッシュがある、params[:session]
このハッシュの値にまたハッシュが含まれており、
{ password: "foobar", email: "user@example.com" }
結果として、次のようにデータにアクセスすることになる。
params[:session][:email]
実際に植えのようにフォームからそうしんされたメールアドレスを取得できる。
params[:session][:password]
上のようにすればフォームから送信されたパスワードを取得できます。
createアクション中ではユーザー認証に必要な情報をparamsハッシュから取り出せることになる。
# ユーザーをデータベースから見つけて検証する
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 && user.authenticate(params[:session][:password])
&&(論理積(and))は取得ユーザーが有効か否かを決定するために使う。
Rubyではnilとfalse以外のすべてのオブジェクトは、真偽値ではtrueになる。
User | Password | a && b |
---|---|---|
存在しない | 何でもよい | (nil && [オブジェクト]) == false |
有効なユーザー | 誤ったパスワード | (true && false) == false |
有効なユーザー | 正しいパスワード | (true && true) == true |
演習
>> user = nil
=> nil
>>
>> !!(user && user.authenticate('foobar'))
=> false
>>
>> user = User.first
(0.4ms) SELECT sqlite_version(*)
User Load (0.1ms) 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-10-07 12:18:50", updated_at: "2021-10-07 12:18:50", password_digest: [FILTERED]>
>>
>> !!(user && user.authenticate('foobar'))
=> false
フラッシュメッセージを表示する
7.33ではユーザー登録のエラーメッセージ表示にUserモデルのエラーメッセージをうまく利用したことを思い出す。
ユーザー登録の場合、エラーメッセージは特定のActiveRecordオブジェクトに関連付けられていたため使えていた。
セッションではActiveRecordのモデルを使っていないため、同じことはできない。
代わりにフラッシュメッセージを表示することにする。
ここではわざと間違えたエラーメッセージ表示のコードを記述する。
# ログイン失敗時の処理を扱う(誤りあり)
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
flash[:danger]を使うことにより
Webサイトのレイアウトに表示することができる。
このコードには誤りがあり、現在のページはエラーメッセージが表示されるが、homeに戻ると消えずに残ってしまう。
表示したテンプレートをrenderメソッドで強制的に再レンダリングしてもリクエストとみなされないため、リクエストのメッセージが消えない。
フラッシュのテスト
フラッシュメッセージが消えないのはこのアプリのバグである。
アプリケーションのログイン挙動を確認するテストのために結合テストを生成する。
$ rails g integration_test users_login
- ログイン用のパスを開く
- 新しいセッションのフォームが正しく表示されたことを確認する。
- わざと無効なparamsハッシュを使いセッション用パスにPOSTする
- 新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する。
- 別のページ(Homeページなど)に一旦移動する。
- 移動先のページでフラッシュメッセージが表示されていないことを確認する。
# フラッシュメッセージの残留をキャッチするテスト
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
assert_template 'sessions/new'
post login_path, params: { session: { email: "", password: "" } }
assert_template 'sessions/new'
assert_not flash.empty?
get root_path
assert flash.empty?
end
end
rails tをするとREDになる。
パスをさせるために以下の記述となる。
# ログイン失敗時の正しい処理
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
rails test test/intergration/users_login_test.rbでパスが通る。
演習
- ログイン画面で間違えて画面移行後にフラッシュメッセージが表示されないことを確認する。
ログイン
無効な値の送信をログインフォームで正しく処理できるようになった。cookieを使った一時セッションでユーザーをログインできるようにする。
cookieはブラウザを閉じると自動的に有効期限が着れるものを使う。
railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになる。
# ApplicationコントローラにSessionヘルパーモジュールを読み込む
class ApplicationController < ActionController::Base
include SessionsHelper
end
log_inメソッド
sessionメソッドを使い、単純なログインを行えるようにする。
このsessionメソッドはハッシュのように扱え次のように代入。
session[:user_id]=user.id
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号済みのユーザーIDが自動作成される。
このあとのページでsession[:user_id]を使い、ユーザーIDを元通り取り出すことができる。一方、cookiesメソッドとは対象的に、sessionメソッドで作成された一時cookiesメソッドは、ブラウザを閉じた瞬間に有効期限が終了する。
使い回せるようにSessionsヘルパーにlog_inという名前のメソッドを定義することにする。
# log_inメソッド
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
sessionメソッドで作成した一時cookiesは自動的に暗号化され保護される。
# ユーザーにログインする
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
Railsでは上のコードを自動変換して、プロフィールページへのルーティングにしている。
user_url(user)
演習
- 有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。
- 先ほどの演習課題と同様に、Expires(有効期限)の値について調べてみてください。
Expires→期限が切れる時間が書いてある。一時セッションの場合はSessionとなっており、ブラウザを閉じたときに切れることがわかる。
現在のユーザー
ユーザーIDを一時セッションの中に安全におけるため、今度はユーザーIDを別ページで取り出す。
current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。current_userメソッドの目的は次のようになる。
<%= current_user.name %>
リダイレクトもできるようにした。
redirect_to current_user
ユーザー検索できるようにする
User.find(session[:user_id])
今度はIDが無効な場合にもメソッドは例外を発生せず、nilを返す。
current_userを次のように定義し直します。
def current_user
if session[:user_id]
User.find_by(id: session[:user_id])
end
end
# セッションに含まれる現在のユーザーを検索する
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
目(演習)(subsubsection)
>> User.find_by(id:"123")
(0.4ms) SELECT sqlite_version(*)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 123], ["LIMIT", 1]]
=> nil
>> 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.1ms) 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: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-14 02:57:10", password_digest: [FILTERED]>
>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-14 02:57:10", password_digest: [FILTERED]>
>>
レイアウトリンクを変更する
ログイン機能でユーザーがログインしている、していないときのレイアウトを変更する。
ログアウト、ユーザー設定、ユーザー一覧、プロフィール表示を追加する。
レイアウトのリンク変更するERBコードは以下のif-else文を使用する。
<% if logged_in? %>
# ログインユーザー用のリンク
<% else %>
# ログインしていないユーザー用のリンク
<% end %>
logged_in?メソッドを変更する。
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではない状態。
これをチェックするために否定演算子の!が必要。
# logged_in?ヘルパーメソッド
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す(いる場合)
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
end
ログアウト用のリンクとパスを使う。
<%= link_to "Log out",logout_path,method::delete %>
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>
Bootstrapのドロップダウンメニュー機能を適用できる状態にした。(cssのdropdownクラス、dropdown-menuを使う)
有効にするためにはRailsのapplication.jsファイルを通してBootstrapに同梱されているJavaScriptライブラリの他にjQueryも読み込む必要がある。
$ yarn add jquery@3.4.1 bootstrap@3.4.1
# WebpackにjQueryの設定を追加する
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
)
module.exports = environment
# 必要なJavaScriptファイルをrequireまたはimportする
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("jquery")
import "bootstrap"
演習は省く
レイアウトの変更をテストする
統合テストを書く
- ログイン用のパスを開く
- セッション用パスに有効な情報をpostする
- ログイン用リンクが表示されなくなったことを確認する
- ログアウト用リンクが表示されていることを確認する
- プロフィール用リンクが表示されていることを確認する
上の変更確認は、テスト時に登録済みユーザーとしてログインしておく必要がある。
このようなテストデータをfixture(フィクスチャ)で作成できる。
# fixture向けのdigestメソッドを追加する
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
# ユーザーログインのテストで使うfixture
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
8章行き詰まったので更新は保留
項(subsection)
目(演習)(subsubsection)
節(section)
項(subsection)
目(演習)(subsubsection)
#4. 用語のまとめ
用語 | 意味 |
---|---|
cookie | ユーザーのブラウザに保存される小さなテキストデータ |
Qitta(キータ) | エンジニアに関する知識を記録・共有するためのサービスです。 |
#5. 感想
- 節の感想
#6. おわりに
章を改めて振り返っての感想
次の目標