URL直打ち対策（ログアウト/ログイン別まとめ）

#はじめに
アプリ実装中、"URL直打ち"対策を行なったので、記録として残します。

Ruby 2.6.5


#目次
1.　"URL直打ち"とは
2.　対 ログアウトユーザー
3.　対 ログインユーザー
4.　対 ログインしている自分
5.　まとめ

#1. "URL直打ち"とは
"URL直打ち"とは、その名前の通りURLに直接書き込むことです。

例えば、他のユーザーが投稿したつぶやきや写真・動画などはこちらが勝手に編集・削除はできませんよね。
それに対し、自分が投稿したものに対してはいつでも編集・削除ができます。
これは自分自身と他のユーザーとで画面上でのボタンの表示の切り替えがされるように実装されているからです。
なので、普通なら他ユーザーの投稿は編集・削除ができません。

しかし、URLの仕組みを理解してると、URLに直接書き込むことで<b>他のユーザーの編集画面ページに遷移する</b>ことができてしまいます！

なので、この対策はしっかりおこなわなければなりません！
それが"URL直打ち"対策というわけです。


### 今回登場するモデルとテーブルの関係

モデルとテーブルの関係を簡単にではありますが、記載しておきます。
![スライド1 (6).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/18c748d6-5ac9-6515-1c75-ba1932dfe676.jpeg)


#2. 対 ログアウトユーザー
まずは、ログアウトユーザーへの対策です。

これはすごくシンプルで、以下のコードを対策したいコントローラー内に設定していれば対策できています！

```ruby
  before_action :authenticate_user!, except: [:index, :show]
```

<br>
実際にコントローラー内に記述したらこんな感じ。

```ruby:items_controller.rb
class ItemsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]

  def index
  end

  def show
  end
end
```
<br>

記述したコードを１つずつ要約すると以下の通りです。

|記述|説明|
|:---|:---|
|<b> before_action</b>|このコードの後ろに書かれたメソッドは、このコントローラー内の処理が動く前に実行される.<br>つまり、一番はじめに実行される.|
|<b>:authenticate_user!</b>|ログイン済みユーザーの認証を行う。<br>ログインしてなければ、ログイン画面にとばす！<br><b><font color="tomato">devise機能を実装することで使えるメソッドです。<br>（作成するモデル名によってuserの部分が変わります。今回はモデル名にUserを使用しています）</font></b>
|<b>except: [:index, :show]</b>|index（一覧）とshow（詳細）は<b>除く</b>|
これを踏まえて、このコードを翻訳すると

<b>「ログインしてなかったら弾き飛ばす。でも、indexとshowはログインしてなくても見れるよ。」</b>

というような感じです。
大体の投稿アプリは、以下のような機能はログアウトしていてもできると思います。

* `他の投稿者のツイートや写真・動画投稿の閲覧`
* `その投稿者の詳細ページ閲覧`
* `検索`

しかし、いざ投稿に対してのコメントや自分が投稿しようとしたらログイン画面に飛ばされてしまいますよね？
それはこの対策がしっかりされているからです。
もちろん、URLを直打ちしてページ遷移をすることも防いでくれています！

ちなみに、`except`はその後に指定されたものを「除く」という意味ですが、これと反対で`only`というものもがあります。<br>こちらは、その後に指定したもの「のみ対象」という意味になります。

```ruby
  before_action :authenticate_user!, only: [:index, :show]
  # indexとshowのみログアウトしてたら見れないよ
```

これで、対ログアウトユーザーへの対策はできました！

# 3. 対 ログインユーザー
次はログインしている<b>自分以外のユーザー</b>に対しての対策です。<br>

例えば自身が投稿した内容はいつでも編集（edit, update）・削除(destroy)できますよね。
画面上ではログイン/ログアウト、自身と他ユーザーによってボタン表示の切り替えはバッチリできていても、URLの対策をしていないと<b>直接URLに打ち込まれてあっさり編集画面に侵入</b>...なんてことが起きてしまいます。
しっかり対策しておきましょう！<br>
他にも色々あると思いますが、シンプルな例として以下のことが挙げられます。

* `自身が投稿（出品）した内容を他ユーザーに編集・削除されたくない`

これに対しては以下のようにコードを記述することで対策ができます。

```ruby
  # 商品情報のidを取得
  @item = Item.find(params[:id])

  # 出品者本人かどうかの分岐
  if @item.user_id != current_user.id
    redirect_to root_path
  end
```


<br>記述したコードを１つずつ要約すると以下の通りです。

