投稿にパスワードを設定する方法
投稿にパスワード機能を付けたのでそのアウトプット用に記事を書きました。
この記事のゴール
以下画像のようにパスワードを設定できるようにします。
投稿機能を作る
さくっと投稿機能を作成。
scaffoldを使えば1分で作れます。
$ rails new post_with_password
$ cd post_with_password
$ rails g scaffold Post description:text
$ rails db:migrate
bcryptを設定
ユーザーがパスワード付きの投稿をして、詳細に飛ぶまでの流れ
1.投稿時(posts/new)にパスワードを設定
2.詳細ページ(posts/show/:id)を開く際パスワードを要求(パスワード有りの場合)
3.idとパスワードが一致すれば詳細ページにリダイレクト
事前の準備としてパスワードを設定するためにbcrypt(gem)を使用します。
Gemfileに以下のgemがコメントアウトされているので#を削除。
gem 'bcrypt', '~> 3.1.7'
$ bundle install
次にモデルに1文追加します。
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と2つの属性を利用可能になります。
※passwordとpassword_confirmationは左記2つが一致していれば、
password_digestに暗号化して格納されるようにな
1.投稿時にパスワードを設定
本題のパスワードを設定していきましょう。
まずはviewでユーザーがパスワードを設定できるようにします。
(省略)
<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を追加します。
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.詳細ページを開く際にパスワードを要求
次に詳細ページを表示する際にパスワードを要求し、設定したパスワードと一致すれば詳細ページにリダイレクトしましょう。
パスワードを認証させるページを作成します。
<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>
ルーティングも追加します。
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
パスワードが一致すれば詳細ページにリダイレクト
最後に認証機能を作成します。
(省略)
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は空の投稿が弾かれてしまうため、
空でも投稿できるように以下を追加します。
class Post < ApplicationRecord
has_secure_password(validations: false)
end
passwaord_digestにパスワードが入っている時と、入っていない時で
詳細ページのリンク先を変更します。
(省略)
<% @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が見れてしまいます。
password_digestの有無でDescriptionの表示を変更します。
<% @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を送ることにします。
理由は2つ。
1.取り出しやすいから
2.安全性が高いから
毎回ランダムの英数字をパラメータに入れて送ろうとも考えたのですが、取り出すのがわからn面倒で諦めました。
また安全性が高いというのはurlにpassword_digestが含まれていれば突破することができるのですが、
値は60桁の大文字、小文字、数字、記号なのでかなり難しいでしょう。
長々と説明して恐縮ですが次からコードを書いていきます。
まずはredirect_toにパラメータを設定します。
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が変わります。
次にパスワードの有無でアクセス制限をかけます。
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使ってあげてくださいね。
ポートフォリオなどの参考になれば嬉しいです。