今日は二日分の学習のアウトプットとして、ログイン機能の実装までをまとめました。
ログイン機能を実装するにおいては
- 認証機能の仕組みの理解
- ユーザアカウント(ログイン情報)を新規保存する機能
- ログイン情報の入力フォーム
- ログインフォームに入力された値が保存されているアカウント情報と一致するか確認する機能
- ログイン状態のユーザ情報を取り出す方法
- ログイン中のみ使用できる機能
を実装していくという工程が必要になります。
何をしているかわからなくならないように、見出しにMVCを付記して進めていきます。
認証機能の仕組み
認証機能について理解するには、HTTPプロトコルについて理解する必要があります。
詳しくは【40日目】の記事に詳しくまとめているので、ここでは簡単にまとめます。
また上記記事はこちらの書籍を参考にしています。合わせてご確認ください。
セッションIDによる認証
ステートレスなプロトコルであるHTML
HTMLプロトコルはもともとWebアプリケーションでの使用を前提に作られた通信プロトコルではありませんでした。
なので基本的にHTMLによる通信は1回1回データを保持することができません。
1回目のHTML通信によってログイン認証したとしても、何の工夫もしなければ次の通信の時にはまたログインが必要になります。
つまり、毎回通信するたびにログイン情報を送ってあげる必要があります。
CookieとセッションIDによる認証
これを解決するためにあるのがCookieとセッション(ID)です。
Cookieとはブラウザ上に小さなデータを保存しておける機能です。
Cookieは接続先のURLごとに管理されており、同じサイトに接続する際に保存しておいたCookieを自動的にサーバに送信します。
セッションとは、ブラウザがWebアプリケーションに接続してから切断するまでの一連の通信のことです。
Webアプリケーションはこのセッションに対して1ブラウザに1つのセッションIDを発行します。
Webアプリケーション側ではセッションIDに対してログイン状態やユーザ情報を関連づけて保存しています。
そしてこのCookieにセッションIDを格納してやりとりすることで、同じサイトに接続した場合に、セッションIDを自動的にWebアプリケーションに送信し、Webアプリケーション側では受け取ったIDをデータベースと参照して、ログイン状態やユーザ情報を呼び出すことができます。
こうして、ステートレスな通信においても、ログイン状態を維持することができています。
具体的なセッションIDのやりとり
理屈が分かったところで、具体的な情報の受け渡しを整理しておきます。
1回目の通信
1回目の通信では、ブラウザからWebアプリケーションに認証情報を送信します。
サーバ側では送られてきた情報をもとにユーザを特定し、セッションIDを発行します。
ブラウザは受け取ったIDをCookieにセットします。
2回目の通信
2回目の通信ではブラウザは認証情報を送信しませんが、自動的にCookieにセッションIDを格納して送信します。
これは、POSTメソッドのリクエストヘッダ内で行われます。
セッションIDを受け取ったサーバはセッションIDに一意に関連づけられたユーザを探し、ユーザの情報を読み込みます。
セッションの他の利用方法
セッション情報は他にもflashメッセージや、買い物かごなど、ユーザー固有の情報を保存しておくために用いられます。
ユーザー認証機能の実装
Railsに備わっている「has_secure_password」の機能を用いる。
事前準備をしたのちに、model -> 掲示板一覧のview -> controller -> 詳細画面のview
事前準備
bcryptのインストール
gemfileを編集して
gem 'bcrypt', '~> 3.1.7'
docker-compose exec web bundle
でインストールします。
インストール後はコンテナを再起動します。
ユーザーアカウントを保存するためのモデルの作成
docker-compose exec web rails g model user name:string password_digest:string
# ユーザアカウントには認証に必要なname属性と、password_digest属性の2つのカラムを用意する。
# password_digestカラムには暗号化されたpasswordが保存される。
ユーザー情報の重複を許可しない設定
同じユーザー名とパスワードを持つ人が複数いたら大変なことなので、重複しないように設定する。
マイグレーションファイルの修正
xxxxx_create_users.rbを修正して、①ユーザ名とパスワードのnull false、②ユーザ名のユニークキーの設定をする。
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :password_digest, null: false
t.timestamps
end
add_index :users, :name, unique: true #usersのname属性をuniqueに制限
end
end
修正したらマイグレーションを実行してデータベースに反映させる。
セッションズコントローラーの作成
ユーザーのセッションの作成、削除を行うためにsessions_controller.rbを作成する。
セッションはそのデータ自体をブラウザ上に表示する必要はなく、あくまでデータの作成削除(=modelとのやりとりのみ)しか行わない。
=>--skip-template-engineオプションによってviewファイルを作成しないようにする。
docker-compose exec web rails g controller sessions create destroy --skip-template-engine
####簡単な構造について
- createアクションについて
- ログインが成功した場合、セッション情報にログインしたユーザーのIDを渡す。
- 次回以降の通信ではこのセッション情報のユーザIDから検索してログインしているか否かを判断する。
- destroyアクションについて
- セッション情報のユーザ IDを削除することでログアウトを実装する。
home_controllerの作成
homeとは、パスの指定のないURLにアクセスした場合に表示される画面のこと。
このコントローラーの機能としては、ホーム画面を表示するindexアクションだけがあればいい。
docker-compose exec web rails g controller home index
users_controller.rbの作成
一番肝心なコントローラーとなる。
docker-compose exec web rails g controller users new create me
####簡単な構造解説
生成するアクションは3つで、create, new, me
new:ユーザー作成フォームを生成する
createアクション: フォームから送信されたユーザーを作成
me: ログイン中のユーザーのマイページを公開する。
※ createアクションにviewはいらないので、該当するファイルは削除すればいい。
ルーティングの設定
ユーザ認証機能を作成する際に必要なルーティングの設定をする。
コントローラーを作成した際に自動的に作成されたルートがあるので、書き換えていく。
route設定の記法は
通信メソッド 'URL', to: 'コントローラー#アクション名'
ただし、usersのnewとcreateはresourcesで設定している。
Rails.application.routes.draw do
get 'mypage', to: 'users#me'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root 'home#index'
resources :users, only: %i[new create]
resources :boards
resources :comments, only: %i[create destroy]
end
userモデルの設定(model)
###パスワードの設定の仕組み
userモデルを編集して設定する。
has_secure_password
この記述によって、password属性とpasseord_confirmation属性がUserモデルに追加される。
passwordとpassword_confirmationに入力した2つのパスワードが一致している場合に、userモデルのpassword_digestカラムに暗号化されたpasswordが保存される仕組み。
バリデーションの設定
user名のバリデーション
userの認証情報として必要なバリデーションを設定する
validates :name, # user名についてのバリデーション
presence:true, # 入力必須
uniquness: true, # 重複禁止
length: { maximum: 16 }, #16文字以下
format: { # 正規表現の設定で、withに指定したパターンにマッチしたもののみ許可している。
with: /\A[a-z0-9]+\z/,
# \A 文頭、[a-z0-9] 小文字英数字のみ許可、\z 行末
message: 'は小文字英数字で入力してください。' エラーメッセージを指定
}
パスワードのバリデーション
validates :password,
length: { minimum: 8 }
ここでは最低文字数のバリデーションのみとしたが、ユーザ名同様正規表現を指定することもできる。
最低文字数を設定すると、presenceのバリデーションは自動的にtrueとなる。
エラーメッセージの日本語化
user:
name: ユーザー名
password: パスワード
password_confirmation: パスワード(確認)
configの中身を更新した場合は、コンテナを再起動する。
ユーザー登録フォームの作成(view)
ヘッダーメニューにユーザー登録のリンクを作成する。
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
ユーザー
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
# if文によって、ログイン中かどうかによって、メニューに表示する内容を切り替える。
<% if @current_user %> # この後インスタンス変数@current_userに、ログイン中のユーザーオブジェクトを格納するように実装する。
<%= link_to 'マイページ', mypage_path, class: 'dropdown-item' %>
<%= link_to 'ログアウト', logout_path, method: :delete, class: 'dropdown-item' %>
<% else %>
<%= link_to '登録', new_user_path, class: 'dropdown-item' %>
<%= link_to 'ログイン', root_path, class: 'dropdown-item' %>
<% end %>
</div>
ユーザコントローラーでアクションの設定(controller)
ここではユーザーモデルのオブジェクトをインスタンス変数とするだけの実装
インスタンス変数にすることで、モデルで作成されたオブジェクトをコントローラーで受け取って、viewに渡すことができる。
def new
@user = User.new
end
ユーザー新規登録のviewを実装
先ほど作成したnewアクションに対するviewを作成する。
viewの中身はformパーシャルで作成し、そこにインスタンス変数をローカル変数として渡すよう設定する。
<h1>ユーザー登録</h1>
<%# 詳細画面はformパーシャルで作成する。
formパーシャルにはuser_controller.rbで作成したインスタンス変数をローカル変数に渡す。 %>
<%= render partial: 'form', locals: { user: @user } %>
formパーシャルの作成
<%= render 'shared/error_messages' %>
<%# validationエラーの時に表示するエラーメッセージのsharedを設定。 %>
<%= form_for user do |f| %>
<%# ローカル変数userにはnew.html.erbでインスタンス変数@userを渡している。 %>
<div class="form-group">
<%# ユーザー名、パスワード、password_confirmationを入力するフォームの作成 %>
<%= f.label :name, 'ユーザー名' %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password, 'パスワード' %>
<%= f.password_field :password, class: 'form-control' %>
<%# password_fieldとすることで、入力内容が見えないようにできる。 %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, 'パスワード(確認)' %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<%= f.submit '作成', class: 'btn btn-primary' %>
<% end %>
ユーザー登録機能の実装
users_controller.rbの編集(controller)
####ストロングパラメータの設定
先ほど作成した登録フォームから送られてきたデータにフィルタを書けます。
private
def user_params
params.require(:user).permit(:name, :password, :password_confirmation)
end
createアクションを作成
フォームをサブミットした場合、createアクションが実行されることになっている。
def create
user = User.new(user_params) # ストロングパラメータでフィルタされたログイン情報のパラメータを渡して新しいuserオブジェエクトを作成
if user.save # saveメソッドによってログイン情報を保存する。
# if文を用いて、バリデーションによって保存できた場合とできなかった場合で分岐
# 保存できた場合
session[:user_id] = user.id # session情報のuser_idというキーに登録したuserのidを渡す(上で定義したuserオブジェクトのid
# session変数の任意のキーに値をセットすると、ページをまたいで値を保持できる。
redirect_to mypage_path # その後マイページに戻る
else
#保存できなかった場合
redirect_to :back, flash: { # 前の画面(フォーム作成画面)に戻る。ただしflashメッセージを保存する。
# flashメッセージとして保持する内容をハッシュ内に羅列(キー: 値,)
user: user, # このuserは上で定義したuserオブジェクトなので、入力されたuser, password, password_confirmationを含む
error_messages: user.errors.full_messages
}
end
end
newアクションの修正
バリデーションエラーでフォームに戻された時、flashメッセージで保存されたデータを渡すようにnewアクションを修正する
def new
@user = User.new(flash[:user])
end
デバッグ
meアクションに binding.pryを設定して、渡されているデータを確認する。
docker attachして、マイページを表示するとpryが起動するので、User.lastで最後に保存されたuser情報を見てみる
#binding.pry実行
=> #<User:0x00007efc758f3860
id: 2,
name: "aab",
password_digest:
"$2a$12$uNkFGy7a7TKWm7ni5L2t8ejPHnnL4l3w3B/wrhZd56Wc29R7vo2kq",
created_at: Sun, 13 Oct 2019 16:06:53 JST +09:00,
updated_at: Sun, 13 Oct 2019 16:06:53 JST +09:00>
password_digestには暗号化されたパスワードが入っているのがわかる。
※ここではどのように暗号化しているかは扱わない。
次に、session[:user_id]を確認するとこのように表示される。
# session[:user_id]
=> 2
確かにsessionがuser_idと一致しているのが確認できた。
ログイン、ログアウトの実装
home画面のviewを作成(view)
home画面にログインログアウトの画面を表示する
app/views/home/index.html.erb を編集する。
ここではログイン画面のパーシャルのrenderを記述するのみ。
<%= render partial: 'users/login_form' %>
次にapp/views/users/_login_form.html.erbにログインフォームの内容を作成。
<h1>ログイン</h1>
<%= form_for(:session, method: :post, url: login_path) do |f| %>
# 第一引数にモデルではなくsessionとしている
# するとユーザー名やパスワードのinput属性にsession[name]やsession[password]がつくようになる。
# これによってrails側ではparams[:session][:name]などによってそれぞれの情報が受け取れるようになる。
# 第二引数ではハッシュ形式でHTTPメソッドと接続先のURLが指定されている。
# RESTfulの考えに基づき、sessions_controller.rbのcreateアクションが呼び出される。
<div class="form-group">
<%= f.label :name, 'ユーザー名' %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password, 'パスワード' %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<%= f.submit 'ログイン', class: 'btn btn-primary' %>
<% end %>
ログイン機能の実装(controller)
上のコメントで書いたように、ホーム画面のログインフォームにsubmitすると、sessions_controller.rbに入力された情報が送られるのでcreateアクションを編集します。
def create
user = User.find_by(name: params[:session][:name])
# find_byは引数で指定した条件にマッチするものを1つ取得するメソッド
# ここではフォームに入力したuser名にマッチするnameを1つ取得している
# 受け取れる情報がない場合はnillになる
if user && user.authenticate(params[:session][:password]);
# &&は左辺が真なら右辺も評価するという意味
# authenticateメソッドは、引数から取得した値がuserオブジェクトの中身(入力したユーザ名)と一致するかを確認している。
# userモデルにhas_secure_passwordを記載したことで自動的に追加されるメソッド
# authenticateメソッドがuserオブジェクトを返せば認証成功、falseを返すと失敗でif分岐
session[:user_id] = user.id
redirect_to mypage_path
else
render 'home/index'
end
end
ログアウト機能の実装(controller)
ログアウトは、セッション情報内にあるuser_idを削除することで実現する
ログアウト後はホーム画面に戻ることとする。
def destroy
sessions.delete(:user_id)
redirect_to root_path
end
ログインユーザーの取得(controller)と特定のユーザーのみに表示されるviewの作成
どのページにいてもログイン中かどうかを判別できるようにしたい。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :current_user
# フィルタによってアクション実行前にcurrent_userメソッドを実行する
# ApplicationControllerクラスは全てのcontrollerで継承されているので、あらゆるアクションの前に呼び出される事になる。
# current_userメソッドはプライベートメソッドとして他のクラスからは実行できない。
private
def current_user
return unless session[:user_id]
# session情報の中にuser_idがない=nillの場合のみreturnが実行され、その戻り値nillが返されます
# returnが実行されるとメソッドを抜け出すので、以下の行は実行されない。
@current_user = User.find_by(id: session[:user_id])
# session情報の中にuser_idがある場合はこちらが実行されます。
# セッション情報にuser_idがあればそのidが@current_userに格納される。
# 全てのアクションの前で実行されるので、 事前に作成したheaderのメニューでは@current_userの有り無しで表示が変わるようにしている。
end
ヘッダーメニューのviewを確認しておく。
<% if @current_user %>
<%= link_to 'マイページ', mypage_path, class: 'dropdown-item' %>
<%= link_to 'ログアウト', logout_path, method: :delete, class: 'dropdown-item' %>
<% else %>
<%= link_to '登録', new_user_path, class: 'dropdown-item' %>
<%= link_to 'ログイン', root_path, class: 'dropdown-item' %>
<% end %>
同じようにセッションを用いてユーザー情報と結びつけることで、機能の使用やviewの表示に制限をかけることができる。
例)ログインユーザーのみ新規作成可能、掲示板作成者のみ編集可能など
ここでは、ログインユーザ名を表示するだけの簡単なマイページを作成する。
ログインユーザー用のマイページの作成(view)
<h1>マイページ</h1>
<%= @current_user.name %>
モデルでユーザーの詳細な情報をモデルで扱えるようにしておけば、名前や画像なども表示できる。