|記述|説明|
|:---|:---|
|<b> @item.user_id </b>|itemsテーブルにあるuser_idを取得。<br>itemモデルとuserモデルでアソシエーションを組んでいるため、このような記述ができます。|
|<b>!=</b>|「等しくない時」という意味。
|<b>current_user.id</b>|【current_user】<br><b><font color="tomato">devise機能を実装することで使えるメソッドです。<br>（作成するモデル名によってuserの部分が変わります。今回はモデル名にUserを使用しています）</font></b><br>ログインしているユーザーを取得してくれます。<br>.idをつけることで現在ログインしているユーザーのidを取得してくれます。|
これを踏まえて、このコードを翻訳すると

<b>「投稿（出品）したユーザーと現在のユーザーのidが違えばトップページに飛ばす。」</b>
<br>というふうになります。

<br>実際にコントローラー内の`edit`だけに適応したければこんな感じになります。

```ruby:items_controller.rb
# editアクションのみに対応
def edit
  @item = Item.find(params[:id])

  if @item.user_id != current_user.id
    redirect_to root_path
  end
end
```

けれど他にも、`destroy`アクション、`update`アクションにも適応させたいです。
しかし、それぞれのアクション内に上記のコードを記述していくと非常に非常に冗長でかつ見にくいコードになってしまいます。
というか普通に邪魔くさいです。

```ruby:items_controller.rb
# それぞれのアクションに記述
# かなり見にくいですよね

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
```
<br>なので、複数同じコードを記述する場合は<br>
`コードを一箇所にまとめて、適応させたいアクションのみ指定してあげる`
<br>とした方がスッキリます！

---
ちなみに、`edit`アクションを防いだ時点で`update`アクションは呼ばれないはずです。
しかし、検証ツールからボタンを表示させる、URLを直接入力をするといったことにより、`update`アクションにたどり着いてしまう可能性があります！
なので、


* `edit`・`update`アクション
* `new`・`create`アクション

はセットで記述した方がいいです。

---

コードを１つにまとめるとこんな感じになります。
before_actionで呼び出している`prevent_url`は私が勝手に考えた名前ですので、ここの命名は自由で大丈夫です！

```ruby:items_controller.rb
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を直打ちしてページ遷移させないようにする実装です。
個人的にここの実装がかなり手こずりました。<br>
実装する内容としては、以下の２つです。<br>
`① ログイン状態の出品者が自身の出品した商品の購入画面に遷移できないようにする`
`② ログイン状態の出品者が売却済みの自身が出品した商品に対して、商品購入画面に遷移できないようにする`

特に②の<b>売却済みの商品</b>という表現に苦戦しました。
結論から言うと、以下のコードを記述して①と②の対策をしました。

```ruby:purchases_controller.rb
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
```

上記のコードは論理演算子`||(または)`を用いて２つに分けています。
<br>
① ログイン状態の出品者が自身の出品した商品の購入画面に遷移できないようにする<br>
`@item.user_id == current_user.id `<br>
これは<b>対 ログインユーザー</b>で説明したコードとほぼ同じです。<br>内容としては、<br>
<b>「投稿（出品）したユーザーと現在のユーザーのidが同じであればトップページに飛ばす。」</b>
<br>となります。
<br>

② ログイン状態の出品者が売却済みの自身が出品した商品に対して、商品購入画面に遷移できないようにする<br>
`@item.purchase != nil `<br>

<br>このコードを１つずつ要約すると以下の通りです。

|記述|説明|
|:---|:---|
|<b> @item.purchase </b>|itemsモデルに紐づくpurchasesモデルのitem_idを取得|
|<b>!=</b>|「等しくない時」という意味。
|<b>nil</b>|「何もない、空」という意味。|


つまり、②のコードを翻訳すると

<b>「purchasesテーブルにあるitem_idがnil(空)でない時」</b>

というふうになります。<br>purchasesテーブルは商品の購入情報を保存するテーブルであるため、<br>
`itemsテーブルのidとpurchasesテーブルのitem_idが一致する = 商品は購入されている`<br>
ということになります。
逆にいうと、itemsテーブルのidとpurchasesテーブルのitem_idが一致しなければ、その商品はまだ販売中ということになります。
![スライド2 (5).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/aeadca16-4846-af27-f163-5f94be0f9852.jpeg)



また、売却済みの自身が出品した商品の編集画面にも遷移できないように実装したいので、itemsコントローラーのところにもコードを追加しました。

```ruby:items_controller.rb
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の直打ち対策をうっかり忘れてしまう...なんてことがないようにしないといけませんね。
今回の実装でかなり勉強になりました！