#はじめに
アプリ実装中、"URL直打ち"対策を行なったので、記録として残します。
Ruby 2.6.5
#目次
1. "URL直打ち"とは
2. 対 ログアウトユーザー
3. 対 ログインユーザー
4. 対 ログインしている自分
5. まとめ
#1. "URL直打ち"とは
"URL直打ち"とは、その名前の通りURLに直接書き込むことです。
例えば、他のユーザーが投稿したつぶやきや写真・動画などはこちらが勝手に編集・削除はできませんよね。
それに対し、自分が投稿したものに対してはいつでも編集・削除ができます。
これは自分自身と他のユーザーとで画面上でのボタンの表示の切り替えがされるように実装されているからです。
なので、普通なら他ユーザーの投稿は編集・削除ができません。
しかし、URLの仕組みを理解してると、URLに直接書き込むことで他のユーザーの編集画面ページに遷移することができてしまいます!
なので、この対策はしっかりおこなわなければなりません!
それが"URL直打ち"対策というわけです。
今回登場するモデルとテーブルの関係
モデルとテーブルの関係を簡単にではありますが、記載しておきます。
#2. 対 ログアウトユーザー
まずは、ログアウトユーザーへの対策です。
これはすごくシンプルで、以下のコードを対策したいコントローラー内に設定していれば対策できています!
before_action :authenticate_user!, except: [:index, :show]
実際にコントローラー内に記述したらこんな感じ。
class ItemsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def index
end
def show
end
end
記述したコードを1つずつ要約すると以下の通りです。
記述 | 説明 |
---|---|
before_action | このコードの後ろに書かれたメソッドは、このコントローラー内の処理が動く前に実行される. つまり、一番はじめに実行される. |
:authenticate_user! | ログイン済みユーザーの認証を行う。 ログインしてなければ、ログイン画面にとばす! devise機能を実装することで使えるメソッドです。 (作成するモデル名によってuserの部分が変わります。今回はモデル名にUserを使用しています) |
except: [:index, :show] | index(一覧)とshow(詳細)は除く |
これを踏まえて、このコードを翻訳すると |
「ログインしてなかったら弾き飛ばす。でも、indexとshowはログインしてなくても見れるよ。」
というような感じです。
大体の投稿アプリは、以下のような機能はログアウトしていてもできると思います。
他の投稿者のツイートや写真・動画投稿の閲覧
その投稿者の詳細ページ閲覧
検索
しかし、いざ投稿に対してのコメントや自分が投稿しようとしたらログイン画面に飛ばされてしまいますよね?
それはこの対策がしっかりされているからです。
もちろん、URLを直打ちしてページ遷移をすることも防いでくれています!
ちなみに、except
はその後に指定されたものを「除く」という意味ですが、これと反対でonly
というものもがあります。
こちらは、その後に指定したもの「のみ対象」という意味になります。
before_action :authenticate_user!, only: [:index, :show]
# indexとshowのみログアウトしてたら見れないよ
これで、対ログアウトユーザーへの対策はできました!
3. 対 ログインユーザー
次はログインしている自分以外のユーザーに対しての対策です。
例えば自身が投稿した内容はいつでも編集(edit, update)・削除(destroy)できますよね。
画面上ではログイン/ログアウト、自身と他ユーザーによってボタン表示の切り替えはバッチリできていても、URLの対策をしていないと直接URLに打ち込まれてあっさり編集画面に侵入...なんてことが起きてしまいます。
しっかり対策しておきましょう!
他にも色々あると思いますが、シンプルな例として以下のことが挙げられます。
自身が投稿(出品)した内容を他ユーザーに編集・削除されたくない
これに対しては以下のようにコードを記述することで対策ができます。
# 商品情報のidを取得
@item = Item.find(params[:id])
# 出品者本人かどうかの分岐
if @item.user_id != current_user.id
redirect_to root_path
end
記述したコードを1つずつ要約すると以下の通りです。
記述 | 説明 |
---|---|
@item.user_id | itemsテーブルにあるuser_idを取得。 itemモデルとuserモデルでアソシエーションを組んでいるため、このような記述ができます。 |
!= | 「等しくない時」という意味。 |
current_user.id | 【current_user】 devise機能を実装することで使えるメソッドです。 (作成するモデル名によってuserの部分が変わります。今回はモデル名にUserを使用しています) ログインしているユーザーを取得してくれます。 .idをつけることで現在ログインしているユーザーのidを取得してくれます。 |
これを踏まえて、このコードを翻訳すると |
「投稿(出品)したユーザーと現在のユーザーのidが違えばトップページに飛ばす。」
というふうになります。
実際にコントローラー内のedit
だけに適応したければこんな感じになります。
# editアクションのみに対応
def edit
@item = Item.find(params[:id])
if @item.user_id != current_user.id
redirect_to root_path
end
end
けれど他にも、destroy
アクション、update
アクションにも適応させたいです。
しかし、それぞれのアクション内に上記のコードを記述していくと非常に非常に冗長でかつ見にくいコードになってしまいます。
というか普通に邪魔くさいです。
# それぞれのアクションに記述
# かなり見にくいですよね
def edit
if @item.user_id != current_user.id
redirect_to root_path
end
end
def update
if @item.user_id != current_user.id
redirect_to root_path
end
end
def destroy
if @item.user_id != current_user.id
redirect_to root_path
end
end
なので、複数同じコードを記述する場合は
コードを一箇所にまとめて、適応させたいアクションのみ指定してあげる
とした方がスッキリます!
ちなみに、edit
アクションを防いだ時点でupdate
アクションは呼ばれないはずです。
しかし、検証ツールからボタンを表示させる、URLを直接入力をするといったことにより、update
アクションにたどり着いてしまう可能性があります!
なので、
-
edit
・update
アクション -
new
・create
アクション
はセットで記述した方がいいです。
コードを1つにまとめるとこんな感じになります。
before_actionで呼び出しているprevent_url
は私が勝手に考えた名前ですので、ここの命名は自由で大丈夫です!
class ItemsController < ApplicationController
before_action :set_furima, only: [:edit, :update, :destroy]
before_action :prevent_url, only: [:edit, :update, :destroy]
def edit
end
def update
end
def destroy
end
private
def set_furima
@item = Item.find(params[:id])
end
def prevent_url
if @item.user_id != current_user.id
redirect_to root_path
end
end
end
これで、対ログアウトユーザーへの対策はできました!
4. 対 ログインしている自分
最後は、ログインしている自分自身にもURLを直打ちしてページ遷移させないようにする実装です。
個人的にここの実装がかなり手こずりました。
実装する内容としては、以下の2つです。
① ログイン状態の出品者が自身の出品した商品の購入画面に遷移できないようにする
② ログイン状態の出品者が売却済みの自身が出品した商品に対して、商品購入画面に遷移できないようにする
特に②の売却済みの商品という表現に苦戦しました。
結論から言うと、以下のコードを記述して①と②の対策をしました。
class PurchasesController < ApplicationController
before_action :set_furima, only: [:index, :create]
before_action :prevent_url, only: [:index, :create]
def index
end
def create
end
private
def set_furima
@item = Item.find(params[:item_id])
end
def prevent_url
if @item.user_id == current_user.id || @item.purchase != nil
redirect_to root_path
end
end
end
上記のコードは論理演算子||(または)
を用いて2つに分けています。
① ログイン状態の出品者が自身の出品した商品の購入画面に遷移できないようにする
@item.user_id == current_user.id
これは対 ログインユーザーで説明したコードとほぼ同じです。
内容としては、
「投稿(出品)したユーザーと現在のユーザーのidが同じであればトップページに飛ばす。」
となります。
② ログイン状態の出品者が売却済みの自身が出品した商品に対して、商品購入画面に遷移できないようにする
@item.purchase != nil
このコードを1つずつ要約すると以下の通りです。
記述 | 説明 |
---|---|
@item.purchase | itemsモデルに紐づくpurchasesモデルのitem_idを取得 |
!= | 「等しくない時」という意味。 |
nil | 「何もない、空」という意味。 |
つまり、②のコードを翻訳すると
「purchasesテーブルにあるitem_idがnil(空)でない時」
というふうになります。
purchasesテーブルは商品の購入情報を保存するテーブルであるため、
itemsテーブルのidとpurchasesテーブルのitem_idが一致する = 商品は購入されている
ということになります。
逆にいうと、itemsテーブルのidとpurchasesテーブルのitem_idが一致しなければ、その商品はまだ販売中ということになります。
また、売却済みの自身が出品した商品の編集画面にも遷移できないように実装したいので、itemsコントローラーのところにもコードを追加しました。
class ItemsController < ApplicationController
before_action :set_furima, only: [:edit, :update, :destroy]
before_action :prevent_url, only: [:edit, :update, :destroy]
def edit
end
def update
end
def destroy
end
private
def set_furima
@item = Item.find(params[:id])
end
def prevent_url
if @item.user_id != current_user.id || @item.purchase != nil # コードを追加
redirect_to root_path
end
end
end
これで出品者本人に対するURL直打ち対策ができました!
5. まとめ
画面上のボタンの表示切り替えはしっかりできていても、URLの直打ち対策をうっかり忘れてしまう...なんてことがないようにしないといけませんね。
今回の実装でかなり勉強になりました!