工作階段是網站的重要組成部分,它允許伺服器識別來自同一個人的請求,尤其是在伺服器端表單驗證或頁面上沒有 JavaScript 時。工作階段是許多允許使用者「登入」的網站的基本構建模組,包括社交、電子商務、商業和教育網站。
當使用 React Router 作為框架時,工作階段是根據每個路由進行管理的(而不是像 express 中介軟體),在您的 loader
和 action
方法中使用「工作階段儲存」物件(它實作了 SessionStorage
介面)。工作階段儲存了解如何解析和產生 Cookie,以及如何在資料庫或檔案系統中儲存工作階段資料。
這是一個 Cookie 工作階段儲存的範例
import { createCookieSessionStorage } from "react-router";
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: "reactrouter.com",
// 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.server.ts` 中設定您的工作階段儲存物件,以便所有需要存取工作階段資料的路由都可以從同一個位置匯入。
工作階段儲存物件的輸入/輸出是 HTTP Cookie。getSession()
從傳入請求的 Cookie
標頭檢索目前的工作階段,而 commitSession()
/destroySession()
為傳出回應提供 Set-Cookie
標頭。
您將在 loader
和 action
函式中使用方法來存取工作階段。
使用 getSession
檢索工作階段後,傳回的工作階段物件具有一些方法和屬性
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
session.get("foo");
session.has("bar");
// etc.
}
請參閱 Session API 以了解工作階段物件上可用的所有方法。
登入表單可能看起來像這樣
import { data, redirect } from "react-router";
import type { Route } from "./+types/login";
import {
getSession,
commitSession,
} from "../sessions.server";
export async function loader({
request,
}: Route.LoaderArgs) {
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("/");
}
return data(
{ error: session.get("error") },
{
headers: {
"Set-Cookie": await commitSession(session),
},
}
);
}
export async function action({
request,
}: Route.ActionArgs) {
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({
loaderData,
}: Route.ComponentProps) {
const { error } = loaderData;
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.server";
import type { Route } from "./+types/logout";
export async function action({
request,
}: Route.ActionArgs) {
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
中登出(或執行任何變更)。否則,您的使用者將面臨 跨站請求偽造 攻擊。
由於巢狀路由,可能會呼叫多個 loader 來建構單一頁面。當使用 session.flash()
或 session.unset()
時,您需要確保請求中的其他 loader 不會想要讀取它,否則您會遇到競爭條件。通常,如果您使用 flash,您會希望有一個 loader 讀取它,如果另一個 loader 想要 flash 訊息,請為該 loader 使用不同的金鑰。
React Router 讓您在需要時輕鬆地將工作階段儲存在自己的資料庫中。createSessionStorage()
API 需要一個 cookie
(有關建立 Cookie 的選項,請參閱 Cookie)和一組用於管理工作階段資料的建立、讀取、更新和刪除 (CRUD) 方法。Cookie 用於持久化工作階段 ID。
createData
將在初始工作階段建立時從 commitSession
呼叫readData
將從 getSession
呼叫updateData
將從 commitSession
呼叫deleteData
從 destroySession
呼叫以下範例示範如何使用通用資料庫用戶端執行此操作
import { createSessionStorage } from "react-router";
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",
},
});
createData
和 updateData
的 expires
引數與 Cookie 本身過期的 Date
相同,並且不再有效。您可以使用此資訊自動從資料庫中清除工作階段記錄以節省空間,或確保您不會為舊的、過期的 Cookie 傳回任何資料。
如果需要,還有其他幾個工作階段工具可用
isSession
createMemorySessionStorage
createSession
(自訂儲存)createFileSessionStorage
(node)createWorkersKVSessionStorage
(Cloudflare Workers)createArcTableSessionStorage
(architect, Amazon DynamoDB)Cookie 是一種小資訊,您的伺服器在 HTTP 回應中傳送給某人,他們的瀏覽器會在後續請求中傳送回伺服器。此技術是許多互動式網站的基本構建模組,它新增了狀態,因此您可以建立身份驗證(請參閱工作階段)、購物車、使用者偏好設定以及許多其他需要記住誰「已登入」的功能。
React Router 的 Cookie
介面為 Cookie metadata 提供了邏輯、可重複使用的容器。
雖然您可以手動建立這些 Cookie,但更常見的是使用工作階段儲存。
在 React Router 中,您通常會在 loader
和/或 action
函式中使用 Cookie,因為這些是您需要讀取和寫入資料的地方。
假設您的電子商務網站上有一個橫幅,提示使用者查看您目前正在促銷的商品。橫幅橫跨您的首頁頂部,並在側面包含一個按鈕,允許使用者關閉橫幅,以便他們至少在一周內不會再看到它。
首先,建立一個 Cookie
import { createCookie } from "react-router";
export const userPrefs = createCookie("user-prefs", {
maxAge: 604_800, // one week
});
然後,您可以 import
Cookie 並在 loader
和/或 action
中使用它。在這種情況下,loader
僅檢查使用者偏好設定的值,以便您可以在元件中使用它來決定是否渲染橫幅。當按鈕被點擊時,<form>
會呼叫伺服器上的 action
並重新載入沒有橫幅的頁面。
import { Link, Form, redirect } from "react-router";
import type { Route } from "./+types/home";
import { userPrefs } from "../cookies.server";
export async function loader({
request,
}: Route.LoaderArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
return { showBanner: cookie.showBanner };
}
export async function action({
request,
}: Route.ActionArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();
if (bodyParams.get("bannerVisibility") === "hidden") {
cookie.showBanner = false;
}
return redirect("/", {
headers: {
"Set-Cookie": await userPrefs.serialize(cookie),
},
});
}
export default function Home({
loaderData,
}: Route.ComponentProps) {
return (
<div>
{loaderData.showBanner ? (
<div>
<Link to="/sale">Don't miss our sale!</Link>
<Form method="post">
<input
type="hidden"
name="bannerVisibility"
value="hidden"
/>
<button type="submit">Hide</button>
</Form>
</div>
) : null}
<h1>Welcome!</h1>
</div>
);
}
Cookie 有幾個屬性,用於控制它們何時過期、如何存取以及傳送到哪裡。這些屬性中的任何一個都可以在 createCookie(name, options)
中指定,也可以在產生 Set-Cookie
標頭時在 serialize()
期間指定。
const cookie = createCookie("user-prefs", {
// These are defaults for this cookie.
path: "/",
sameSite: "lax",
httpOnly: true,
secure: true,
expires: new Date(Date.now() + 60_000),
maxAge: 60,
});
// You can either use the defaults:
cookie.serialize(userPrefs);
// Or override individual ones as needed:
cookie.serialize(userPrefs, { sameSite: "strict" });
請閱讀關於這些屬性的更多資訊,以更好地了解它們的作用。
可以簽署 Cookie 以在收到時自動驗證其內容。由於偽造 HTTP 標頭相對容易,因此對於您不希望任何人能夠偽造的任何資訊(例如身份驗證資訊(請參閱工作階段))來說,這是一個好主意。
若要簽署 Cookie,請在第一次建立 Cookie 時提供一個或多個 secrets
const cookie = createCookie("user-prefs", {
secrets: ["s3cret1"],
});
具有一個或多個 secrets
的 Cookie 將以確保 Cookie 完整性的方式儲存和驗證。
可以透過將新的 secrets
新增到 secrets
陣列的前面來輪換 secrets
。使用舊的 secrets
簽署的 Cookie 仍然可以在 cookie.parse()
中成功解碼,並且最新的 secret
(陣列中的第一個)將始終用於簽署在 cookie.serialize()
中建立的傳出 Cookie。
export const cookie = createCookie("user-prefs", {
secrets: ["n3wsecr3t", "olds3cret"],
});
import { data } from "react-router";
import { cookie } from "../cookies.server";
import type { Route } from "./+types/my-route";
export async function loader({
request,
}: Route.LoaderArgs) {
const oldCookie = request.headers.get("Cookie");
// oldCookie may have been signed with "olds3cret", but still parses ok
const value = await cookie.parse(oldCookie);
return data("...", {
headers: {
// Set-Cookie is signed with "n3wsecr3t"
"Set-Cookie": await cookie.serialize(value),
},
});
}
如果需要,還有其他幾個 Cookie 工具可用
若要了解有關每個屬性的更多資訊,請參閱 MDN Set-Cookie 文件。