1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Next.js×Rails API】AWS本番環境にてgetServerSidePropsでCookieを送信するのにハマったことと対応まとめ

Last updated at Posted at 2023-10-21

概要

  • 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

インフラ構成図

image.png

前提

ここで採用しているログイン関連処理

ログイン認証はセッション認証方式をとっており、ログインが成功すると、セッション用のCookieが作成されるようになっている。
ログイン認証後は、必要なページのgetServerSidePropsにて、Cookieの情報をRails APIのcheck_sessionアクションに送信することで、ログインユーザー情報の取得を通じて、ログイン状態の継続を確認している。

関連処理抜粋

pages/communities/index.jsから抜粋
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 } }
  }
}
routes.rb
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
sessions_controller.rb
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

発生した不具合:ログイン状態の確認処理が動作していない

  • 基本的な機能について正常に動作している
  • ログイン認証自体は正常に機能している
    image.png
  • Next.jsのクライアントサイドで実行されるAPIへのPOSTリクエストは正常に動作している

ことまでは確認できているものの、
ログイン状態の確認処理が動作していないことで、画面の表示が非ログイン時と同じ状態となっており、ログインユーザーの情報(ユーザー自体の情報・投稿へのいいねやブックマーク状況等)が一切反映されていない。

詳細状況確認

ログイン確認処理前後に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種類となっている。

  1. クライアントサイドからフロントエンド(www.example.com)へのGETリクエスト(静的なページの表示)
  2. クライアントサイドからバックエンド(back.example.com)への各種APIリクエスト(動的なコンテンツの表示)
  3. フロントエンドサーバーサイドからバックエンド(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となっており、別ドメイン間の通信はそもそもされていなかったと思われる。
image.png

明示的なドメイン間のCookie送信許可設定を追記することで、

example-app/back/config/application.rb
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となっている。
image.png

※application.rbに設定を追加する前のDomainはback.example.comとなっていた
image.png

参考

最後に

より良い方法や間違い等ありましたらご指摘いただけますと幸いです!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?