React Router v7 已發布。 查看文件
Session
本頁內容

Sessions

Sessions 是網站的重要組成部分,它允許伺服器識別來自同一個人的請求,尤其是在伺服器端表單驗證或頁面上沒有 JavaScript 時。Sessions 是許多允許使用者「登入」的網站的基本構建模組,包括社交、電子商務、商業和教育網站。

在 Remix 中,sessions 是在每個路由的基礎上(而不是像 express 中間件那樣)在您的 loaderaction 方法中使用「session 儲存」物件(實現 SessionStorage 介面)來管理的。Session 儲存瞭解如何解析和產生 cookies,以及如何將 session 資料儲存在資料庫或檔案系統中。

Remix 提供了幾種針對常見情境的預建 session 儲存選項,以及一個用於建立您自己的

  • createCookieSessionStorage
  • createMemorySessionStorage
  • createFileSessionStorage (node)
  • createWorkersKVSessionStorage (Cloudflare Workers)
  • createArcTableSessionStorage (architect, Amazon DynamoDB)
  • 使用 createSessionStorage 的自訂儲存。

使用 Sessions

這是一個 cookie session 儲存的範例

// app/sessions.ts
import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno

type SessionData = {
  userId: string;
};

type SessionFlashData = {
  error: string;
};

const { getSession, commitSession, destroySession } =
  createCookieSessionStorage<SessionData, SessionFlashData>(
    {
      // a Cookie from `createCookie` or the CookieOptions to create one
      cookie: {
        name: "__session",

        // all of these are optional
        domain: "remix.run",
        // Expires can also be set (although maxAge overrides it when used in combination).
        // Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
        //
        // expires: new Date(Date.now() + 60_000),
        httpOnly: true,
        maxAge: 60,
        path: "/",
        sameSite: "lax",
        secrets: ["s3cret1"],
        secure: true,
      },
    }
  );

export { getSession, commitSession, destroySession };

我們建議在 app/sessions.ts 中設定您的 session 儲存物件,以便所有需要存取 session 資料的路由都可以從同一個位置導入(另請參閱我們的 路由模組限制)。

與會話儲存物件的輸入/輸出是 HTTP Cookie。getSession() 從傳入請求的 Cookie 標頭中檢索目前的會話,而 commitSession()/destroySession() 則為傳出的響應提供 Set-Cookie 標頭。

您將使用方法來存取您 loaderaction 函式中的會話。

登入表單可能看起來像這樣

import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json, redirect } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";

