その10から続く
10.2.1 ユーザーにログインを要求する
ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御してみましょう。
(こういったセキュリティ上の制御機構をセキュリティモデルと呼びます)。
許可されていないページに対してアクセスするログイン済みのユーザーがいたら(例えば他人のユーザー編集ページにアクセスしようとしたら)、
ルートURLにリダイレクトさせるようにします。
転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルター
を使います。beforeフィルターは、before_actionメソッドを使って何らかの処理
が実行される直前に特定のメソッドを実行する仕組みです。
今回はユーザーにログインを要求するために、logged_in_userメソッドを
定義してbefore_action :logged_in_userという形式で使います
logged_in_user
を追加する redapp/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update] .
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?flash[:danger] = "Please log in."redirect_to login_urlend end
end
適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、
:editと:updateアクションだけにこのフィルタが適用されるように制限をかけています
rails test
test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
log_in_as(@user) get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user) get edit_user_path(@user)
.
.
.
end
end
#beforeフィルターにlogged_in_userを追加する red
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update] ※ココ
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in? ※ココ
flash[:danger] = "Please log in." ※ココ
redirect_to login_url ※ココ
end
end
end
原因は、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったためです。
このため、editアクションやupdateアクションをテストする前にログインしておく必要があります。
解決策は簡単で、log_in_asヘルパーを使うことです。
修正した結果を示します
test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
log_in_as(@user) get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user) get edit_user_path(@user)
.
.
.
end
end
セキュリティモデルを確認するためにbeforeフィルターをコメントアウトするgreen
app/controllers/users_controller.rb
class UsersController < ApplicationController
# before_action :logged_in_user, only: [:edit, :update]
.
.
.
end
beforeフィルターは基本的にアクションごとに適用していくので、
Usersコントローラのテストもアクションごとに書いていきます。
具体的には、正しい種類のHTTPリクエストを使ってeditアクションとupdateアクションをそれぞれ実行させてみて、
flashにメッセージが代入されたかどうか、
ログイン画面にリダイレクトされたかどうかを確認してみましょう。
editとupdateアクションの保護に対するテストする red
test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "should redirect edit when not logged in" do
get edit_user_path(@user)
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch user_path(@user),
params: { user: { name: @user.name,
email: @user.email } }
assert_not flash.empty?
assert_redirected_to login_url
end
end
10.2.2 正しいユーザーを要求する
ユーザーの情報が互いに編集できないことを確認するために、
サンプルユーザーをもう一人追加します。ユーザー用のfixtureファイルに2人目のユーザーを追加してみましょう。
test/fixtures/users.yml
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:name: Sterling Archeremail: duchess@example.govpassword_digest: <%= User.digest('password') %>
私たち開発者は単体テストか統合テストかを意識せずに、
ログイン済みの状態をテストしたいときはlog_in_asメソッドをただ呼び出せば良い、
既にログイン済みのユーザーを対象としているため、ログインページではなくルートURLにリダイレクトしている点に注意してください。
test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer) end
.
.
.
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user)
get edit_user_path(@user)
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
assert flash.empty?
assert_redirected_to root_url
end
end
別のユーザーのプロフィールを編集しようとしたらリダイレクトさせたいので、correct_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにします。
beforeフィルターのcorrect_userで@user変数を定義しているため、
editとupdateの各アクションから、@userへの代入文を削除している点にも注意してください。
app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update] .
.
.
def edit
end
def update
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])redirect_to(root_url) unless @user == current_user end
end
今度はテストスイートが greenになるはずです。
rails test
リファクタリングではありますが、
一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装します。
correct_userの中で使えるようにしたいので、
Sessionsヘルパーの中にこのメソッドを追加します。
このメソッドを使うと今までの
unless @user == current_user
といった部分が、次のように (少し) 分かりやすいコードになります。
current_user?
メソッドapp/helpers/sessions_helper.rb
module SessionsHelper # 渡されたユーザーをログイン
def log_in(user)
session[:user_id] = user.id
end
# 永続セッションとしてユーザーを記憶するdef remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# 渡されたユーザーがログイン済みユーザーであればtrueを返す
def current_user?(user)
user == current_user end
# 記憶トークン (cookie) に対応するユーザーを返す
def current_user
.
.
.
end
.
.
.
end
correct_user
の実装 greenapp/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
def edit
end
def update
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user) end
end
演習
1:何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。
それぞれのパスが異なるため。
edit_user GET /users/:id/edit(.:format) users#edit
user PATCH /users/:id(.:format) users#update
2:上記のアクションのうち、どちらがブラウザで簡単にテストできるアクション?
editアクションGETメソッドのeditの方がURLを打ち込んでアクセスするのみなので簡単
10.2.3 フレンドリーフォワーディング
保護されたページにアクセスしようとすると、問答無用で自分のプロフィールページに移動させられてしまいます。
別の言い方をすれば、ログインしていないユーザーが編集ページにアクセスしようとしていたなら、ログインした後にはその編集ページに
リダイレクトされるようにするのが望ましい動作です。
リダイレクト先は、ユーザーが開こうとしていたページにしてあげるのが親切。
フレンドリーフォワーディングのテストは非常にシンプルに書くことができます。
ログインした後に編集ページへアクセスする、という順序を逆にしてあげるだけです
実際のテストはまず編集ページにアクセスし、ログインした後に、
(デフォルトのプロフィールページではなく) 編集ページにリダイレクトされているかどうかをチェックするといったテストをする。
test/integration/users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit with friendly forwarding" doget edit_user_path(@user)log_in_as(@user)assert_redirected_to edit_user_url(@user) name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があります。
この動作をstore_locationとredirect_back_orの2つのメソッドを使って実現してみましょう。なお、これらのメソッドはSessionsヘルパーで定義しています。
フレンドリーフォワーディングの実装 red
app/helpers/sessions_helper.rb
# 記憶したURL (もしくはデフォルト値) にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
request.original_urlでリクエスト先が取得できます↑
転送先のURLを保存する仕組みは、ユーザーをログインさせたときと同じで、session変数を使います。requestオブジェクトも使っています。
store_locationメソッドでは、リクエストが送られたURLを
session変数の:forwarding_urlキーに格納しています。
ただし、GETリクエストが送られたときだけ格納するようにしておきます。
ログインしていないユーザーがフォームを使って送信した場合、
転送先のURLを保存させないようにできます。
例えばユーザがセッション用のcookieを手動で削除してフォームから送信するケースなどです。
こういったケースに対処しておかないと、POSTやPATCH、
DELETEリクエストを期待しているURLに対して
(リダイレクトを通して)GETリクエストが送られてしまい、
場合によってはエラーが発生します。
このため、if request.get?という条件文を使ってこのケースに対応しています。
先ほど定義したstore_locationメソッドを使って、早速beforeフィルター(logged_in_user) を修正してみましょう。
ログインユーザー用beforeフィルターにstore_locationを追加する
app/controllers/users_controller.rb
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location ※ココ
flash[:danger] = "Please log in."
redirect_to login_url
end
end
フォワーディング自体を実装するには、redirect_back_orメソッドを使います。
リクエストされたURLが存在する場合はそこにリダイレクトし、ない場合は何らかのデフォルトのURLにリダイレクトします。
デフォルトのURLは、Sessionコントローラのcreateアクションに追加し、
サインイン成功後にリダイレクトします。
redirect_back_orメソッドでは、次のようにor演算子||を使います。
session[:forwarding_url] || default
このコードは、値がnilでなければsession[:forwarding_url]を評価し、
そうでなければデフォルトのURLを使っています。
session.delete(:forwarding_url) という行を通して転送用のURLを
削除している点にも注意してください。
これをやっておかないと、
次回ログインしたときに保護されたページに転送されてしまい、
ブラウザを閉じるまでこれが繰り返されてしまいます。
ちなみに、転送用のURLを削除する動作はredirect文の後に置かれていても実行されるという点を覚えておくとよいでしょう。
実は、明示的にreturn文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しません。
したがって、redirect文の後にあるコードでも、そのコードは実行されるのです。
create
アクション greenapp/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])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
rails test
演習
1:フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。
次回以降のログインのときには、
転送先のURLはデフォルト (プロフィール画面) に戻っている必要があります。
ヒント:session[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。
users_edit_test.rb
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
(中略)
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
assert_equal session[:forwarding_url], edit_user_url(@user)
log_in_as(@user)
assert_nil session[:forwarding_url]
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user),
params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
2:debuggerメソッドをSessionsコントローラのnewアクションに置いてみましょう。
その後、ログアウトして /users/1/edit にアクセスしてみてください。
(デバッガーが途中で処理を止めるはずです)。ここでコンソールに
移り、session[:forwarding_url]の値が正しいかどうか確認してみましょう。
また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう
(byebug) session[:forwarding_url]
"https://rails-tutorial-******.c9users.io/users/1/edit"
(byebug) request.get?
true