Rails – 入力整合性~password_field~bcrypt

概要

bcrypthas_secure_passwordを使ったときの整合性チェックの挙動はtext_fieldの整合性チェックと少し挙動が違う。

has_secure_passwordをモデルに書くと、:passwordに対する整合性を外してもpresenceconfirmationの検証が強制的に行われる。このため意図しないメッセージが重複したり、敢えて空白の入力をスルーしたりしたい場合に、この検証が邪魔になることがある。

この強制的な検証を止めたいときには、has_secure_passwordのオプションにvalidations: falseを指定する(Railsドキュメント)。

手順

設定

  • Gemのbcryptがインストールされていて、Userモデルにhas_secure_passwordが記述されている
  • Userモデルではpassword_digestが準備されている
  • フォームでpasswordの入力欄と再確認用のpassword_confirmationの入力欄の2つを表示する
  • フォーム送信後、2つの入力欄の値が等しいかどうかをバリデーションで確認する

ルーティング

  • usersコントローラーのconfirm_passwordアクションで入力フォームを表示
  • フォームからのPOSTをconfirm_password_processアクションで受け取って処理

モデルの生成

:name属性と:password_digest属性を持つUserモデルを構成する。

マイグレート後

ビュー

名前入力フィールド、パスワード入力フィールドとパスワード確認フィールドを準備する。確認フィールドは入力フィールドの名前に_confirmationポストフィックスを付ける(入力フィールド:passwordに対して:password_confirmation)。

モデル

Userモデルでhas_secure_passwordを設定し、バリデーションではパスワードの整合性のみ検証させる。

コントローラー

confirm_passwordアクションでUserインスタンスを準備してフォームを表示し、confirm_password_processでフォームから受け取ったパラメーターの整合性をチェック。

実行結果

強制的なバリデーションの実行

:password:password_confirmation"aaa""aaa"のときは、valid? == trueでエラーメッセージは空。

パラメーターにはpasswordpassword_confirmationの内容が平文で保持されているが、Userインスタンスにあるのはpassword_digestのみ。

:password:password_confirmationの内容が異なる場合はvalid? == falseで、エラーメッセージは同じ"doesn't match ..."が2回現れている。

:passwordに何か入力して:password_confirmationを空欄にしても同じ結果になる。

:passwordを空欄にした場合、:password_confirmationの内容の有無に関わらずvalid? == falseで、エラーメッセージは"can't be blank"が2回、"doesn't match ..."が1回発生。

また、:password:password_confirmation両方を空白のままにした時も同じ結果になる。

:password:password_confirmationの内容がそれぞれA、B、空白の組み合わせに対する結果をまとめると以下のようになり、検証結果が不適切な場合の全てでメッセージが2つ出されている。

pass\conf A B 空白
A true
[]
false
“doesn’t match”
“doesn’t match”
false
“doesn’t match”
“doesn’t match”
B false
“doesn’t match”
“doesn’t match”
true
[]
false
“doesn’t match”
“doesn’t match”
空白 false
“can’t be blank”
“doesn’t match”
false
“can’t be blank”
“doesn’t match”
false
“can’t be blank”
“doesn’t match”

bcryptによるバリデーションの抑止

ここで、has_secure_passwordのオプションでバリデーションを無効にしてみる。

これで先と同じ票のように実行してみる。

pass\conf A B 空白
A true
[]
false
“doesn’t match”
false
“doesn’t match”
B false
“doesn’t match”
true
[]
false
“doesn’t match”
空白 false
“doesn’t match”
false
“doesn’t match”
false
“doesn’t match”

validatesの方でpresenceのチェックをせずconfirmationだけをチェックしているので、空白の場合でも"can't be blank"は発生していない。

また、bcrypt側の検証を止めているので、"doesn't match ..."のメッセージもvalidatesconfirmによるものだけになっている。

 

Rails – 入力整合性~text_field

概要

Railsのバリデーションには入力の整合性チェックの機能があり、以下のような2つの入力フィールドの内容が同じかどうかをチェックする。


手順

設定

  • SimpleUserモデルのname属性に対してフォーム入力する
  • フォームではnameの入力欄と再確認用の入力欄の2つを表示する
  • フォーム送信後、2つの入力欄の値が等しいかどうかをバリデーションで確認する

ルーティング

  • simple_usersコントローラーのconfirm_nameアクションで入力フォームを表示
  • フォームからのPOSTをconfirm_name_processアクションで受け取って処理

