【JavaScript】Cookieが送られない?Set-Cookiesされない?原因と解決策を解説!

「JavaScriptでAPIを叩いたけど、なぜかCookieが送られない…」「サーバーでSet-Cookieしてるのに、ブラウザに保存されてない…」そんな経験ありませんか?

この問題、クロスオリジン通信(CORS)時の設定が原因であることが非常に多いです。特にcredentials: 'include'withCredentials = true の指定を忘れると、Cookieは送受信されません

この記事では、上記の問題を解決する方法を解説しています。以下の内容をサンプルコードを用いてわかりやすく解説していますので、ご参考になれば幸いです。

  • credentials: 'include'とは?
  • withCredentials = trueとは?
  • Cookieが送受信されているかを確認する手順【fetch + Expressで検証】

credentials: 'include'とは?

JavaScriptのfetch()でリクエストを送る際、デフォルトではCookieや認証情報は送信されません。クロスオリジン(異なるドメイン間)でリクエスト時にCookieを送信するためには、credentials: 'include'を明示的に指定する必要があります。

fetch(url, {
  credentials: 'include'
});

これを指定することで、同一オリジンでもクロスオリジンでもCookieを含めたリクエストが可能になります。

credentialsの種類一覧

説明
'omit'Cookieなどの認証情報は送信しない(デフォルト)
'same-origin'同一オリジンのリクエストにはCookieを送信。クロスオリジンでは送信しない
'include'常にCookieを送信する(クロスオリジン含む)

サーバーでSet-Cookieした際の挙動

credentials: 'include'を指定せずにクロスオリジンでAPIを叩くと、サーバーがSet-Cookieを返しても、ブラウザはそのCookieを保存しません。表で表すと以下のようになります。

credentialsの値同一オリジンクロスオリジン
'omit'❌ 保存しない❌ 保存しない
'same-origin'✅ 保存する❌ 保存しない
'include'✅ 保存する✅ 保存する

サーバー側にも設定が必要

クライアント側でcredentials: 'include'を設定しても、サーバー側が適切にレスポンスヘッダーを返さないとCookieは送受信されません。サーバー側のレスポンスには、以下のようなヘッダーが必要です。

Access-Control-Allow-Origin: http://localhost:5500 ← リクエストを送るオリジンを指定
Access-Control-Allow-Credentials: true

なお、Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true併用できませんAccess-Control-Allow-Credentials: trueを設定した場合、オリジンはワイルドカード(*)ではなく、明示的に指定する必要があります。

withCredentials = trueとは?

withCredentials = trueは、fetch以外のAPI(例:XMLHttpRequestaxios)で、Cookieを送信するために使うオプションです。これはfetchcredentials: 'include'と同じ意味です。

// axiosの例
axios.get('http://localhost:4000/api/message', {
  withCredentials: true
});

Cookieが送受信されているかを確認する手順【fetch + Expressで検証】

実際に、Cookieの送受信ができるかを確認してみましょう。手順を以下に示します。

ステップ

  • APIサーバー(Express)を作成して起動する
  • 別オリジンのフロントエンド(HTML + fetch)を作成する
  • ブラウザでフロントHTMLを開く
  • Cookieが送受信されているかを確認する

なお、今回使用するディレクトリ構成は以下の通りです。

credentialsSample/
├── frontend/ ← フロントエンド(HTML + fetch)
│   ├── index.html
│   └── script.js
└── apiServer/ ← APIサーバー側(Express)
    └── server.js 

APIサーバー(Express)を作成して起動する

まずAPIサーバーを作成します。サンプルコード(apiServer/server.js)を以下に示します。

// 1. Expressモジュールを読み込んでインスタンス化してappに代入
const express = require('express');
const app = express();

// 2. CORS(クロスオリジンリクエスト)対策用のミドルウェアを設定
app.use((req, res, next) => {
  // クライアント(http://localhost:5500)からのリクエストを許可
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
  // 認証情報(Cookieなど)を含めることを許可
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  // 許可するHTTPメソッドを指定
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  // 許可するHTTPヘッダーを指定
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  // プリフライトリクエスト(OPTIONSメソッド)には即時レスポンスを返す
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204); // No Content
  }

  // 次のミドルウェアまたはルートに処理を渡す
  next();
});

