👺

【React/CRA】express-sessionでセッションを実現する。

2022/07/17に公開

まえがき

express-sessionによるセッションを使ってみます。

各ショッピングサイトでお馴染みの買い物かご機能を実装してみましょう。
・フロント(買い物画面) を CRA(create-react-app)で実装
・買い物かご(セッション)を操作するAPI をexpress/express-sessionで実装

01. 【フロント】CRAプロジェクト作成

npx create-react-app cra-proxy-practice --template typescript

必要なライブラリをインストール

npm install axios
npm install express
npm install express-session
npm install npm-run-all
npm install --save-dev @types/express
npm install --save-dev @types/express-session
npm install --save-dev ts-node

02. 【フロント】買い物画面用意

src/App.ts
import React, {useEffect, useState} from 'react';
import axios from 'axios';

const App = () => {
  const [cart, setCart] = useState([]);

  // 画面表示時にセッション取得
  useEffect(() => {
    axios.get("/init-cart").then(res => {
      setCart(res.data.products);
    })
  }, [])
  
  const addCart = (name: string) => {
    axios.get(`/add-cart?product=${name}`).then(res => {
      setCart(res.data.products);
    });
  }

  const clearCart = async () => {
    await axios.get("/clear-cart");
    setCart([]);
  }

  const products = ["りんご", "ぶどう", "バナナ", "キウイ"];
  return (
    <div style={{"margin": "30px"}} className="App">
      <h1 style={{"textDecoration": "underline"}}>買い物画面</h1>
      <h2>かごの中身: {cart.join("、")}</h2>
      <h2><button onClick={() => clearCart()}>かごを空にする</button></h2>
      <h2>商品一覧</h2>
      {products.map((product) =>
         (<h3>{product} 
        <button onClick={() => addCart(product)}>追加</button>
      </h3>)
      )}
      
    </div>
  );
}

export default App;

こんな画面になります。

03.【バック】APIとセッションを実装。

server/index.ts
import express from "express";
import session from "express-session";

const app = express();

app.use(
  // 最低限の設定です。本当はもっと色々設定します。
  session({
    secret: "secret",
    cookie: {},
  })
);

app.get("/init-cart", (req, res) => {
  if (req.session["cart"]) {
    return res.send({ products: req.session["cart"] });
  }
  return res.send({ products: [] });
});

app.get("/add-cart", (req, res) => {
  if (!req.session["cart"]) {
    req.session["cart"] = [] as string[];
  }
  req.session["cart"].push(req.query["product"]);
  res.send({ products: req.session["cart"] });
});

app.get("/clear-cart", (req, res) => {
  req.session.destroy((err) => {
    res.send("clear done");
  });
});

app.listen(8080, () => {
  console.log("mockapi server is listening on 8080.");
});

server/index.ts用のTSコンパイル設定ファイル

tsconfig.server.json
{
  "compilerOptions": {
    "baseUrl": "./server",
    "module": "commonjs", // import/export構文をTS→JS変換時にrequire構文にしてくれる
    "esModuleInterop": true
  }
}

npm run devでReactアプリとAPIサーバが起動するようスクリプトを定義。

pacakge.json
{
  "scripts": {
    "dev": "npm-run-all --parallel --print-label dev:react dev:mockapi",
    "dev:react": "react-scripts start",
    "dev:mockapi": "npx ts-node --project ./tsconfig.server.json server/index.ts",
  },
  "proxy": "http://localhost:8080",
}

その他メモ

express-sessionの「secret」って何?

↓で設定していたsecret。これ何に使ってるのって話。

app.use(
  session({
    secret: "secret",
    cookie: {},
  })
);

公式の説明によれば、セッションIDが格納されているCookieをsignするためにsecretを使っているとのこと。

This is the secret used to sign the session ID cookie.

Cookieをsignするってどゆこと?

と思い、ぐぐったらcookie-signatureというライブラリがあるらしく、おそらくこいつと同等のことをやってると思われる。

# ブラウザ側に送信するのは【セッションIDが格納されたCookieをsignしたもの】
var singedSessionIDCookie = cookie.sign('sessionID', 'secret');
# その後リクエストが来るたびに、SignedCookieをunsignしてセッションIDを取得&検証
var unsignedSessionIDCookie = cookie.unsign('署名済sessionIDCookie', 'secret')

signすることで、ブラウザ側に生のsessionIDを渡さない。よりSECUREだね👍

package.jsonの「proxy」

・create-react-appで使うプロパティ。
・ローカル開発環境でReactアプリからAPIサーバをコールしたい時に利用可能。

package.json
{
  "proxy": "http://localhost:8080",
}

なぜ必要なのか、設定ありなしの場合を比較する。

proxyの設定をしない場合

# 異なるオリジンのためReactアプリからAPIコールするとCORSエラーとなる。
ブラウザ → Reactアプリ(localhost:3000)
     → Mock用API(localhost:8080)

proxyの設定をした場合

# APIコールもReactアプリを経由するのでCORSエラーとならない。
ブラウザ → Reactアプリ(localhost:3000) → Mock用API(localhost:8080)

ポイント
Acceptヘッダがtext/html以外のリクエストを全てproxyに設定したURLにプロキシする。
✅APIが複数ある場合は、proxyから各APIをコールするようにexpressを設定すればOK👍

Discussion