会话是网站的重要组成部分,它允许服务器识别来自同一人的请求,尤其是在服务器端表单验证或页面上没有 JavaScript 时。会话是许多允许用户“登录”的网站的基本构建块,包括社交、电子商务、商业和教育网站。
当使用 React Router 作为框架时,会话是在每个路由的基础上管理的(而不是像 express 中间件那样),在你的 loader 和 action 方法中使用“会话存储”对象(它实现了 SessionStorage 接口)。会话存储了解如何解析和生成 cookies,以及如何在数据库或文件系统中存储会话数据。
这是一个 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 cookies。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>
</>
);
}
由于嵌套路由,可以调用多个 loader 来构建单个页面。当使用 session.flash() 或 session.unset() 时,你需要确保请求中的其他 loader 不会想要读取它,否则你会遇到竞态条件。通常,如果你正在使用 flash,你将希望有一个 loader 读取它,如果另一个 loader 想要 flash 消息,请为该 loader 使用不同的键。
如果需要,React Router 可以轻松地将会话存储在你自己的数据库中。createSessionStorage() API 需要一个 cookie(有关创建 cookie 的选项,请参阅cookies)和一组用于管理会话数据的创建、读取、更新和删除 (CRUD) 方法。cookie 用于持久化会话 ID。
以下示例展示了如何使用通用数据库客户端来做到这一点
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 相同。你可以使用此信息从数据库中自动清除会话记录以节省空间,或确保你不会为旧的、过期的 cookies 返回任何数据。
如果你需要,还有其他几个会话实用工具可用
isSession
createMemorySessionStorage
createSession
(自定义存储)createFileSessionStorage
(node)createWorkersKVSessionStorage
(Cloudflare Workers)createArcTableSessionStorage
(architect, Amazon DynamoDB)cookie 是你的服务器在 HTTP 响应中发送给某人的一小段信息,他们的浏览器将在后续请求中发回。这项技术是许多交互式网站的基本构建块,它添加了状态,因此你可以构建身份验证(请参阅会话)、购物车、用户偏好设置以及许多其他需要记住谁“已登录”的功能。
React Router 的 Cookie
接口为 cookie 元数据提供了一个逻辑的、可重用的容器。
虽然你可以手动创建这些 cookies,但更常见的是使用会话存储。
在 React Router 中,你通常会在你的 loader 和/或 action 函数中使用 cookies,因为这些是你需要读取和写入数据的地方。
假设你的电子商务网站上有一个横幅,提示用户查看你当前正在促销的商品。横幅横跨你的主页顶部,并在侧面包含一个按钮,允许用户关闭横幅,以便他们至少在一周内不再看到它。
首先,创建一个 cookie
import { createCookie } from "react-router";
export const userPrefs = createCookie("user-prefs", {
maxAge: 604_800, // one week
});
然后,你可以导入 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>
);
}
Cookies 有几个属性,用于控制它们的过期时间、访问方式和发送位置。这些属性中的任何一个都可以在 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 进行签名,以便在收到 cookie 时自动验证其内容。由于伪造 HTTP 标头相对容易,因此对于任何你不希望别人能够伪造的信息(如身份验证信息(请参阅会话))来说,这是一个好主意。
要签名 cookie,请在首次创建 cookie 时提供一个或多个 secrets
const cookie = createCookie("user-prefs", {
secrets: ["s3cret1"],
});
具有一个或多个 secrets
的 cookies 将以确保 cookie 完整性的方式存储和验证。
可以通过将新的 secrets 添加到 secrets 数组的前面来轮换 secrets。使用旧 secrets 签名的 cookies 仍然可以在 cookie.parse()
中成功解码,并且最新的 secret(数组中的第一个)将始终用于签名在 cookie.serialize()
中创建的传出 cookies。
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 文档。