こんにちは! エンジニアインターンの斉藤です。
複数のサービスにおける認証において、同一アカウントでログインするための実装をRailsで行いました。
自分なりに工夫した箇所がいくつかあるので、今回はそれらを含めて実装の仕方を紹介したいと思います。
複数サービスとタイトルにはありますが、これらは本来、1つの社内システムに集約されていました。APIとUIの基準化を行っていたため、開発の工数が少なく便利だったのですが、機能・役割の肥大化が問題視されていました。機能・役割が大きくなりすぎると、障害時のリスク増大、機能ごとの権限管理の複雑化などを引き起こす恐れがあります。
そこで、この基幹システムの一部役割を機能ごとに切り離し、別々のサービスにすることになりました。切り離した際にサービスごとにユーザーを管理することは面倒なので、ユーザー管理は従来通り基幹システムの1箇所で管理し、各サービスでは権限管理のみを行うことになりました。
本記事では1箇所で管理しているユーザーの認証をどのように各サービスで行っているのか具体的に説明したいと思います。
今回の認証を実装するにあたって重要な点が2つあったので紹介します。
基幹システムでユーザーを削除した際、他の全サービスで使えないようにする必要があります。そのため、ログイン後はセッションからユーザーIDを取り出し、ユーザーが存在するかリクエストの度に確認する必要がありますが、毎回基幹システムに確認しに行くと負荷をかけてしまいます。
そこで、ユーザー情報を短時間各サーバーにキャッシュし、セッションから得られたユーザー情報がキャッシュされていなければ基幹システムにユーザーの存在を問い合わせ、再度一定時間の有効期限をつけてキャッシュしました。
基幹システムでユーザーを削除してから各サービスで利用不可になるまで多少の時差はありますが、基幹システムサーバーへの負荷を鑑みた有効期限を設定しています。
ユーザー情報は基幹システムサーバーのDBにあるため、各サービスからログインをした場合、基幹システムにIDとパスワードを使って問い合わせを行います。しかし、基幹システムサーバーはパブリックであるため、APIの仕様が漏洩してしまった場合に総当たり攻撃を受けてしまう可能性があります。
そこで、ユーザー情報の問い合わせには有効期限つきのAPIトークンを必要とするようにしました。APIトークンにも有効期限があるので、その間だけ各サーバーにキャッシュしておき、キャッシュされていなければ再度発行することで、基幹システムの負担も最小限に抑えました。
これらを踏まえ、認証の全体の流れをログイン時とログイン後のバリデーションに分けて説明します。
1. ブラウザからID・パスワードをPOSTメソッドで受け取る
2. Tokenがキャッシュされているか確認し、なければ基幹システムから取得し、キャッシュする。
3. 基幹システムにTokenを使ってID・パスワードでユーザーを問い合わせる
4. 取得したユーザーの権限を確認(今回は説明を省略)
5. ユーザー情報をキャッシュする
6. ブラウザへ権限情報を返す。この際、セッションIDとユーザIDを含めたCookieをセットする
1. ブラウザからCookieを含んだリクエストを受け取る
2. CookieからユーザーIDを取得し、キャッシュされているか確認(キャッシュされている場合、権限情報を返して処理終了)
3. Tokenがキャッシュされているか確認し、なければ基幹システムから取得し、キャッシュする。
4. ユーザーIDを基幹システムに問い合わせ、有効かどうか確認する
5. 取得したユーザーの権限を確認(今回は説明を省略)
6. ユーザー情報をキャッシュする
7. ブラウザへ権限情報を返す。この際、セッションIDとユーザIDを含めたCookieをセットする
ここからは実装のポイントで挙げた内容をコードで抜粋して紹介します。
ここまでの説明であった「基幹システム」はコード内で"core_system"としています。
module SessionAuthHelper
def login_session(user_id:)
session_id = SecureRandom.uuid
session[:user] = {
user_id: user_id,
session_id: session_id,
}
Rails.cache.write(session_id, user_id, expires_in: expire_time)
end
end
まず、ログイン時に呼び出される関数でCookieをセットできるようにし、ユーザーIDとセッションIDをキャッシュします。
class ApplicationController < ActionController::API
before_action :authenticate_session!, only: [:some_function]
def some_function
#認証が必要なアクション
end
def authenticate_session
sess = session[:user]
raise SessionAuthHelper::AuthorizationError if sess.nil?
user_id = sess["user_id"]
session_id = sess["session_id"]
cached_user_id = Rails.cache.read(session_id)
if cached_user_id.nil?
raise SessionAuthHelper::ForbiddenError unless check_core_system_user(user_id).present?
Rails.cache.write(session_id, user_id, expires_in: expire_time)
end
end
end
そして認証が必要なAPIが叩かれた時に実行されるアクションは、before_actionでauthenticate_sessionを呼び出し、無効なsessionの場合は弾くようにしています。
ユーザーIDとセッションIDがキャッシュされていない場合はcheck_core_system_userで基幹システムにユーザーが存在するかどうかを問い合わせ、再度キャッシュします。
module CoreSystemAuthController
extend ActiveSupport::Concern
included do
def core_system_api_token
Rails.cache.fetch(:core_system_api_token, expires_in: expire_time) do
get_core_system_token()
end
end
def get_core_system_token
begin
uri = URI.parse("#{ENV["CORE_SYSTEM_HOST"]}/get_token")
http = Net::HTTP.new(uri.host, uri.port)
req = Net::HTTP::Post.new(uri)
req['X-Api-key'] = ENV['CORE_SYSTEM_API_KEY']
res = http.request(req)
core_system_token = res.body
Rails.cache.write("core_system_token", core_system_token, expires_in: expire_time)
core_system_token
rescue => e
Rails.logger.error "Error Get Token From Core System"
Rails.logger.error e.message
end
end
end
end
下に示すモジュールをincludeしているコントローラーがcore_system_api_tokenを呼び出します。
この関数はAPIトークンがキャッシュされていなければget_core_system_tokenを呼び出します。
get_core_system_tokenは基幹システムからAPIトークンを取得し、キャッシュします。
いかがでしたでしょうか。
これによって1つのシステムを機能ごとに複数サービスに分割しても、同じアカウントでログインができるようになりました。
今回は省略しましたが、実際にはreCAPTCHAによりセキュリティを高めたり、Googleログインも実装してスムーズなログインを実現したりしています。
ログインが必要なシステムを分割しようとしている方、複数サービスでのログイン実装を検討している方はぜひ参考にしてみてください。
最後まで読んでいただき、ありがとうございました!
※2023年3月13日時点