import { getSession, commitSession } from "../sessions";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const session = await getSession(
    request.headers.get("Cookie")
  );

  if (session.has("userId")) {
    // Redirect to the home page if they are already signed in.
    return redirect("/");
  }

  const data = { error: session.get("error") };

  return json(data, {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

export async function action({
  request,
}: ActionFunctionArgs) {
  const session = await getSession(
    request.headers.get("Cookie")
  );
  const form = await request.formData();
  const username = form.get("username");
  const password = form.get("password");

  const userId = await validateCredentials(
    username,
    password
  );

  if (userId == null) {
    session.flash("error", "Invalid username/password");

    // Redirect back to the login page with errors.
    return redirect("/login", {
      headers: {
        "Set-Cookie": await commitSession(session),
      },
    });
  }

  session.set("userId", userId);

  // Login succeeded, send them to the home page.
  return redirect("/", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

export default function Login() {
  const { error } = useLoaderData<typeof loader>();

  return (
    <div>
      {error ? <div className="error">{error}</div> : null}
      <form method="POST">
        <div>
          <p>Please sign in</p>
        </div>
        <label>
          Username: <input type="text" name="username" />
        </label>
        <label>
          Password:{" "}
          <input type="password" name="password" />
        </label>
      </form>
    </div>
  );
}

然後登出表單可能看起來像這樣

import { getSession, destroySession } from "../sessions";

export const action = async ({
  request,
}: ActionFunctionArgs) => {
  const session = await getSession(
    request.headers.get("Cookie")
  );
  return redirect("/login", {
    headers: {
      "Set-Cookie": await destroySession(session),
    },
  });
};

export default function LogoutRoute() {
  return (
    <>
      <p>Are you sure you want to log out?</p>
      <Form method="post">
        <button>Logout</button>
      </Form>
      <Link to="/">Never mind</Link>
    </>
  );
}

重要的是,您必須在 action 中登出(或執行任何變更),而不是在 loader 中。否則,您將使您的使用者容易受到跨站請求偽造攻擊。此外,Remix 僅在呼叫 actions 時才重新呼叫 loaders

會話注意事項

由於巢狀路由,可能會呼叫多個 loader 來建構單一頁面。當使用 session.flash()session.unset() 時,您需要確保請求中的其他 loader 不會想要讀取它,否則您會遇到競爭條件。通常,如果您使用 flash,您會希望只有一個 loader 讀取它,如果另一個 loader 想要快閃訊息,請為該 loader 使用不同的鍵。

createSession

待辦事項

isSession

如果物件是 Remix 會話,則返回 true

import { isSession } from "@remix-run/node"; // or cloudflare/deno

const sessionData = { foo: "bar" };
const session = createSession(sessionData, "remix-session");
console.log(isSession(session));
// true

createSessionStorage

如果需要,Remix 可以輕鬆地將會話儲存在您自己的資料庫中。createSessionStorage() API 需要一個 cookie(有關建立 Cookie 的選項,請參閱cookies)以及一組用於管理會話資料的建立、讀取、更新和刪除 (CRUD) 方法。 Cookie 用於持久化會話 ID。

  • 當 Cookie 中不存在任何會話 ID 時,將在初始會話建立時,從 commitSession 呼叫 createData
  • 當 Cookie 中存在會話 ID 時,將從 getSession 呼叫 readData
  • 當 Cookie 中已存在會話 ID 時,將從 commitSession 呼叫 updateData
  • destroySession 呼叫 deleteData

以下範例顯示如何使用一般資料庫客戶端執行此操作

import { createSessionStorage } from "@remix-run/node"; // or cloudflare/deno

function createDatabaseSessionStorage({
  cookie,
  host,
  port,
}) {
  // Configure your database client...
  const db = createDatabaseClient(host, port);

  return createSessionStorage({
    cookie,
    async createData(data, expires) {
      // `expires` is a Date after which the data should be considered
      // invalid. You could use it to invalidate the data somehow or
      // automatically purge this record from your database.
      const id = await db.insert(data);
      return id;
    },
    async readData(id) {
      return (await db.select(id)) || null;
    },
    async updateData(id, data, expires) {
      await db.update(id, data);
    },
    async deleteData(id) {
      await db.delete(id);
    },
  });
}

然後您可以像這樣使用它

const { getSession, commitSession, destroySession } =
  createDatabaseSessionStorage({
    host: "localhost",
    port: 1234,
    cookie: {
      name: "__session",
      sameSite: "lax",
    },
  });

createDataupdateDataexpires 引數是 Cookie 本身過期且不再有效的相同 Date。 您可以使用此資訊從資料庫中自動清除會話記錄,以節省空間,或確保您不會針對舊的、過期的 Cookie 返回任何資料。

createCookieSessionStorage

對於純粹基於 Cookie 的會話(其中會話資料本身與瀏覽器一起儲存在會話 Cookie 中,請參閱cookies),您可以使用 createCookieSessionStorage()

Cookie 會話儲存的主要優點是您不需要任何額外的後端服務或資料庫即可使用它。 它在某些負載平衡的情況下也可能很有用。但是,基於 Cookie 的會話可能不會超過瀏覽器允許的最大 Cookie 長度(通常為 4kb)。

缺點是您幾乎必須在每個 loader 和 action 中執行 commitSession。 如果您的 loader 或 action 有任何變更會話,則必須提交它。 這表示如果您在 action 中執行 session.flash,然後在另一個 action 中執行 session.get,則必須提交它,該快閃訊息才會消失。 使用其他會話儲存策略時,您只需在建立時提交它(瀏覽器 Cookie 不需要變更,因為它不儲存會話資料,只儲存在其他地方找到它的鍵)。

import { createCookieSessionStorage } from "@remix-run/node"; // or cloudflare/deno

const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    // a Cookie from `createCookie` or the same CookieOptions to create one
    cookie: {
      name: "__session",
      secrets: ["r3m1xr0ck5"],
      sameSite: "lax",
    },
  });

請注意,其他會話實作會在 Cookie 中儲存唯一的會話 ID,並使用該 ID 在真實來源(記憶體中、檔案系統、DB 等)中查詢會話。在 Cookie 會話中,Cookie *是*真實來源,因此沒有現成的唯一 ID。如果您需要在 Cookie 會話中追蹤唯一的 ID,您需要透過 session.set() 自己新增一個 ID 值。

createMemorySessionStorage

此儲存將所有 Cookie 資訊保留在您伺服器的記憶體中。

這應僅在開發中使用。 在生產環境中使用其他方法之一。

import {
  createCookie,
  createMemorySessionStorage,
} from "@remix-run/node"; // or cloudflare/deno

// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
  secrets: ["r3m1xr0ck5"],
  sameSite: true,
});

const { getSession, commitSession, destroySession } =
  createMemorySessionStorage({
    cookie: sessionCookie,
  });

export { getSession, commitSession, destroySession };

createFileSessionStorage (node)

對於檔案備份的會話,請使用 createFileSessionStorage()。 檔案會話儲存需要檔案系統,但這應該在大多數執行 express 的雲端供應商上都容易取得,可能需要一些額外的設定。

檔案備份會話的優點是只有會話 ID 儲存在 Cookie 中,而其餘資料則儲存在磁碟上的常規檔案中,非常適合超過 4kb 資料的會話。

