【rails】投稿にパスワードを設定する方法、url直打ち対策済み

# 投稿にパスワードを設定する方法

投稿にパスワード機能を付けたのでそのアウトプット用に記事を書きました。

## この記事のゴール
以下画像のようにパスワードを設定できるようにします。

![post_with_password.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637746/4b9db1ff-3d48-8a04-3109-21778017120c.gif)


## 投稿機能を作る
さくっと投稿機能を作成。
scaffoldを使えば１分で作れます。

`$ rails new post_with_password`
`$ cd post_with_password`
`$ rails g scaffold Post description:text`
`$ rails db:migrate`

![投稿機能.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637746/83a9f50d-44a6-8c74-0eab-df7dbd41807d.gif)


## bcryptを設定
ユーザーがパスワード付きの投稿をして、詳細に飛ぶまでの流れ
1.投稿時(posts/new)にパスワードを設定
2.詳細ページ(posts/show/:id)を開く際パスワードを要求(パスワード有りの場合)
3.idとパスワードが一致すれば詳細ページにリダイレクト

事前の準備としてパスワードを設定するためにbcrypt(gem)を使用します。

Gemfileに以下のgemがコメントアウトされているので#を削除。
gem 'bcrypt', '~> 3.1.7'

`$ bundle install`

次にモデルに1文追加します。

```ruby:_form.html.erb
class Post < ApplicationRecord
  has_secure_password
end
```

`$ rails g migration add_password_digest_to_posts password_digest:string`
`$ rails db:migrate`

モデルにhas_secure_passwordを追加、
password_digestカラムを追加することで、
passwordとpassword_confirmationと２つの属性を利用可能になります。

※passwordとpassword_confirmationは左記２つが一致していれば、
password_digestに暗号化して格納されるようにな

### 1.投稿時にパスワードを設定
本題のパスワードを設定していきましょう。

まずはviewでユーザーがパスワードを設定できるようにします。

```ruby:post.rb
(省略)
  <div class="field">
    <%= form.label :description %>
    <%= form.text_area :description %>
  </div>
  
  <div class="field">
    <%= form.label :password %>
    <%= form.password_field :password %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
```

次にストロングパラメータに:passwordを追加します。

```ruby:posts_controller.rb
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def post_params
      params.require(:post).permit(:description, :password)
    end
end

```

これでパスワードをdbに保存できるようになりました。

### 2.詳細ページを開く際にパスワードを要求
次に詳細ページを表示する際にパスワードを要求し、設定したパスワードと一致すれば詳細ページにリダイレクトしましょう。

パスワードを認証させるページを作成します。

```ruby:posts_with_password.html.erb
<div class="users-new-wrapper">
  <div class="container">
    <div class="row">
      <div class="col-md-offset-4 col-md-4 users-new-container">
        <h1 class="text-center text-white">password</h1>
        <%= form_for(:post, {controller: 'posts', action: "posts_with_password/#{@post}" }) do |f| %>
          <div class="form-group">
            <%= f.label :password %>
            <%= f.password_field :password, class: 'form-control' %>
          </div>
            <%= f.submit "送信", class: 'btn-block' %>
        <% end %>
      </div>
    </div>
  </div>
</div>
```

ルーティングも追加します。

```ruby:qiita.rb
Rails.application.routes.draw do
  resources :posts
  get       'posts_with_password/:id', to: 'posts#posts_with_password'
  post      'posts_with_password/:id', to: 'posts#authenticate'
end
```

### パスワードが一致すれば詳細ページにリダイレクト
最後に認証機能を作成します。

```ruby:posts_controller.rb
(省略)
 def authenticate
    post_id = Post.find(params[:id])
    if post_id && post_id.authenticate(params[:post][:password])
      redirect_to post_path(post_id)
    else
      render 'posts_with_password'
    end
  end

  def posts_with_pass
```
投稿とパスワードが一致すれば詳細ページにリダイレクトされるようになりました。

昨日はこれで完成です。

ただこれだと全ての投稿にパスワードを設定する必要があるため、パスワードの設定は任意にします。

bcryptで設定したhas_secure_passwordは空の投稿が弾かれてしまうため、
空でも投稿できるように以下を追加します。

```ruby:post.rb
class Post < ApplicationRecord
  has_secure_password(validations: false)
end
```

passwaord_digestにパスワードが入っている時と、入っていない時で
詳細ページのリンク先を変更します。

```ruby:index.hetml.erb
(省略)
<% @posts.each do |post| %>
  <tr>
    <td><%= post.description %></td>
    <td><%= link_to 'Show', post_path(post) %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
```

しかしこれだとせっかくパスワードを設定してもindexからDescriptionが見れてしまいます。

<img width="311" alt="スクリーンショット 2020-07-13 14.02.37.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637746/2ef2cfb6-2208-2668-c479-4f915071249a.jpeg">

password_digestの有無でDescriptionの表示を変更します。

