はじめに
第7回目となる今回は、モデルにセキュアなパスワードをもたらしてくれるhas_secure_password
メソッドの使い方を紹介していきます。
パスワードは他人に知られてはまずいものです。万が一、データを抜かれたり画面に表示されちゃったりしても一目でわからないようになっていることが望ましいですね。そんなことを実現してくれるメソッドがhas_secure_password
です。
前回のソースコード
前回のソースコードはこちらに格納してます。今回のだけやりたい場合はこちらからダウンロードしてください。
has_secure_password
Railsでは、モデルにセキュアなパスワード属性を実装するメソッドとして、has_secure_password
が用意されています。モデルに対してhas_secure_password
を適用することでモデルは以下の恩恵を受けることができます。
- 仮想属性として
password
とpassword_confirmation
を利用可能 -
password
とpassword_confirmation
の同一性について勝手に検証してくれる - DBにはハッシュ化した
password_digest
を保存するようになる - ハッシュ値でパスワード検証をする
authenticate
メソッドを利用可能
ハッシュ化についてちょっとお話ししておきます。ハッシュ化はある文字列を不可逆な別の文字列に変換してくれます。『不可逆』とは元に戻せないという意味です。
『暗号化』の場合は『復号化』することで元の文字列に変換しなおせるんです。これは『可逆』といいますね。
では、早速モデルにhas_secure_password
メソッドを適用していきます。
has_secure_password
ではハッシュ化を行うためにbcrypt
gemを利用します。まずは、bcrypt
のインストールからやっていきます。Gemfile
の中身をみるとわかりますが、bcrypt
は最初からGemfile
の中に書かれておりコメントアウトされているだけです。なのでコメントアウトをとってdocker-compose build
をするだけでOKです。
...
# Use Active Model has_secure_password
- # gem 'bcrypt', '~> 3.1.7'
+ gem 'bcrypt', '~> 3.1.7'
...
$ docker-compose build
ビルド後はコンテナを起動してコンテナ内に入っておきます。
$ docker-compose up -d
$ docker-compose exec web ash
次に、has_secure_password
メソッドを使うようになるとpassword
をハッシュ化した値をpassword_digest
カラムにDB保存するようになります。マイグレーションファイルを作成しDBにpassword_digest
カラムを追加しておきます。
# rails g migration add_password_digest_column_to_user password_digest:string
Running via Spring preloader in process 251
invoke active_record
create db/migrate/20200130075100_add_password_digest_column_to_user.rb
# rails db:migrate
== 20200130075100 AddPasswordDigestColumnToUser: migrating ====================
-- add_column(:users, :password_digest, :string)
-> 0.0522s
== 20200130075100 AddPasswordDigestColumnToUser: migrated (0.0533s) ===========
そして、Userモデルにhas_secure_passwordメソッドを適用します。
class User < ApplicationRecord
before_save { self.email = email.downcase }
+ has_secure_password
validates :name,
presence: true,
length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email,
presence: true,
length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
これで完了です!簡単ですね!
では恩恵がちゃんと受けられているか確認してみましょう。
password
とpassword_confirmation
まずはpassword
属性とpassword_confirmation
属性の二つの仮想属性が利用できることを確認してみます。
# rails c
> user = User.new(name: "hanako", email: "hanako@sample.com", password: "password", password_confirmation: "password")
=> #<User:0x00005610de139120
id: nil,
name: "hanako",
email: "hanako@sample.com",
created_at: nil,
updated_at: nil,
password_digest: [FILTERED]>
> user.password
=> "password"
> user.password_confirmation
=> "password"
> user.password_digest
=> "$2a$12$KWcqXmTL6sVj0ch127o0.eemXFmAgPRqU98D5CLqXNJ8.5F.10AiS"
DBカラム的にpassword_digest
しかないはずですが、password
、password_confirmation
が使えていることがわかります。さらにその結果がpassword_digest
として利用可能なのもわかりますね。
password
とpassword_confirmation
の一致性確認
ここで一度valid?
メソッドを使ってみましょう。これは今現在のモデルでvalidationを突破できるのかを確認できるメソッドです。先ほどまではsave
でvalidationできるかを確認していましたが、実はvalidationを調べるためだけであればこれでOK。
> user.valid?
User Exists? (6.5ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
=> true
今はpassword
とpassword_confirmation
がマッチしているのでvalidationは通っているようです。では、password_confirmation
を別の文字列に変更した場合どうでしょうか?
> user.password_confirmation = "password1"
=> "password1"
> user.valid?
User Exists? (2.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
=> false
> user.errors.full_messages
=> ["Password confirmationとPasswordの入力が一致しません"]
validationでエラーになっていることがわかりましたね...あ、日本語化していない。
日本語化しましょう!
ja:
activerecord:
attributes:
user:
name: "お名前"
email: "メールアドレス"
+ password: "パスワード"
+ password_confirmation: "確認用パスワード"
errors:
...
> reload!
Reloading...
=> true
> user.valid?
User Exists? (2.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
=> false
> user.errors.full_messages
=> ["確認用パスワードとパスワードの入力が一致しません"]
password
とpassword_confirmation
が一致していない場合はエラーが起きていますね。
ただ、password_confirmation
のようなものは必要なのか?という論争もあると思います。
「登録フォームにおけるパスワード確認用の入力欄は必要か | UX MILK」
例えばこちらの記事では、確認用パスワードを用意するのではなく、パスワードの欄のマスクを外せるようにした方がコンバージョンが上がると述べられていたりします。
has_secure_password
では、password_confirmation
がnil
の場合、password
とpassword_confirmation
の一致を確認しません。
> user.password_confirmation = nil
=> nil
> user.valid?
User Exists? (3.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
=> true
なので確認用パスワードを使う使わないによらず、has_secure_password
メソッドは強力なのです。
DBではハッシュ化されたpassword_digest
が使われる
最初にもお話しした通り、ハッシュ化は不可逆なデータ変換です。
has_secure_password
ではpassword_digest
にハッシュ化されたpassword
を保存します。それを確認してみましょう!
実際にUserを作成してみましょう。
> user.save
(0.4ms) BEGIN
User Exists? (2.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
User Create (8.5ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["name", "hanako"], ["email", "hanako@sample.com"], ["created_at", "2020-03-09 14:37:38.358397"], ["updated_at", "2020-03-09 14:37:38.358397"], ["password_digest", "$2a$12$KWcqXmTL6sVj0ch127o0.eemXFmAgPRqU98D5CLqXNJ8.5F.10AiS"]]
(2.3ms) COMMIT
=> true
> user = User.find(1)
User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
=> #<User:0x00005610e0b21ed8
id: 1,
name: "hanako",
email: "hanako@sample.com",
created_at: Mon, 09 Mar 2020 14:37:38 JST +09:00,
updated_at: Mon, 09 Mar 2020 14:37:38 JST +09:00,
password_digest: [FILTERED]>
> user.password
=> nil
> user.password_digest
=> "$2a$12$KWcqXmTL6sVj0ch127o0.eemXFmAgPRqU98D5CLqXNJ8.5F.10AiS"
user.password_digest
をみてもなんのことやらわかりませんね。ちょっと安心です。
パスワード認証するauthenticate
メソッド
さて、ハッシュ化されてセキュアになったのはいいのですが、不可逆なのでこのままではパスワードで認証ができません。has_secure_password
ではハッシュ化されたパスワードの認証をするためのauthenticate
メソッドが用意されています。これは、password_digest
の値と入力したpassword
を同じハッシュ関数でハッシュ化した値の一致をチェックしてくれるものです。
> User.find_by(email: "hanako@sample.com").authenticate("a")
User Load (3.9ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
=> false
> User.find_by(email: "hanako@sample.com").authenticate("password")
User Load (2.5ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "hanako@sample.com"], ["LIMIT", 1]]
=> #<User:0x00005610de34c250
id: 1,
name: "hanako",
email: "hanako@sample.com",
created_at: Mon, 09 Mar 2020 14:37:38 JST +09:00,
updated_at: Mon, 09 Mar 2020 14:37:38 JST +09:00,
password_digest: [FILTERED]>
このようにfind_by
と組み合わせて使えばメールアドレスとパスワードの2key認証を実装できます。authenticate
は不一致の場合はfalse
を、一致の場合はモデルオブジェクトを返却します。
password
にvalidationを設ける
仮想属性であるpassword
に対してもvalidationをつけることができます。
一方で、has_secure_password
で作られたpassword
にはデフォルトでpresence
のvalidationが設定されています。これに加えて6文字以上でないといけない文字数制限をつけてみましょう。
デフォルトでpresence
がついてはいるのですが、入力がない場合にpresence
とlength
の両方に引っかかってしまうのでpresence
については適用しない方がわかりやすいでしょう。
has_secure_password
にvalidations: false
オプションをつけてデフォルトのpresence
validationを向こうにした後に、他の属性と同様にlength
のvalidationをつけてみましょう。
class User < ApplicationRecord
before_save { self.email = email.downcase }
- has_secure_password
+ has_secure_password validations: false
validates :name,
presence: true,
length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email,
presence: true,
length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
+
+ validates :password,
+ length: { minimum: 6 }
end
password
のvalidationについて試してみましょう。
> reload!
Reloading...
=> true
> user = User.new(name: "john", email: "john@sample.com")
=> #<User:0x00005610deba8110
id: nil,
name: "john",
email: "john@sample.com",
created_at: nil,
updated_at: nil,
password_digest: nil>
> user.valid?
User Exists? (4.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "john@sample.com"], ["LIMIT", 1]]
=> false
> user.errors.full_messages
=> ["パスワードは6文字以上で入力してください"]
password
にnil
だとバリデーションにひっかかります。
> user.password = "a" * 5
=> "aaaaa"
> user.valid?
User Exists? (4.6ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "john@sample.com"], ["LIMIT", 1]]
=> false
> user.errors.full_messages
=> ["パスワードは6文字以上で入力してください"]
password
が5文字以下でも同様にバリデーションに引っかかります。
> user.password = "a" * 6
=> "aaaaaa"
> user.valid?
User Exists? (3.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "john@sample.com"], ["LIMIT", 1]]
=> true
ということで、password
のlength
validationが正しく挙動していることがわかりました。
Userを作成
最後にこのバリデーションの中でちゃんとUserを作成できることを確認しておきましょう!
> User.create(name: "John Smith", email: "john@sample.com", password: "password")
(0.8ms) BEGIN
User Exists? (4.0ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "john@sample.com"], ["LIMIT", 1]]
User Create (1.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["name", "John Smith"], ["email", "john@sample.com"], ["created_at", "2020-03-09 14:44:33.407908"], ["updated_at", "2020-03-09 14:44:33.407908"], ["password_digest", "$2a$12$FTRT7cRJ4c0k.H68k49yOOurmwSbGaRHwzOXLuP2LYyXD30sg6FDS"]]
(1.9ms) COMMIT
=> #<User:0x00005610e06db9a0
id: 2,
name: "John Smith",
email: "john@sample.com",
created_at: Mon, 09 Mar 2020 14:44:33 JST +09:00,
updated_at: Mon, 09 Mar 2020 14:44:33 JST +09:00,
password_digest: [FILTERED]>
validationにひっかからないUserであればちゃんと作成できることが確認できましたね!
ユーザー情報を確認するViewを作ってみる
ここまででUserモデルがほぼ完成しました。この情報をUIで見れるようにViewを作ってみましょう!
Scaffoldを思い出してください。ユーザーの詳細情報を見るためのページは/users/:id
のURLにアクセスして閲覧することができ、show
アクションにルーティングされていました。
今回もそれにそってユーザーのページを作っていきます。
ユーザー詳細ページを作成する
ユーザー詳細ページは、/users/:id
のパスにアクセスした時に、そのid
のユーザーの詳細情報が表示されるページにしたいと思います。
この通りになるように、ファイルの編集や作成を行っていきます。いままでrails g ~
コマンドでファイルを作成してきましたが、Railsの規則に則っていれば普通にファイルを作成しても動きます。
Routing
まずは、/users/:id
のルーティングを生成します。以前、Scaffoldの時にresources
メソッドを使ってUsersコントローラーに対するルーティングを定義しました。/users/:id
はその時のshow
アクションへのルーティングと同じです。
resources
メソッドはonly
オプションをつけることで特定のアクションへのルーティングのみを定義してくれます。今回はこのオプションを使ってルーティングを定義します。
Rails.application.routes.draw do
root 'static_pages#home'
+
+ resources :users, only: [:show]
end
これでルーティングの設定は完了です。
今rails console
を起動していると思うのでshow-routes
コマンドでルートを確認してみましょう。
> show-routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
user GET /users/:id(.:format) users#show
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index
POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create
new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new
edit_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) rails/conductor/action_mailbox/inbound_emails#edit
rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show
PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy
rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create
rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
色々と出てくると思いますが、とりあえず重要なのは最初の2つのルートです。今、config/routes.rb
で定義しているルーティングが表示されていることがわかると思います。
同様のことをrails routes
コマンドでも確認できます。
> quit
# rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
user GET /users/:id(.:format) users#show
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index
POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create
new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new
edit_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) rails/conductor/action_mailbox/inbound_emails#edit
rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show
PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy
rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create
rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
Controller
次にUsersコントローラーを作成していきます。コントローラーは複数形の名称にするのがルールです。
# touch app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
end
users
コントローラにshow
アクションがある、というところまでは今までの内容からわかりますね。
show
アクションの中身をみてみます。
params[:id]
は/users/:id
のパスパラメータの:id
を取得しています。例えば/users/1
にアクセスした場合はparams[:id]=1
だし、users/2
にアクセスした場合はparams[:id]=2
です。
User.find()
はModel CRUDの回で話した通り、id
でUserのモデルオブジェクトを取得するメソッドなので、パスのid
のユーザーがこれで取得できるわけです。
あとはViewに引き渡すためにこれをインスタンス変数@user
に代入しています。
ではこれを受け取るViewを作っていきます。
View
まず、Viewファイルを格納するディレクトリを作成して、その中にshow.html.erb
ファイルを作ります。
UsersコントローラのためのViewなのでapp/views/users/
ディレクトリ内にファイルを作成していきます。
# mkdir app/views/users
# touch app/views/users/show.html.erb
最初は情報が表示されていればOKとしましょう。特にUIは拘らない。
<div class="container my-5">
<%= @user.name %>
<br>
<%= @user.email %>
</div>
name
とemail
を表示。
http://localhost:3000/users/2
にアクセスしてみましょう。
こんなページが表示されていれば成功です!
(id
は実際にDBに格納されているデータ次第ですので、Rails consoleでUser.all
を打つなどして存在するUserのid
を確認してみてくださいね。)
後片付け
次回に向けてデータを消しておきます。
# exit
$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset
DBコンテナが立ち上がった状態だと思うのでdownさせます。
$ docker-compose down
まとめ
今回は、has_secure_password
メソッドを使ってセキュアなパスワードが利用できるようになりました。
コード的にいえばたった1行has_secure_password
を付け加えるだけでパスワードをハッシュ化してセキュアに扱うことができるようになる。これって強力なメソッドですよね。
さらにユーザーの情報を表示するページまで作ることができました。
次回はSign up(ユーザー登録)ページを作っていきましょう!
では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
Next: コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.8 - Sign up - - Qiita
本日のソースコード
Reference
- Ruby on Rails チュートリアル:実例を使って Rails を学ぼう
- 登録フォームにおけるパスワード確認用の入力欄は必要か | UX MILK
- 「ハッシュ」とは何なのか、必ず理解させます | ALIS