モデルの生成

:name属性1つだけを持つSimpleUserモデルを構成する

マイグレート後

ビュー

入力フィールドと確認フィールドを準備する。確認フィールドは入力フィールドの名前に_confirmationポストフィックスを付ける(入力フィールドが:nameの場合、:name_confirmation)。

モデル

Userモデルで整合性のみ検証させる。

コントローラー

confirm_nameアクションでSimpleUserインスタンスを準備してフォームを表示し、confirm_name_processでフォームから受け取ったパラメーターの整合性をチェック。

実行結果

:name:name_confirmation"aaa""aaa"のときは、valid? == trueでエラーメッセージは空。

"aaa""bbb"のときは、valid? == falseでエラーメッセージがセットされる。

"aaa"""のとき(:name_confirmationに入力しないとき)は、valid? == falseでエラーメッセージがセットされる。

""""のとき(どちらも入力しないとき)は、valid? == trueでエラーメッセージは空。

 

Rails – パスワードの暗号化~bcrypt

概要

Gemの1つ、bcryptをインストールすることで、パスワードの暗号化が容易に実装できるようになる。手順の概要は、Userモデルを例にとると以下のようになる。

<ユーザー登録時>

  • bcryptを導入する
  • usersテーブルのパスワードカラムをpassword_digestとする
  • Userモデルにhas_secure_passwordメソッドを記述
  • サインインフォームに:passwordフィールドを置く
  • フォームからのPOSTに対してUserインスタンスを生成
  • Userインスタンスをデータベースに保存

<ユーザー認証時>

  • フォームのPOSTに対してユーザーを特定し、Userインスタンスを生成
  • フォームに入力されたパスワードパラメーターを使って、Userインスタンスのauthenticateメソッドで認証

手順

設定

  • 登録画面でユーザーの名前とパスワードを登録
  • 認証画面でユーザーの名前とパスワードを入力して認証

bcryptの導入

Gemfilebcryptのコメントを外す。なければ追加。

Railsサーバー停止状態でインストール。

ユーザーモデルの準備

マイグレーションファイルのパスワード属性はpassword_digestとする。

マイグレート後。

ルーティング

この例では4つのアクションを以下のようにルーティング

  • secure_sign_upで登録フォームを表示
  • secure_sign_up_processで登録処理(パスワードを暗号化)
  • secure_sign_inで認証フォームを表示
  • secure_sign_in_processで認証処理

ユーザー登録

登録画面

登録フォームの基本構成はシンプルで、パスワードは:passwordの名前でpassword_formを置くだけでよい。

登録処理

フォームからのPOSTに対して、:passwordを含むパラメーターでUserインスタンスを生成し、データベースに保存するだけでパスワードが暗号化される。

ユーザー認証

認証画面

認証画面も登録画面と同じで:passwardを拾えばよい。

認証処理

認証手順は以下の通り。

  1. ユーザー名などからデータベース上のユーザーを取り出してUserインスタンスを生成
  2. 生成したユーザーインスタンスのauthenticateメソッドにフォームパラメーターの:pasuwordの内容を与えて認証

認証の際に、@user&.authenticate...と「ぼっち演算子」を使っているのは、未登録ユーザーの場合にauthenticateメソッドがエラーとなるのを防ぐため。

実行結果

登録時

名前:山田、パスワード:yamadaとして登録した結果が以下の通り。

パラメーターで平文のパスワードが、インスタンス生成後にはダイジェストで保持されていて、この値がデータベースに保存される。

認証時

名前:山田、パスワード:yamadaとしてサインインした場合。

フォーム入力から生成されたUserインスタンスのpassword_digestが上の内容と等しくなっているのが確認できる。

また、authenticateの結果が正しい場合は、Userインスタンスが返されている。

異なるパスワードを入力した場合。

password_digestの値が異なっていて、authenticateの戻り値はfalse

存在しないユーザーを入力した場合。

 

Rails – 掲示板 – 投稿の削除

 

ルーティング

postsについてはresourcesで設定したため、削除用のルーティングも自動生成されている。

  • DELETEメソッド
  • URLは"/posts/:id"
  • コントローラーとアクションはposts#destroy
  • Prefixはpost(post_path)

ビュー

トップの投稿一覧中、削除アイコンのリンク先を設定する。

/posts/:id → post_path(post)

また、メソッドをdeleteで指定する。

top.html.erb

posts#destroyアクション

destroyアクションにデータの削除処理を記述。

  • params[:id]でURLパラメータの:idを取得
  • このidを使ってデータベースからPostデータを取得
  • Postデータ→PostImageデータから画像ファイルのパスを取得してファイルを削除
  • 取得したPostデータをdestroy(Postとそれに従属するPostImagesが連動して削除される)

なお、ブラウザー側でpointer-eventsが効かない場合も考慮して、DBから取得したPostuser_idとセッション中のユーザーのidが一致する場合のみ削除している。

また、画像ファイルのフルパスを得るためのヘルパーを別に準備している。

post_image_pathヘルパー

画像ファイルのオブジェクトpost_imageを与えて画像ファイルのフルパスを返すヘルパー。

もう一つのpost_image_urlはビューで使用するもので、それぞれパスの表現が違う。

 

Rails – 掲示板 – 投稿記事表示

概要

これまでのトップページは、仮にユーザーの一覧を表示していた。新規投稿機能が実装されたので、投稿記事を表示するようトップページを変更する。

  • 新しい記事から順に表示
  • 投稿ユーザー名を表示
  • メッセージと画像は存在する場合に表示
  • プレースホルダーとして”like it”、”設定”、”記事削除”のアイコンメニューを記事ごとに表示
  • 設定と削除については、他ユーザーの記事の場合はリンクが機能しないようにしてグレーアウト

pages#topアクション

topアクションにルーティングされると、すべての投稿記事をデータベースからインスタンス変数@postに読み込む(all)。読み込む順序は降順、すなわち新しい順(order("id desc"))。

app/controllers/pages.controller.rb

post_image_urlヘルパー

topビューで投稿画像を表示するのにpost_image_urlヘルパーを使っている。このヘルパーはPostに関して使うので、PostsHelperモジュールに書いている。

画像ファイルのパスについては画像ファイルの配置とパス指定を参照

app/helpers/posts_helper.rb

ヘルパー関数を含むモジュールをApplicationControllerでインクルードする。

app/controllers/application_controller.rb

topビュー

メニューアイコンにはCDNのFont Awesomeを利用している。サインイン中のユーザーが操作可能なメニュー以外を抑止するため、link_toに動的にクラスを設定している。

app/views/pages/top.html.erb

スタイル

リストの横並び画像のフィッティング・センタリングlink_toの動的なスタイル設定などを参照。

 

Rails – link_toの動的なスタイル

概要

Railsのヘルパーでクラスを動的に変更する方法。<%...%>を入れ子にすることができないが、文字列内の変数展開を使えば、変数の内容をクラスに設定できる。

コントローラー

コントローラーでインスタンス変数の配列を定義し、その内容によってlink_toのスタイルを変更する。

ビュー

インスタンス変数の配列の各要素を取り出しながら、

  • 変数availabilityenabled/disabledを設定
  • link_toのclass設定で変数availabilityを文字列展開

この結果、HTMLソースは以下の様になる。

スタイル

クラスに設定されるenabled/disabledに対してスタイルを設定する

実行結果

disableを設定したリンクの色が薄くなっている。また、このリンクを無効にしたので、クリックしてもコンソールに反応が出ない。

Rails – 掲示板 – 新規投稿機能

概要

新規投稿ページの枠組みをつくったので、これに投稿機能を実装する。

掲示板の第2段階へ

準備

投稿画像ディレクトリー

投稿画像ファイル保存用のディレクトリーを作成する。

public/post_images

ビュー

新規投稿のnewビューは再掲。

コントローラー

post_paramsヘルパー

ビューのフォームからパラメーターを受け取る際のヘルパーで、コントローラーで扱うパラメーターを許可している。

:upload_fileは直接保存しないので含めていない。

createアクション

フォーム入力を受けて投稿記事を生成・保存する本体。ファイルのアップロードについてはこちらを参照。

  • 記事を投稿したユーザーをデータベースから拾っている(L7)
  • メッセージか画像かどちらかがあれば投稿できる(L10-13)

app/controllers/posts_controllers.rb

メッセージと画像、メッセージのみ、画像のみの投稿後のデータベースの例は以下のとおり。

 

Rails – バリデーターのカスタマイズ

概要

モデルの検証を行うバリデーターを自作するのにいくつかの方法がある。

  • バリデーションメソッドをモデルで定義
  • ActiveModel::Validatorのサブクラスを定義
  • ActiveModel::EachValidatorのサブクラスを定義

バリデーションメソッド

検証したいモデルの中でカスタムメソッドを定義し、validate(単数形)でメソッドを呼び出す。

以下のコードは、:name属性と:email属性を持つUserモデルをカスタムメソッドにより検証する例。

 

  • カスタムメソッドname_or_email_presentsを定義
  • メソッドでは、:name:emailの両方とも空の場合にエラーを追加
  • モデルでvalidateによってメソッドをコールバックとして追加(validateは単数形)

ActiveModel::Validator

ActiveModel::EachValidator

 

Rails – 掲示板 – 新規投稿ページ

概要

サインインユーザーによる新規記事投稿のページを実装していく。

  • サインインユーザーがメニューアイコンをクリックして新規投稿ページへ
    • 新規投稿ページでは、メッセージを入力、画像ファイルを指定
    • サインインしていないユーザーに対してはアクセス制限
  • ここではresourcesによるRails標準のルーティングを使う
  • コントローラーはPostsController.rb

掲示板の第2段階へ

ルーティング

コントローラーに対応したpostsについてresourcesでルーティングを設定する。

config/routes.rb

resourcesにより設定されたルーティングは以下のとおり。

rails routes

コントローラー

PostControllerコントローラーを生成する。アクションについては、生成後のコントローラーファイルを編集することとして、ここでは枠組みだけを生成。

rails generate controller posts

生成されたコントローラーファイルにアクションを追加。

  • new
    • 他画面から遷移して投稿画面をレンダリング
    • ビューで扱うモデルはPostなので、@postUserのインスタンスをいれておく
  • create
    • 投稿画面からのPOSTにより処理が渡され、投稿記事の新規作成処理を行う

またアクション実行前にauthorizeをコールバック登録し、アクセス制限をかけている。

app/controllers/posts_controller.rb

ビュー

newアクションによりレンダリングされるページのコード。

メッセージ入力用のtext_areaとファイル選択用のfile_field、送信用のボタンを配置する。

views/posts/new.html.erb

スタイル

ビューに対する最低限のスタイルを設定。

app/assests/stylesheets/posts.scss

リンク設定

サインインユーザー用ヘッダーメニューのアイコンから記事投稿ページへのリンクを設定。

app/views/layouts/application.html.erb

 

Rails – 掲示板 – モデルとデータベースの準備

概要

第2段階の仕様のうちプロフィールに続いて記事投稿の機能を実装していく。

まず、記事投稿に必要なモデルとデータベースのテーブルを準備する。

  • Post:投稿記事のモデル
  • PostImage:投稿記事に付加可能な画像ファイルのモデル

手順の詳細は「モデル生成とマイグレーション」、「アソシエーション~基本」を参照

モデルの生成

Postモデル

以下のRailsコマンドでPostモデルを生成する。Userモデルを参照するため、user:referencesを指定。

生成されたモデルの内容を確認。references指定に基づいて、Railsによりbelongs_toが記述されている。

app/models/post.rb

モデルと同時に生成されるマイグレーションファイルを確認。ここでもusersテーブルを参照する外部キーが設定されている。

20210331121751_create_posts

PostImageモデル

以下のRailsコマンドでPostモデルを生成する。Postモデルを参照するため、post:referencesを指定。

生成されたモデルの内容を確認。references指定に基づいて、Railsによりbelongs_toが記述されている。

models/post_image.rb

モデルと同時に生成されるマイグレーションファイルを確認。ここでもpostsテーブルを参照する外部キーが設定されている。

20210331122545_create_post_images.rb

アソシエーションの設定

Userは複数の記事(Post)を投稿するため、Userモデルにhas_manyを追加する。

models/User.rb

 

1つの記事(Post)が複数の画像(PostImage)を持つことができるので、Postモデルにhas_manyを設定する。post_imagesと複数形になっている点に注意。

さらに、記事投稿時に画像ファイルも削除するよう、dependentも設定。

models/post.rb

post_image.rbPostに従属するが、Railsによってbelongs_toが設定済みなので編集不要。

マイグレート

以上の設定に基づいてマイグレーションを実行。その結果、postsテーブルとpost_imagesテーブルが生成される。

postsテーブル、post_imagesテーブルの構造を確認。

rails consoleでの確認

rails consoleでアソシエーションが機能しているか確認。

まず、Userに属するPostデータとPostに属するPostImageデータを作成。

テーブルに登録されたデータを確認。

このあとpost.destroyにより、postデータとPostImageデータが連動して削除される。