// 2. listen()メソッドを実行して4000番ポートで待機
const port = 4000;
app.listen(port, () => {
  console.log(`サーバー起動中: http://localhost:${port}`);
});

// 3. 以下アプリケーションの処理を記述する
// "/api/message" にGETリクエストが来たときの処理
app.get('/api/message', (req, res) => {
  // Set-Cookie ヘッダーを設定(名前: sample_cookie, 値: hello)
  res.setHeader('Set-Cookie', 'sample_cookie=hello; HttpOnly; SameSite=Lax');
  res.json({ message: 'こんにちは、クライアントさん!' });
});

作成したAPIサーバーを起動する前に、npmを使ってExpressをインストールします。以下のコマンドを実行してください。

cd apiServer
npm init -y
npm install express --save

次にサーバーを起動します。カレントディレクトリがserver.jsが格納されているapiServerディレクトリであることを確認して、以下のコマンドを実行してください。

node server.js

これでサーバーが起動します。サーバーはhttp://localhost:4000で待ち受けています。

別オリジンのフロントエンド(HTML + fetch)を作成する

frontend/index.htmlのコードを以下に示します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Express API Test</title>
  </head>
  <body>
    <h1>Express API テスト</h1>
    <button id="fetchBtn">APIを呼び出す</button>
    <p id="result">← ここに結果が表示されます</p>

    <script src="script.js"></script>
  </body>
</html>

frontend/script.jsのコードを以下に示します。

document.getElementById('fetchBtn').addEventListener('click', () => {
  fetch('http://localhost:4000/api/message', {
    method: 'GET',
    credentials: 'include', // クッキーなどを送る axiosやXMLHttpRequestのwithCredentials = trueと同じ
  })
    .then((response) => {
      if (!response.ok) {
        throw new Error('APIエラー');
      }
      return response.json();
    })
    .then((data) => {
      document.getElementById('result').textContent = data.message;
    })
    .catch((error) => {
      console.error('エラー:', error);
      document.getElementById('result').textContent = 'エラーが発生しました';
    });
});

ブラウザでフロントHTMLを開く

クロスオリジン通信を確認するため、HTMLファイルをブラウザで直接開かず、Live ServerなどのローカルサーバーでHTMLファイルを開きます。VSCodeのLive Serverを使う場合、以下の手順で開くことができます。

  • index.htmlが格納されているfrontendディレクトリでVSCodeを開く。
  • index.htmlを右クリックして「Open with Live Server」をクリックする。
  • ブラウザでhttp://localhost:5500を開く。

Cookieが送受信されているかを確認する

http://localhost:5500/index.htmlにアクセスしたら、「APIを呼び出す」ボタンをクリックしてください。クリックすると、http://localhost:4000/api/messageにリクエストが送られます。ブラウザ画面に"こんにちは、クライアントさん!"が表示されたらリクエスト成功です。

Google Chromeのデベロッパーツールを見ると、サーバーでSet-CookieされたCookieが保存されていることを確認できます。

Cookieが送受信されているかを確認する01

また、2回目のリクエストでは、保存されたCookieをリクエスト時に送信していることも確認できます。

Cookieが送受信されているかを確認する02

frontend/script.jscredentials: 'include'を削除して、「APIを呼び出す」ボタンをクリックすると、サーバーでSet-Cookieしているのにも関わらず、ブラウザにCookieが保存されていないことも確認できます。

本記事のまとめ

この記事では以下の内容を説明しました。

  • クロスオリジンでCookieを送るには、credentials: 'include'(または withCredentials: true)の指定が必須
  • サーバー側ではAccess-Control-Allow-Credentials: trueと、明示的なAccess-Control-Allow-Originの指定が必要
  • Access-Control-Allow-Origin: *Allow-Credentials: trueは併用できない
  • Cookieを保存・送信させるには、クライアントとサーバーの両方に正しい設定が必要
  • 動作確認には、ローカルでも別オリジンになるようLive Serverなどを使うと確実

お読み頂きありがとうございました。