<Ruby on Rails> 複数サービスの認証を1本化する

 複数サービスの認証を1本化する

こんにちは! エンジニアインターンの斉藤です。

複数のサービスにおける認証において、同一アカウントでログインするための実装をRailsで行いました。

自分なりに工夫した箇所がいくつかあるので、今回はそれらを含めて実装の仕方を紹介したいと思います。

  1. 背景
  2. 実装のポイント
    1. 1. 一定時間ごとにユーザーが存在するかを基幹システムに確認する
    2. 2. 基幹システムへユーザーの確認をする際はAPIトークンを使う
  3. 認証の流れ
    1. ログイン時
    2. ログイン後(バリデーション時)
  4. 実装
    1. ユーザー確認部分
    2. APIトークン取得部分
  5. さいごに

背景

複数サービスとタイトルにはありますが、これらは本来、1つの社内システムに集約されていました。APIとUIの基準化を行っていたため、開発の工数が少なく便利だったのですが、機能・役割の肥大化が問題視されていました。機能・役割が大きくなりすぎると、障害時のリスク増大、機能ごとの権限管理の複雑化などを引き起こす恐れがあります。

そこで、この基幹システムの一部役割を機能ごとに切り離し、別々のサービスにすることになりました。切り離した際にサービスごとにユーザーを管理することは面倒なので、ユーザー管理は従来通り基幹システムの1箇所で管理し、各サービスでは権限管理のみを行うことになりました。

本記事では1箇所で管理しているユーザーの認証をどのように各サービスで行っているのか具体的に説明したいと思います。

実装のポイント

今回の認証を実装するにあたって重要な点が2つあったので紹介します。

1. 一定時間ごとにユーザーが存在するかを基幹システムに確認する

基幹システムでユーザーを削除した際、他の全サービスで使えないようにする必要があります。そのため、ログイン後はセッションからユーザーIDを取り出し、ユーザーが存在するかリクエストの度に確認する必要がありますが、毎回基幹システムに確認しに行くと負荷をかけてしまいます。

そこで、ユーザー情報を短時間各サーバーにキャッシュし、セッションから得られたユーザー情報がキャッシュされていなければ基幹システムにユーザーの存在を問い合わせ、再度一定時間の有効期限をつけてキャッシュしました。

基幹システムでユーザーを削除してから各サービスで利用不可になるまで多少の時差はありますが、基幹システムサーバーへの負荷を鑑みた有効期限を設定しています

2. 基幹システムへユーザーの確認をする際はAPIトークンを使う

ユーザー情報は基幹システムサーバーの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_actionauthenticate_sessionを呼び出し、無効なsessionの場合は弾くようにしています。

ユーザーIDとセッションIDがキャッシュされていない場合はcheck_core_system_userで基幹システムにユーザーが存在するかどうかを問い合わせ、再度キャッシュします。

APIトークン取得部分

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日時点

Techブログ 新着記事一覧