はじめに
railsチュートリアルを一周し、勉強のためアプリを作成した際の、ログイン機能の備忘録です。
内容はrailsチュートリアル6章〜8章ででてくる内容がほとんどになります。
仕様
- ログイン機能にdeviseは使用しない
- railsチュートリアル6章〜8章の内容
- CSSにbootstrap使用
- ruby 3.0.0 Rails 6.0.3.4
usersテーブル
| カラム名 | 型 |
|---|---|
| id | integer |
| name | string |
| string | |
| password_digest | string |
integer:整数
string:文字列が255字以内
バリデーション
- nameは空白禁止、50文字以内
- emailは空白禁止、255文字以内、かぶりなし、フォーマットはxxx@xxx.xxx
- passwordは空白禁止、6文字以上
usersコントローラー、モデル作成
コントローラーは複数形、モデルは単数形で表記(railsの慣習)
コントローラーの作成
$ rails generate controller Users
モデルの作成
$ rails generate model User name:string email:string
$ rails db:migrate
user.rbに以下を追記
validates :name, presence: true, #空白禁止
length: { maximum: 50 } #50文字以内
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i #大文字小文字無視のxxx@xxx.xxxの正規表現
validates :email, presence: true, #空白禁止
length: { maximum: 255 }, #255文字以内
format: { with: VALID_EMAIL_REGEX }, #フォーマット指定
uniqueness:true #一意性
パスワードのハッシュ化
パスワードをそのままDBに保存するのはセキュリティの観点よりNG
そのため、ハッシュ化しDBに保存できる状態にする
ハッシュ化:不可逆性の文字列に変換すること
usersテーブルにpassword_digest(string型)を追加
マイグレーション名は自由に設定可能だが、to_usersにすることで、usersテーブルを参照
(今回のマイグレーション名:add_password_digest_to_users)
$ rails generate migration add_password_digest_to_users password_digest:string
$ rails db:migrate
ハッシュ関数であるbcryptを使用するため、gemを追加
gem 'bcrypt' #追記
$ bundle install
下記3点の機能を持たせるためにuser.rbにhas_secure_password追記
- セキュアにハッシュ化されたパスワードをpassword_digestに保存 (セキュア:安全に)
- 仮想的にpasswordとpassword_confirmationが使える(存在性と値が一致するかのバリデーションも追加)
- authenticateメソッド追加(引数とパスワードの比較 一致→Userオブジェクト 間違い→false)
また、パスワードのバリデーションも追記
※has_secure_passwordがもつバリデーションは新規製作時のみ適応、更新時は不適応のため
has_secure_password
validates :password, presence: true, length: { minimum: 6 } #空白禁止かつ6文字以上
コンソールにてハッシュ化されているか確認
$ rails console --sandbox
user1 = User.create(name: "qqq", email: "q@q.q", password:"password", password_confirmation: "password")
user2 = User.create(name: "www", email: "w@w.w", password:"password", password_confirmation: "password")
user1.password_digest
=> "$2a$12$poMxe7FIiQpZISIO51XanuJgDkwYD45hCKZHt0.M0Qx1vW5jcFYm."
user2.password_digest
=> "$2a$12$Ld2mRJgw3LVItTgfn2k8LuiRPH.RHkV71.wbfzfdxDT2p5pFgvkva"
パスワードは同じだが、password_digestの値が違うのは、
簡単に言えば、bcryptが引数+ランダムな値を元に文字列を生成しているから
ユーザー情報の表示
/users/1を開くことで、id=1のユーザー情報(名前、eメール)を画面表示出来るようにする
コンソール上でユーザーの追加(現段階ではユーザー情報が無いため)
$ rails console
User.create(name: "テスト太郎", email: "test@test.com", password:"password", password_confirmation: "password")
=> #<User id: 1, name: "テスト太郎", email: "test@test.com", created_at: "2021-02-14 13:11:00", updated_at: "2021-02-14 13:11:00", password_digest: [FILTERED]>
RESTに基づくルーティングの作成
resources :users
# resources :users, only: [:index, :show] ←必要なものだけを指定する場合
# resources :users, expect: [:index, :show] ←不必要なものを指定する場合
個人ページの作成、表示はnameとemailのみ
名前:<%= @user.name %><br>
Eメール:<%= @user.email %>
showコントローラの記入
User.find(params[:id])と記入することで、送られてきたurl(/users/1)よりid=1を取得できる
(resourcesを記入したことで、/users/:idのurlが送られてきた場合、showを実行する設定のため)
def show
@user = User.find(params[:id])
end
以上で、/users/1にid=1の名前とEメールが表示される
登録フォーム作成
/users/newを開くことで、新規ユーザー登録が出来る状態にする
新規登録フォームには、新規Userオブジェクトを与えないといけないため
newコントローラで与えれるように記入する
def new
@user = User.new
end
form_withヘルパーメソッドを用いて、投稿フォームを作成する
<div class="row">
<div class="col-md-6 col-md-offset-3"> <!--12分割の4−9部分で描画-->
<%= form_with(model: @user, local: true) do |f| %>
<%= f.label :name, "名前" %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email, "Eメール" %>
<%= f.text_field :email, class: 'form-control' %>
<%= f.label :password, "パスワード" %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "パスワードの再確認" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "ユーザー登録", class: "btn btn-primary" %>
<% end %>
</div>
</div>
フォームで記入した情報をDBに登録する
user_params:フォームから送られてきた情報以外の情報を受け取らない処理
(外部から隠したいため、private内に記載)
def create
@user = User.new(user_params) #@userにフォームで記入したユーザー情報を与える
if @user.save #saveの実行
redirect_to @user #save成功時user_url(@user)にいく
else
render 'new'#save失敗時、フォーム画面を再描画する
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
登録失敗時、フォームにエラー文を日本語で表示させる
各フィールド毎のエラー文を、フィールド近くに表示させる状態にする
日本語化に必要なgemのインストール
gem 'rails-i18n'
$ bundle install
config/application.rbに以下を追記
config.i18n.default_locale = :jack_o_lantern:
カラム名はgemを入れただけでは日本語にはならないため、
config/localesにja.ymlを新規作成し、以下を記載
$ touch config/locales/ja.yml
ja:
activerecord:
attributes:
user:
name: 名前
email: Eメール
password: パスワード
password_confirmation: パスワードの再確認
new.html.erbにエラーメッセージを追記
(エラーメッセージはテンプレート化 f.objectで@userを取得可能)
<%= f.label :name, "名前" %>
<%= render 'shared/error_messages', object: f.object, column: :name %> <!--追記-->
<%= f.text_field :name, class: 'form-control' %>
<% if object.errors.include?(column) %>
<span style="color: red;"><%= object.errors.full_messages_for(column).first %></span>
<% end %>
登録成功時、成功したことを表示させる
createアクションにsave成功時のflashを追記する
def create
@user = User.new(user_params)
if @user.save
flash[:success] = "新規登録が成功しました" #追記
redirect_to @user
else
render 'new'
end
end
flashメッセージが存在する際場合にどのページでも表示されるように、
application.html.erbに以下を記載
<main>
<div class="container">
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
<%= yield %>
</div>
</main>
ログインページの作成
新たにsessioonsコントローラ、newアクションを生成する
$ rails generate controller Sessions new
ルーティングの追加
ログインフォーム→new ログイン処理→create ログアウト処理→destroy
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
/loginページの作成
sessionはモデルを持たないため、送信先urlとscopeをform_withで与えている
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(url: login_path, scope: :session, local: true) do |f| %>
<%= f.label :email, "Eメール" %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password, "パスワード" %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
</div>
</div>
コントローラ記入前に、以下の機能を使い回せるようにSessionsヘルパーに記入
①sessionヘルパーを用いて、渡されたユーザーを与えることでログイン状態にするメソッド
※sessionを用いると、一時cookiesに暗号化された状態で保存可能
②session[:user_id]にuser.idが保存されたいた場合、そのユーザーを返すメソッド
※1リクエストに2回以上このメソッドが呼び出されたとき、
データベースの問い合わせが2回以上発生してしまうため、
インスタンス変数を用いて2回目以降はインスタンス変数の中身を参照されるようにする
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] =user.id
end
# 現在ログインユーザーがいる場合、ユーザーを返す
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
#@current_user = @current_user || User.find_by(id: session[:user_id])と同義
#find(session[:user_id])では、データがない場合に例外を返してしまう(find_byはnilを返す)
end
end
コントローラはそのままではヘルパーは使えないため、以下を記述する
(viewファイルで使う場合は記載不要、modelで使いたい場合は記載必要)
class ApplicationController < ActionController::Base
include SessionsHelper
end
createコントローラの記入
sessionモデルが無いことよりエラーメッセージが生成されないため、
flash.nowを用いて、ログイン失敗を表現する
(render→flash.now ※現在のリクエストまで残る redirect_to→flash ※次のリクエストまで残る)
def create
user = User.find_by(email: params[:session][:email])
#{ session: { password: "xxx", email: "xxx" } }のemailを取得
if user && user.authenticate(params[:session][:password])
#ユーザーがデータベースに存在し、パスワードの認証もok時の処理
log_in user
redirect_to user
else
flash.now[:danger] = 'Eメールとパスワードの組み合わせが無効です'
render 'new'
end
end
ログイン時と非ログイン時で表示を変更する方法
header部リンク先などを、ログインしている状態か否かで表示内容を変更できる状態にする
Sessionsヘルパーに、current_userがnilではない事で、ログイン状態か否かを判断するメソッドを追加
# ユーザーがログイン状態か否かの確認
def logged_in?
!current_user.nil?
end
end
viewファイルなどにifを用いてlogged_in?を使用し、表示内容を変更
<% if logged_in? %>
<li><%= link_to "Log out", logout_path, method: :delete %></li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
ユーザー登録時にログイン状態にする
Sessionsヘルパーに記入したlog_in(user)をuserコントローラのcreateアクションに追記
def create
@user = User.new(user_params)
if @user.save
log_in @user #追加
flash[:success] = "新規登録が成功しました"
redirect_to @user #user_url(@user)→@userで表現している
else
render 'new'
end
end
ログアウト処理
session(:user_id)の中身を消すことでログアウトを行える状態にする
Sessionヘルパーにsession(:user_id)をnilにするlog_outメソッドを記載
def log_out
session.delete(:user_id)
@current_user = nil
end
コントローラのdestroyアクションにlog_outを記載
ログアウト後はルートパスへ飛ぶようにする
def destroy
log_out
redirect_to root_url
end