```ruby:index.hetml.erb
<% @posts.each do |post| %>
  <tr>
    <% if post.password_digest.nil? %>
      <td><%= post.description %></td>
    <% else %>
      <td>secret</td>
    <% end %>
    <td><%= link_to 'Show', post_path(post) %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
```

password_digestが空の場合は通常通り、空ではない場合は"secret"と表示させるように設定しました。

## アクセス制限をかける

最後にアクセス制限をかけて完成です。

showをクリックした際にパスワードがなければ、詳細ページに、パスワードがあればパスワード入力フォームに飛ぶようにに設定します。

またurlを直接入力するとパスワードを突破できてしまう問題も解決していきます。

仕組みは以下です。
1.posts/show/:idのパスワードの有無を確認
2.パスワードがなければそのままposts/show/:idを表示
3.パスワードがある場合はurlにpassword_digestの値が含まれていれば、posts/show/:idにリダイレクト
含まれていなければパスワード入力フォームへレダイレクト

3.は解説は解説が必要ですね。
パスワードがない場合は直接urlを打ち込んで詳細ページが表示されて、問題ありません。
パスワード付きの投稿に直接urlを打ち込むと詳細ページが表示されてしまうのは問題です。

とりあえず「パスワード付きの投稿はアクセス制限でパスワード入力フォームにリダイレクトさせよう」
と短絡的に考えるとハマります。(ハマりました)

上記のやり方だと一生詳細ページを開くことはできません。
なぜならパスワード入力フォームで認証をしようがしまいが詳細ページにはパスワードがついているからです。

私が考えたのは、パスワードの認証に成功したときredirect_toに一緒にパラメータを送信し
urlにそのパラメータを含む場合は詳細ページを表示させる方法です。

redirect_toにはパラメータも一緒に送ることができます。
今回はpassword_digestを送ることにします。

理由は２つ。
1.取り出しやすいから
2.安全性が高いから

毎回ランダムの英数字をパラメータに入れて送ろうとも考えたのですが、取り出すのが~~わからn~~面倒で諦めました。

また安全性が高いというのはurlにpassword_digestが含まれていれば突破することができるのですが、
値は60桁の大文字、小文字、数字、記号なのでかなり難しいでしょう。

長々と説明して恐縮ですが次からコードを書いていきます。

まずはredirect_toにパラメータを設定します。

```ruby:posts_controller.rb
def authenticate
    post_id = Post.find(params[:id])
    if post_id && post_id.authenticate(params[:post][:password])
ここ-> redirect_to post_path(post_id, parameter: post_id.password_digest)
    else
      render 'index'
    end
  end
```

これでパスワード認証後はパラメータが付与され、urlが変わります。

次にパスワードの有無でアクセス制限をかけます。

```ruby:posts_controller.rb
before_action :with_password, only: :show

(省略)
private
  def with_password
    url = request.url.gsub!(/%21|%22|%23|%24|%24|%25|%26|%27|%28|%29|%2A|%2B|%2C|%2F|%3A|%3B|%3C|%3D|%3E|%3F|%40|%5B|%5D|%5E|%60|%7B|%7C|%7D|%7E|/,
    "%21" => "!", "%22" => '"', "%23" => "#", "%24" => "$", "%25" => "%", "%26" => "&", "%27" => "'", "%28" => "(", "%29" => ")",
    "%2A" => "*", "%2B" => "+", "%2C" => ",", "%2F" => "/", "%3A" => ":", "%3B" => ";", "%3C" => "<", "%3D" => "=", "%3E" => ">", "%3F" => "?", "%40" => "@",
    "%5B" => "[", "%5D" => "]", "%5E" => "^", "%60" => "`", "%7B" => "{", "%7C" => "|", "%7D" => "}", "%7E" => "~")
    @post = Post.find(params[:id])
    if !@post.password_digest.nil? && !url.try(:include?, @post.password_digest)
      redirect_to "/posts_with_password/#{@post.id}", danger: 'パスワードを入力してください'
    end
  end
```

上から順に説明していきます。
url = request.url
まず上記のコードでurlを取得します。

コードを書いているときにここでハマりました。
urlをそのまま取得しても認証されませんでした。

byebugを使いurlとpassword_digestの中身を比較してみると$など一部の文字が
違っていました。
調べていくとurlには使えない文字列があるようでこれをエンコードする必要が出てきました。

一発で変換する方法を探すことができなかったためgsubを使い無理やり変更しました。（もしこれ以外に良い書き方があったら教えてください。。）

以下の情報を参考にしました。
https://www.seil.jp/doc/index.html#tool/url-encode.html

これでurlの取得は完了です。

最後の仕上げです。
以下のコードの説明をして完成です。

if !@post.password_digest.nil? && !url.try(:include?, @post.password_digest)

パスワード有かつurlにpassword_digestが含まれていない状態で詳細ページを開こうとした場合、
パスワード入力フォームにリダイレクトさせます。

tryを使わないとパスワード無しの場合エラーが起きてしまうために入れています。

## まとめ

以上で完成です。

ログイン以外にもbcrypt使ってあげてくださいね。

ポートフォリオなどの参考になれば嬉しいです。