EY-Office ブログ

やはりCORSは難しい!

以前書いていた「Apollo GraphQLに入門してみた(1 , 2 , 3 , 4 , 最終回 , おまけ , テスティング )」シリーズで作っていたバックエンドですが、いよいよAWS上にデプロイし開発チームで使う事になました。

しかし、またCORS(Cross-Origin Resource Sharing) 問題にはまり2日ほど苦しみました。😅

CORS

環境

バックエンドとフロントエンドの構成は下図のようになります。

  • バックエンド
    • Apollo Server : GraphQLサーバーフレームワーク、この上にアプリは作られています
    • Express : ポピュラーなWebアプリケーション・フレームワーク、セッションやCORSなどの制御を行っています
    • Node.js : JavaScript実行環境
    • nginx : クライアントとの間にはngixを使ったリーバープロキシーが入っています
  • フロントエンド
    • React : フロントエンドのアプリケーションはReactを使って作られています、ステート管理にはRedux Toolkitを使っています
    • Apollo Clinet : サーバーとのGraphQLやりとりを行っています。通信自身はweb browserのFetch APIを呼び出しています
    • web browser : 画面表示以外に、サーバーとの通信(Fetch API)やCookieのコントロールも行っています

URL(サイト)
環境バックエンドフロントエンドnginx
バックエンド開発http://localhost:5000http://localhost:3000なし
開発チームhttps://backend-dev.yyy.zzzhttp://localhost:3000あり
本番https://backend.yyy.zzzhttps://web.yyy.zzzあり
セッション管理

セッション管理(ログイン認証済み管理)にはいくつかの方法がありますが、今回はCookieを使う事にしました。詳細はApollo GraphQLに入門してみた(3)の後半に書きました。

現象

バックエンド開発環境では上手く動いていましたが、開発チーム環境ではログイン成功後のデータ取得APIが未認証エラーになってしまいます。

  • ログイン成功のレスポンスにはCookieが送られてきます
  • しかし、データ取得APIリクエストにはCookieが付いていません
  • したがって、データ取得APIが未認証エラーになってしまいます

CORS対応

今回はバックエンド開発環境でもバックエンドとフロントエンドはポートが違うのでCORS環境になっています。そこでCORS対応するコードを入れてありました。

  • バックエンドはExpressのcorsミドルウエアを使っています
const corsOriginURLs = (process.env.CORS_ORIGIN_URLS ?? "").split(",");

const app = express();
app.set('trust proxy', true);
app.use(cors({origin: corsOriginURLs, credentials: true}));
app.use(session({
  // ・・・ 一部省略 ・・・
  cookie: { maxAge: 1 * 60 * 60 * 1000 }
}));
  • フロントエンドは
  const httpLink = createHttpLink({
    uri: process.env.REACT_APP_BACKEND_URL,
    credentials: 'include'
  });

ヒント

むやみに試しても上手く行かないので、もう一度それぞれの技術(仕様)を勉強してみました。CORSには付いては、オリジン間リソース共有 (CORS)に詳しく書かれています。またApollo GraphQLのドキュメントにもConfiguring CORSにコード例を含め書かれています。

いろいろと調べるなかで、CORSはポート番号が違えば別サイトとして扱われますが、Cookieはポート番号が違っても同一サイトとして扱われます! バックエンド開発環境は両方とも http://localhostなのでCookieについてはCORS対応は出来てなくても問題ありませんでした! 😅

CookieにはSameSite属性がありデフォルトはLaxで同一サイト間ですか共有(やりとり)できません。ただしSameSite=Noneを指定するとCORS環境でも共有できるようになります、ただしSecure(HTTPS)にする必要があります。ということで、

  • バックエンドのサーバーはHTTPSにする
  • またバックエンドのコードでsameSite, secure, httpOnlyを指定
const corsOriginURLs = (process.env.CORS_ORIGIN_URLS ?? "").split(",");

const app = express();
app.set('trust proxy', true);
app.use(cors({origin: corsOriginURLs, credentials: true}));
app.use(session({
  // ・・・ 一部省略 ・・・
  cookie: { maxAge: 1 * 60 * 60 * 1000, secure: true, httpOnly: true, sameSite: 'none' }
}));

しかし、まだ上手く行きませんでした。現象は同じです 😵

解決

いろいろと調べていると、expressでtrust proxyを設定した場合はクライアントのIPアドレスやプロトコルはX-Forwarded-For,X-Forwarded-Protoヘッダーを使います、参照プロキシの背後にある表現

ということで、nginxの設定に以下を追加する事で無事に動くようになりました。😊

    location /api/ {
      proxy_pass http://server:5000/graphql/;
      proxy_set_header Host $http_host;
      proxy_set_header   X-Forwarded-Proto $scheme;
      proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header   X-Real-IP         $remote_addr;
    }

わかってみると当たり前の事ですが、今回の環境ではCORS, Cookie, Proxyと3つの技術(仕様)を組みあせているますが、それぞれを知識の理解が浅かったということです。今時のインフラは難しいですね! 😅

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