#一時cookieによるログイン機構
この記事ではログインの基本的な仕組みを実装する。ログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))である。
##セッション
HTTPはステートレス (Stateless) なプロトコルであり、リクエストの情報を全く保存出来ないので、ユーザーのブラウザに保存されるcookiesという小さなテキストデータにユーザーIDなどの情報を保存する。このcookieを用いて、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に設定する。今回はsessionというRailsのメソッドを使ってブラウザを閉じると破棄される一時セッションを作成する。
セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと合わせて理解しやすい。ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった風に。ただしUsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使うという違いがある。
###セッションコントローラ
ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。ログインのフォームは、newアクションで処理する。createアクションにPOSTリクエストを送信すると、実際にログインし、destroyアクションにDELETEリクエストを送信すると、ログアウトする。
$ rails generate controller Sessions new
まず、上のコードでセッションコントローラとnewアクションを作成する。このnewアクションに対応するビューでログインフォームのページを作成する。
Usersリソースのときは専用のresourcesメソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、以下のように「名前付きルーティング」だけを使う。
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
end
これで、getリクエストを/loginに発行すればセッションコントローラのnewアクションを呼び出せる(名前つきルートはlogin_path)などといった操作が実現される。
###ログインフォーム
コントローラとルーティングを定義したので、今度は新しいセッションで使うビュー、つまりログインフォームを整える。ログインフォームとユーザー登録フォームの違いは、4つあったフィールドが [Email] と [Password] の2つに減っていることだけである。
ユーザー登録フォームでform_forヘルパーを使い、ユーザーのインスタンス変数@userを引数にとっていた。
<%= form_for(@user) do |f| %>
.
.
.
<% end %>
しかし、セッションにはSessionモデルというものがなく(ユーザー登録においてはユーザーモデルが存在した)、そのため@userのようなインスタンス変数に相当するものもない。したがって、新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。
form_for(@user)
Railsではユーザーモデルがある場合は上のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定する、セッションの場合はリソースの名前とそれに対応するURLを以下のように具体的に指定する必要がある。
form_for(:session, url: login_path)
以上を踏まえると、ログインフォームのコードは以下のようになる。
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= 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 %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
ユーザーがすぐクリックできるように、ユーザー登録ページのリンクを追加してある。フォームに入力した値が、paramsハッシュのparams[:session][:email]とparams[:session][:password]となる。
なお、paramsは次のような入れ子ハッシュになっている。ハッシュの中にハッシュがある構造である。
{ session: { password: "foobar", email: "user@example.com" } }
###ユーザーの検索と検証
最初に、セッションコントローラーのcreateアクションを完成させていく。createアクションではユーザー認証に必要なあらゆる情報をparamsハッシュから取り出せる。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase) #フォームに入力したメールアドレスと同じメールアドレスをもつユーザーをデータベースから探す
if user && user.authenticate(params[:session][:password]) #フォームに入力したパスワードをハッシュ化したものと、先ほど探したユーザのpassword_digestと一致するかを確認する
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
# エラーメッセージを作成する
render 'new'
end
end
def destroy
end
end
入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ入力されたパスワードがそのユーザーのパスワードである場合のみ、if文がtrueになる。
##ログイン
この節では、cookiesを使った一時セッションでユーザーをログインできるようにする。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。
sessionを実装するには、Sessionsコントローラを生成した時点で既に自動生成されているセッション用ヘルパーモジュールを、Applicationコントローラにこのモジュールを読み込ませる必要がある。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
###log_in メソッド
Railsで事前定義済みのsessionメソッドを使って、単純なログインを行えるようにする (なお、これは先ほど生成したSessionsコントローラとは無関係である)。このsessionメソッドはハッシュのように扱えるので、次のように代入する。
session[:user_id] = user.id
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができる。sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了する。
同じログイン手法を様々な場所で使い回せるようにするために、Sessionsヘルパーにlog_inという名前のメソッドを定義する。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
sessionメソッドで作成した一時cookiesは自動的に暗号化され、上のコードは保護される。そして重要な事には、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできない。ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当せず。cookiesメソッドで作成した「永続的セッション」には当てはまらない。
以下にlog_inメソッドを含むcreateアクションを示す。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
このコードにより、ユーザーが認証に成功すると、log_inメソッドにより一時セッションにユーザーIDが保存されログインし、ユーザーのプロフィールページにリダイレクトされる。
###現在のユーザー
ここでは、current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。current_userメソッドの目的は、次のようなコードを書けるようにすることである。
<%= current_user.name %>
また、次のようなコードで、ユーザーのプロフィールページに簡単にリダイレクトできるようにもしたい。
redirect_to current_user
セッションヘルパーのcurrent_userメソッドは次のようになる
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])
end
end
end
セッションにユーザーIDが保存されている場合、session[:user_id]に対応するidをもつユーザーをデータベースから探し、@current_userに代入する。
###レイアウトリンクを変更する
ログイン機構の最初の効用として、ログイン時と非ログイン時でレイアウトを変更してみる。下の図のように、ログイン時は「ユーザー一覧」リンク、「プロフィール表示」リンク、「ユーザー設定」リンク、「ログアウト」リンクを追加する。ユーザー設定のリンクとプロフィールのリンク、ログアウトのリンクは [Account] メニューの項目として表示されている点に注意する。
ログインしているかどうかを判定するためのlogged_in?メソッドをヘルパーファイルに追加する。
module SessionsHelper
.
.
.
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
end
これでユーザーのログイン時にレイアウトを変えられるようにする準備が整った。レイアウトのヘッダーパーシャルは次のようになる。
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
レイアウトに新しいリンクを追加したので、Bootstrapのドロップダウンメニュー機能を適用できる状態になった。具体的には、Bootstrapに含まれるCSSのdropdownクラスやdropdown-menuなどを使っている。これらのドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示する。
//= require jquery
//= require bootstrap
###ユーザー登録時にログイン
以上で認証システムが動作するようになったが、今のままでは、登録の終わったユーザーがデフォルトではログインしていないので、ユーザーがとまどう可能性がある。ユーザー登録が終わってからユーザーに手動ログインを促すと、ユーザーに余分な手順を強いることになるので、ユーザー登録中にログインを済ませておくことにする。ユーザー登録中にログインするには、Usersコントローラのcreateアクションにlog_inを追加するだけで良い。
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
##ログアウト
このアプリケーションで扱う認証モデルでは、ユーザーが明示的にログアウトするまではログイン状態を保てなくてはならない。この節では、そのために必要なログアウト機能を追加する。これまで、SessionsコントローラのアクションはRESTfulルールに従っていた。newでログインページを表示し、createでログインを完了するといった具合に。セッションを破棄するdestroyアクションも、引き続き同じ要領で作成する。
ログアウトの処理では、log_inメソッドの実行結果を取り消す。つまり、セッションからユーザーIDを削除する。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id #セッションからユーザーIDを削除
end
.
.
.
# 現在のユーザーをログアウトする
def log_out
session.delete(:user_id)
@current_user = nil
end
end
ここで定義したlog_outメソッドは、Sessionsコントローラのdestroyアクションでも同様に使っていく。
class SessionsController < ApplicationController
.
.
.
def destroy
log_out
redirect_to root_url
end
end