如果您要部署到無伺服器函式,請確保您有權存取持久性檔案系統。 它們通常沒有任何額外的設定。

import {
  createCookie,
  createFileSessionStorage,
} from "@remix-run/node"; // or cloudflare/deno

// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
  secrets: ["r3m1xr0ck5"],
  sameSite: true,
});

const { getSession, commitSession, destroySession } =
  createFileSessionStorage({
    // The root directory where you want to store the files.
    // Make sure it's writable!
    dir: "/app/sessions",
    cookie: sessionCookie,
  });

export { getSession, commitSession, destroySession };

createWorkersKVSessionStorage (Cloudflare Workers)

對於Cloudflare Workers KV 備份的會話,請使用 createWorkersKVSessionStorage()

KV 備份會話的優點是只有會話 ID 儲存在 Cookie 中,而其餘資料則儲存在一個全域複寫、低延遲的資料儲存中,該資料儲存具有極高的讀取量和低延遲。

import {
  createCookie,
  createWorkersKVSessionStorage,
} from "@remix-run/cloudflare";

// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
  secrets: ["r3m1xr0ck5"],
  sameSite: true,
});

const { getSession, commitSession, destroySession } =
  createWorkersKVSessionStorage({
    // The KV Namespace where you want to store sessions
    kv: YOUR_NAMESPACE,
    cookie: sessionCookie,
  });

export { getSession, commitSession, destroySession };

createArcTableSessionStorage(架構、Amazon DynamoDB)

對於 Amazon DynamoDB 備份的會話,請使用 createArcTableSessionStorage()

DynamoDB 備份會話的優點是只有會話 ID 儲存在 Cookie 中,而其餘資料則儲存在一個全域複寫、低延遲的資料儲存中,該資料儲存具有極高的讀取量和低延遲。

# app.arc
sessions
  _idx *String
  _ttl TTL
import {
  createCookie,
  createArcTableSessionStorage,
} from "@remix-run/architect";

// In this example the Cookie is created separately.
const sessionCookie = createCookie("__session", {
  secrets: ["r3m1xr0ck5"],
  maxAge: 3600,
  sameSite: true,
});

const { getSession, commitSession, destroySession } =
  createArcTableSessionStorage({
    // The name of the table (should match app.arc)
    table: "sessions",
    // The name of the key used to store the session ID (should match app.arc)
    idx: "_idx",
    // The name of the key used to store the expiration time (should match app.arc)
    ttl: "_ttl",
    cookie: sessionCookie,
  });

export { getSession, commitSession, destroySession };

會話 API

在使用 getSession 檢索會話後,返回的會話物件具有一些方法和屬性

export async function action({
  request,
}: ActionFunctionArgs) {
  const session = await getSession(
    request.headers.get("Cookie")
  );
  session.get("foo");
  session.has("bar");
  // etc.
}

session.has(key)

如果會話具有具有指定 name 的變數,則返回 true

session.has("userId");

session.set(key, value)

設定會話值,以供後續請求使用

session.set("userId", "1234");

session.flash(key, value)

設定會話值,該會話值將在第一次讀取時取消設定。 之後,它就消失了。 對於「快閃訊息」和伺服器端表單驗證訊息最有用

import { commitSession, getSession } from "../sessions";

export async function action({
  params,
  request,
}: ActionFunctionArgs) {
  const session = await getSession(
    request.headers.get("Cookie")
  );
  const deletedProject = await archiveProject(
    params.projectId
  );

  session.flash(
    "globalMessage",
    `Project ${deletedProject.name} successfully archived`
  );

  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

現在我們可以在 loader 中讀取訊息。

每當您讀取 flash 時,都必須提交會話。 這與您可能習慣的有所不同,在某些情況下,某些類型的 middleware 會自動為您設定 Cookie 標頭。

import { json } from "@remix-run/node"; // or cloudflare/deno
import {
  Meta,
  Links,
  Scripts,
  Outlet,
} from "@remix-run/react";

import { getSession, commitSession } from "./sessions";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const session = await getSession(
    request.headers.get("Cookie")
  );
  const message = session.get("globalMessage") || null;

  return json(
    { message },
    {
      headers: {
        // only necessary with cookieSessionStorage
        "Set-Cookie": await commitSession(session),
      },
    }
  );
}

export default function App() {
  const { message } = useLoaderData<typeof loader>();

  return (
    <html>
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        {message ? (
          <div className="flash">{message}</div>
        ) : null}
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

session.get()

從先前的請求存取會話值

session.get("name");

session.unset()

從會話中刪除值。

session.unset("name");

使用 cookieSessionStorage 時,每當您 unset 時都必須提交會話

export async function loader({
  request,
}: LoaderFunctionArgs) {
  // ...

  return json(data, {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}
文件和範例依授權 MIT