###この記事は?
Ruby on Railsで二重認証をかける話です。
###二重認証?
Basic認証とユーザー認証の二段構えの認証を行いたい、みたいな話です。
Basic認証とユーザー認証(フォームによる認証)の違いについては、こちらを参考にするとよいのではないかと思います。
###必要性
身内のみで使うことを想定したWebアプリなんかで、Basic認証で身内以外の人を弾いてフォームによる認証で個人を識別したい、というときに使うことを考えています。
Webサイトに最初にアクセスした時に
のような認証を出してこれを通過した場合には、
のようなフォームによる認証を出し両方を通過した場合のみWebアプリケーションを利用できる、という仕組みを作りたいと思います。
###アプリケーション構成
実験的なアプリケーションとして最低限必要なものはUserデータベースとそれを操作する足場(Scaffold)、そしてログイン認証のコントローラとそのビューと言ったところでしょうか?
まずは、足場を作ってしまいます。
$ rails g scaffold user name:string password_digest:string
今回はログイン機能に最低限必要なコラムだけ作りました。続いて、セキュアな暗号化を提供してくれるgemをインストールします。Gemfileを開いて、'bcrypt'にかかっているコメントアウトを外して保存し
$ bundle install
とします。次に、ログイン認証のコントローラを作ります。
$ rails g controller login
これでひとまずの準備は整ったので、ファイルをいじっていきます。membersモデルのscaffoldingによってアクセス可能なアクションとして、
'users#index'、'users#show'、'users#edit'、'users#new'が追加されているはずです。これらのうち'user#new'以外にアクセスしようとした場合にはbasic認証とユーザー認証がかかるようにします。
###Basic認証
RailsでBasic認証を利用するのはとても簡単です。今回は特定のグループ所属のユーザー以外を弾きたい、という場合を想定しているのでグループに所属していない人にはそもそもアクセスできないようにしたいので、Applicationコントローラー内にフィルターとして実装します。全機能に制限をかけたいのでonly/exceptオプションはなしです。このとき、ユーザー認証に関しても予めフィルタとして書いておきます。basicフィルタがBasic認証に関して、check_loginedが二段階目のユーザー認証に関してのアクションです。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :basic, :check_logined
private
def basic
name = 'hogehoge'
passwd = '39975bb0ba31825c4fdd3de775dd468081b3522b'
authenticate_or_request_with_http_basic('BA') do |n, p|
n == name && Digest::SHA1.hexdigest(p) == passwd
end
end
end
basicフィルターはそのブラウザで初めてアクセスした際にBasic認証を行い、ブラウザを閉じるまでは認証を維持します。
上の例ではユーザ名に'hogehoge'、パスワードに'fugafuga'を入れることで認証されます。なお、パスワードはハッシュ化されたものをコード内に記述しましたが、自分でパスワードを設定してハッシュ化したものを記述したい場合は、
$ rails console
でrailsの対話環境を開き
irb(main)> Digest::SHA1.hexdigest('パスワード化したい文字列')
を実行するとハッシュ化された文字列が帰ってくるので、それをソース内に記述すると良いでしょう。なお、ハッシュ関数はこの他にもいろいろ利用できるようです。
###フォームによるユーザー認証
フォームによる認証の場合、ユーザーの情報を記録したデータベースが必要になりますがこれは先程作成しました。
まずは、Applicationコントローラー内のcheck_loginedフィルタを定義してしまいます。
class ApplicationController < ActionController::Base
(中略)
private
(中略)
def check_logined
if session[:usr] then
begin
@usr = User.find(session[:usr])
rescue ActiveRecord::RecordNotFound
reset_session
end
end
unless @usr
flash[:referer] = request.fullpath
redirect_to controller: :login, action: :check
end
end
end
これによってセッション情報がUsersテーブルに存在するユーザーであるかどうかを判定して、ログイン済みかどうかを判定することができます。ユーザー情報が取得できなかった場合はloginコントローラのcheckアクションにリダイレクトします。したがって、次はcheckアクションをloginコントローラーに定義しにいきます。
このとき、bcryptによるセキュアな暗号化機能を利用したいのでUserモデルを編集します。
class User < ApplicationRecord
has_secure_password
end
このメソッドは、モデルにpassword/password_confirmationプロパティやその検証機能、認証のためのauthenticateメソッドを自動的に付加します。従って、こちらはモデルにpasswordプロパティなどを定義する必要はありません。ただし、このメソッドはusersテーブルにpassword_digestフィールドが存在することを前提としています。このpassword_digestフィールドには暗号化されたパスワード列が保存されます。
password_digestフィールドは先程のscaffoldingの際に作ってしまったので再定義の必要はありません。
####認証用フォームの作成
認証用のフォームを作成します。check_loginedフィルタでリダイレクト先としてloginコントローラーのcheckアクションが指定されていたのでcheckアクションに対応したビューを作成します。
<p style="color: Red">
<%= @error %>
</p>
<%= form_tag action: :check do %>
<div class="field">
<%= label_tag :username, 'ユーザ名' %> : <br/>
<%= text_field_tag :username, '', size: 20 %>
</div>
<div class="field">
<%= label_tag :password, 'パスワード' %> : <br/>
<%= password_field_tag :password, '', size: 20 %>
</div>
<%= hidden_field_tag :referer, flash[:referer] %>
<%= submit_tag 'ログイン' %>
<% end %><br/>
<%= link_to '新規登録', new_user_path %>
一方、loginコントローラーのcheckアクションの方にはフォームの入力内容が送信されてくるのでそれを処理するコードを記述します。
class LoginController < ApplicationController
skip_before_action :check_logined
def check
usr = User.find_by(name: params[:username])
if usr && usr.authenticate(params[:password]) then
reset_session
session[:usr] = usr.id
redirect_to params[:referer]
else
flash.now[:referer] = params[:referer]
@error = 'ユーザ名/パスワードが間違っています。'
render 'check'
end
end
end
二行目のskip_before_actionに:check_loginedを記述しないと堂々巡りが発生しログインフェーズに映ることができません。
####ルーティング
このままだとルーティングが失敗するのでroutes.rbにcheckアクションへのルートを記述します。
Rails.application.routes.draw do
get 'login/check'
post 'login/check'
resources :users
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
resourcesの部分は、usersモデルをscaffoldで作成した場合はすでに書いてあるはずです。
####usersビュー側の整形
これで認証部分は終わりですが、usersのビューに少し不備があるので直します。newテンプレートは特に直す必要はありませんがせっかくなので日本語化しておきます。
<h1>新規登録</h1>
<%= render 'form', user: @user %>
<%= link_to '戻る', users_path %>
showテンプレートはデフォルトだとpassword_digestを表示するようになっていますが、ハッシュされたものだとしてもこれが見えるのは嬉しくはないので表示するパラグラフを消しておきます。
<p id="notice">
<%= notice %>
</p>
<p>
<strong>名前:</strong>
<%= @user.name %>
</p>
<%= link_to '編集', edit_user_path(@user) %> |
<%= link_to '戻る', users_path %>
続いてindexテンプレートですが、こちらもデフォルトではpassword_digestが見えてしまうので消しておきます。
<p id="notice">
<%= notice %>
</p>
<h1>ユーザー一覧</h1>
<table>
<thead>
<tr>
<th>名前</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td>
<%= user.name %>
</td>
<td>
<%= link_to '見る', user %>
</td>
<td>
<%= link_to '編集', edit_user_path(user) %>
</td>
<td>
<%= link_to '破棄', user, method: :delete, data: { confirm: '本当に破棄しますか?' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to '新規作成', new_user_path %>
editテンプレートはとくに修正の必要はありません。日本語化だけしておきます。
<h1>ユーザーの編集</h1>
<%= render 'form', user: @user %>
<%= link_to '見る', @user %> |
<%= link_to '戻る', users_path %>
最後に部分テンプレート_form.html.erbは、少し修正が必要です。
<%= form_with(model: user, local: true) do |form| %>
<% if user.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user.errors.full_messages.each do |message| %>
<li>
<%= message %>
</li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :user_name %>
</div>
<div class="field">
<%= form.label :password %>
<%= form.password_field :password %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
デフォルトでは、パスワードの部分がpassword_digestになってしまっていてform.text_fieldになってしまっているはずなので、password_fieldに変えて引数も:passwordに変更します。passwordプロパティはbcryptの機能で自動的に設定されています。ここに受け取った文字列を暗号化したものがpassword_digestに保存されるという仕組みです。
####最後に
ユーザーの新規登録だけはログインせずにでもできるようにしないといけません。usersコントローラーにskip_before_actionの一文を追加します。また、StrongParameters機能も少し書き換えなければなりません。デフォルトでは:password_digestになっているはずなので、:passwordに書き換えます。
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
skip_before_action :check_logined, only: [ :new, :create ]
(中略)
def member_params
params.require(:user).permit(:name, :password)
end
end
これで全ての準備が整ったので、二重認証ができるようになるはずです。