概要
- Railsのみで構成していたアプリケーションを、Next.js×Rails APIのSPA構成に変更した
- SPA化に伴い、AWS本番環境の設定を変更した
- ALBにホストベースルーティングの設定を追加し、Frontendのドメインをwww.example.com、Backendのドメインをback.example.comとした
- 以上までの設定はこちらに記載
- 大体の機能は正常に動作しているものの、ドメインの違いにより、ログイン関連処理の一部が正常に動作しなかったため対応をまとめ
環境
- Backend
- ruby 3.0.2
- rails 6.1.4 (APIモード)
- Frontend
- react 18.2.0
- next 13.1.6
インフラ構成図
前提
ここで採用しているログイン関連処理
ログイン認証はセッション認証方式をとっており、ログインが成功すると、セッション用のCookieが作成されるようになっている。
ログイン認証後は、必要なページのgetServerSidePropsにて、Cookieの情報をRails APIのcheck_sessionアクションに送信することで、ログインユーザー情報の取得を通じて、ログイン状態の継続を確認している。
関連処理抜粋
export const getServerSideProps = async (context) => {
try {
const cookie = context.req?.headers.cookie
const response = await apiClient.get('https://back.example.com/api/v1/check', {
headers: {
cookie: cookie,
},
})
const user = await response.data.user
return { props: { user: user } }
} catch (error) {
// エラーに応じたメッセージを取得する
let errorMessage = ''
if (error.response) {
errorMessage = error.response.errorMessage
} else if (error.request) {
errorMessage = error.request.errorMessage
} else {
errorMessage = error.errorMessage
}
return { props: { errorMessage: errorMessage } }
}
}
Rails.application.routes.draw do
namespace :api, format: 'json' do
namespace :v1 do
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
get '/check', to: 'sessions#check_session'
end
end
end
module Api
module V1
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] ? remember(user) : forget(user)
message = [I18n.t('sessions.create.flash.success')]
render json: { status: 'success', message: message }
else
message = [I18n.t('sessions.create.flash.danger')]
render json: { status: 'failure', message: message }
end
end
def destroy
log_out if logged_in?
message = [I18n.t('sessions.destroy.flash.success')]
render json: { status: 'justLoggedOut', message: message }
end
def check_session
# ログイン中のユーザー情報を返す
render json: { user: current_user }
end
end
end
end
発生した不具合:ログイン状態の確認処理が動作していない
ことまでは確認できているものの、
ログイン状態の確認処理が動作していないことで、画面の表示が非ログイン時と同じ状態となっており、ログインユーザーの情報(ユーザー自体の情報・投稿へのいいねやブックマーク状況等)が一切反映されていない。
詳細状況確認
ログイン確認処理前後にconsole.logを仕込み、正常に動作している開発環境とのログを比較。
※console.logはpages/communities/index.js
に仕込み、https://www.example.com/gadgets
からhttps://www.example.com/communities
への遷移時のログを確認した。
--------------------------------------------context.req?.headers--------------------------------------------
{
'x-forwarded-for': '106.72.146.64',
'x-forwarded-proto': 'https',
'x-forwarded-port': '443',
host: 'www.example.com',
'x-amzn-trace-id': 'Root=1-651ff964-0c553774641291f00531b6d6',
pragma: 'no-cache',
'cache-control': 'no-cache',
'sec-ch-ua': 'Google Chrome;v=117, Not;A=Brand;v=8, Chromium;v=117',
'x-nextjs-data': '1',
'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
'sec-ch-ua-platform': 'macOS',
accept: '*/*',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'https://www.example.com/gadgets',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'ja,en-US;q=0.9,en;q=0.8'
}
--------------------------------------------context.req?.headers--------------------------------------------
{
host: 'localhost:8000',
connection: 'keep-alive',
'sec-ch-ua': '"Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
'x-nextjs-data': '1',
'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
'sec-ch-ua-platform': '"macOS"',
accept: '*/*',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:8000/gadgets',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'ja,en-US;q=0.9,en;q=0.8',
cookie: '_session_id=xTVlmxOS6XyEaYv4myq4SfJo8NVMHuQ4H9WYjm8rxHy1prUoSNIuvThKJtoONmxITa07BX2R2pAQUBhUFmE6LuY7ySKwpGpLEa2xLkGSPg7286W2Ruro4Nfg%2Bsc21oZJvbjl885S5h7QrOm3IBzEpNxfrWBvOa%2F9hUwkyIlfMb%2BB4WqWwP3bfhHfa5FaDSwFht8q--HiJZ8%2FMkv38IG4qk--TYJ1oy7t4GLTEk0mdWRsEw%3D%3D; __profilin=p%3Dt'
}
ここで、本番環境のcontext.req.headersにCookieが含まれていないことが判明した。
そもそもCookieとは
- ブラウザが持っているテキスト情報
- WEBサーバーからブラウザに送信されるもの
- 有効期限が切れると、ブラウザが削除する
- ユーザーが削除することも可能
- 基本的にset-Cookieされた時のサーバ(ドメイン)と同一のサーバ(ドメイン)へのリクエスト時のみ送信される
set-Cookieされた時のドメインとリクエスト送信先ドメインについて
当アプリケーションで動作するリクエストは以下の3種類となっている。
- クライアントサイドからフロントエンド(
www.example.com
)へのGETリクエスト(静的なページの表示) - クライアントサイドからバックエンド(
back.example.com
)への各種APIリクエスト(動的なコンテンツの表示) - フロントエンドサーバーサイドからバックエンド(
back.example.com
)への各種APIリクエスト(getServerSideProps内で実行される、ログイン確認や詳細ページ情報の取得)
ログイン認証時のset-Cookieはバックエンドドメイン(back.example.com)から送信されているため、バックエンドへのAPIリクエストであれば、Cookieは送信されるはず(つまり、2と3については問題なくCookie送信されるはず)、、、という認識だったが、3で送信するCookieは、1で送信されたCookieをリクエストヘッダーから明示的に取得してきているもの
であり、3のリクエストヘッダーに自動的に載るものではなかった。
つまり、
-
https://www.example.com/gadgets
からhttps://www.example.com/communities
へ遷移した際は、クライアントサイドから、フロントエンド(
www.example.com)へのGETリクエスト
となっている - ログイン認証時のset-Cookieはバックエンドドメイン(back.example.com)から送信されたものとなっているため、このフロントエンド(
www.example.com
)へのGETリクエストではCookieは送信されない - そのため、getServerSideProps内のcontext.req.headersにCookieは含まれないことになる
- Cookieが無いため、check_sessionの返り値は常に
{ user:null }
といった状況になっていることで、ログイン確認処理が正常に動作していなかった(常に非ログイン状態であるとみなされていた)。
以上から、バックエンドドメイン(back.example.com)でセットされたCookieを、フロントエンド(www.example.com
)へのGETリクエスト時にも送信できるようにする必要がある。
ドメインをまたいでCookieを送信するためには
- SameSite属性をNoneにする
- Secure属性をTrueにする(HTTPS通信)
これらの設定は問題ないはず。現に、開発環境では問題なく動作できていた。
参考
という認識だったが、開発環境では、Domainがlocalhostとなっており、別ドメイン間の通信はそもそもされていなかったと思われる。
明示的なドメイン間のCookie送信許可設定を追記することで、
module ExampleApp
class Application < Rails::Application
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore,
+ domain: :all,
+ tld_length: 2,
+ secure: true
config.action_dispatch.cookies_same_site_protection = nil
end
end
back.example.com
で発行したCookieを、www.example.com
へのリクエスト時に送信できるようになった。
Domainは*.example.com
となっている。
※application.rbに設定を追加する前のDomainはback.example.comとなっていた
参考
最後に
より良い方法や間違い等ありましたらご指摘いただけますと幸いです!