【Rails】DB を使わずに、jsonファイルで簡易ユーザー管理機能を実装する
ある Rails アプリケーションにおいて、下図のように、1つの DB を複数のアプリケーションが参照する場合がある。(ただし、複数のアプリケーションから、自由に DB の書き込みができるのではなく、1つのみに Read/Write 権限を持たせて、その他は Read 権限しかない。)
この場合に、DB の Read 権限しかない Rails アプリケーションでは、主にデータの可視化だけを行っている。そして、データの可視化において、ログインユーザーに応じて表示を変えたいという話になり、本題の DB を使わずに、jsonファイルで簡易ユーザー管理を実装したので、今回紹介する。(マルチDBでの実装も検討したが、一旦はログインユーザー管理のみとのことだったので、jsonファイルでの代用で押し切ることとした。)
Rails のユーザー管理といえば、device だと思うが、今回、deviceライクな実装になるようにしてみた。
jsonファイル/ディレクトリ構成
今回、jsonファイルは DB と同じ扱いをするため、Rails.root/db に配置することとした。
Rails.root ├─ app ├─ db │ └─ user.json └─ Gemfile
簡単であるが、試験的に2人のユーザーを設定した。
{ "members": [ { "user_id": "admin", "name": "管理者", "password": "admin", }, { "user_id": "test1", "name": "テストユーザー1", "password": "test1", } ] }
Uerモデルを作る
Userモデルでは、以下を考慮している。
- ログイン画面から、ID/PASSで認証できる
- ログイン後には、sessionにID/PASSを暗号化したaccess_tokenを保存しておき、リロード時には自動的にログインさせる(復号化時に使用した OpensslUtil に関しては、後ほど紹介する)
- membersのデータを都度読み込むのではなく、インスタンス生成時に、読み込ませるようにしている
class User include ActiveModel::Model def initialize(user_id: "", password: "", name: "") @user_id = user_id @password = password @name = name @members = [] # jsonファイルのmembersを読み込む File.open("#{Rails.root}/db/user.json") do |file| hash = JSON.load(file) @members = hash["members"] end return @members end attr_accessor :user_id, :name, :password validates :user_id, presence: true validates :name, presence: true validates :password, presence: true # ログインユーザーを認証する def authenticated? @members.any? {|member| member["user_id"] == @user_id && member["password"] == @password} end # sessionのaccess_tokenから自動で再ログインさせる def present?(access_token) str_decrypted_token = OpensslUtil.aes_decrypt(access_token) str_arr = str_decrypted_token.split("::") current_user = @members.find {|member| member["user_id"] == str_arr[0] && member["password"] == str_arr[1]} if !current_user.blank? @user_id = current_user["user_id"] @password = current_user["password"] @name = current_user["name"] return self else return false end end end
include ActiveModel::Model
を追加しておくことで、Action PackやAction Viewと連携する以下の機能を使うことができるようになる。
- モデル名の調査
- 変換
- バリデーション
また、form_forやrenderなどのAction Viewヘルパーメソッドも使えるようになるので、便利である。
routesを設定する
ログイン画面、ログイン処理、ログアウト処理ができるように、ルートを追加する。
Rails.application.routes.draw do <-- 省略 --> controller :sessions do get 'login' => :new #ログイン画面表示 post 'login' => :create #フォームに入力されたID/PASSでログインを行う delete 'logout' => :destroy #ログアウトを行う end end
Controller を作る
全てのControllerで、ログインユーザーである current_user を参照したいので、application_controller.rb
に current_user を定義する。
また、before_action :login_required
を行うことで、ログインユーザーでない場合は、ログイン画面にリダイレクトさせる。
class ApplicationController < ActionController::Base helper_method :current_user before_action :login_required private def current_user @current_user ||= User.new().present?(session[:access_token]) if session[:access_token] end def login_required redirect_to login_path status: :unauthorized unless current_user end end
SessionsController に関しては、ログインしていない場合にログイン画面を表示させたいので、skip_before_action :login_required
を追加しておく必要がある。
また、認証された場合には、#{user_id}::#{password}
の文字列として暗号化を行い、sessionに保存する。(暗号化時に使用した OpensslUtil に関しては、後ほど紹介する)
class SessionsController < ApplicationController skip_before_action :login_required def new end #ログイン処理 def create login_user = User.new(user_id: params[:user_id], password: params[:password]) if login_user.authenticated? #認証された場合 session[:access_token] = OpensslUtil.aes_encrypt("#{login_user.user_id}::#{login_user.password}") redirect_to(root_path) && return else render :new, status: :bad_request end end #ログアウト処理 def destroy session.delete :access_token redirect_to action: :new end end
Viewを作る
ログイン画面を作る
.row.justify-content-center .col-xl-5.col-lg-6.col-md-9 .card.o-hidden.border-0.shadow-lg.my-5 .card-body.p-0 .row .col-lg-12 .p-5 = form_tag login_path, method: :post, class: "user" do .form-group = text_field_tag(:user_id, nil, class: "form-control form-control-user", placeholder: "ユーザーID", required: true, value: params[:user_id]) .form-group = password_field_tag(:password, nil, class: "form-control form-control-user", placeholder: "パスワード", required: true) button.btn.btn-primary.btn-user.btn-block(type="submit") ログイン
暗号化・復号化に関して
暗号化・復号化のメソッドに関しては、modelやcontrollerでも使いたかったので、Rails.root/lib/utils/の配下に作成することとした。
Rails.root/lib/utils/ を初回起動時に、読み込むように config/application.rb に以下を追記する。
module App class Application < Rails::Application # ~ 省略 ~ # Autoload utils config.paths.add 'lib/utils', eager_load: true end end
暗号化と復号化に関しては、OpenSSL を使って実装している。
class OpensslUtil SALT = "AM3hFSF0MLkTgKRG6U418gBC" PASSWORD = "TEST1234" BIT = 256 # ====================================== # <暗号化> # ====================================== def self.aes_encrypt(plain_text) enc = OpenSSL::Cipher::AES.new(BIT, :CBC) enc.encrypt salt = Base64.decode64(SALT) key_iv = OpenSSL::PKCS5.pbkdf2_hmac(PASSWORD, salt, 2000, enc.key_len + enc.iv_len, "sha256") enc.key = key_iv[0, enc.key_len] enc.iv = key_iv[enc.key_len, enc.iv_len] encrypted_text = enc.update(plain_text) + enc.final Base64.encode64(encrypted_text).chomp end # ====================================== # <復号> # ====================================== def self.aes_decrypt(encrypted_text) encrypted_text = Base64.decode64(encrypted_text) salt = Base64.decode64(SALT) dec = OpenSSL::Cipher::AES.new(BIT, :CBC) dec.decrypt key_iv = OpenSSL::PKCS5.pbkdf2_hmac(PASSWORD, salt, 2000, dec.key_len + dec.iv_len, "sha256") dec.key = key_iv[0, dec.key_len] dec.iv = key_iv[dec.key_len, dec.iv_len] dec.update(encrypted_text) + dec.final end end
sessionの保存期間を設定する
access_tokenを保存している session の保存期間を設定するために、config/initializers/session_store.rb
を作成する。今回は、保存期間を1時間とした。
# Set Expired Session # https://edgeapi.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html Rails.application.config.session_store :cookie_store, expire_after: 1.hour
まとめ
今回は、ちょっとイレギュラーな DB を使わずに、jsonファイルで簡易ユーザー管理機能を実装した。deviceライフな感じにしたので、使い勝手としても悪くない気がしている。ただし、Token管理やログイン履歴まではできないので、本当に簡易版での使用用途になると思われる。
それでは、ステキな開発ライフを。