会话是网站的重要组成部分,它允许服务器识别来自同一个人的请求,尤其是在服务器端进行表单验证或页面上没有 JavaScript 时。会话是许多网站的基础构件,这些网站允许用户“登录”,包括社交、电子商务、商业和教育网站。
当使用 React Router 作为你的框架时,会话是在你的 loader
和 action
方法中基于每个路由进行管理的(而不是像 express 中间件那样),通过一个“会话存储”对象(实现了 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
中。否则,你会让你的用户面临跨站请求伪造(CSRF)攻击的风险。
由于嵌套路由的存在,多个 loader 可能会被调用来构建一个页面。当使用 session.flash()
或 session.unset()
时,你需要确保请求中没有其他 loader 会读取该值,否则你会遇到竞态条件。通常情况下,如果你使用 flash,你会希望只有一个 loader 读取它。如果另一个 loader 需要一个 flash 消息,请为该 loader 使用一个不同的键。
如果需要,React Router 可以让你轻松地将会话存储在自己的数据库中。createSessionStorage()
API 需要一个 cookie
(关于创建 cookie 的选项,请参见 cookies)和一套用于管理会话数据的创建、读取、更新和删除(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 元数据提供了一个逻辑上可重用的容器。
虽然你可以手动创建这些 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 有几个属性,它们控制着 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 将以一种确保其完整性的方式进行存储和验证。
可以通过将新的密钥添加到 secrets
数组的前面来轮换密钥。用旧密钥签名的 cookie 在 cookie.parse()
中仍然可以成功解码,而最新的密钥(数组中的第一个)将始终用于对 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 文档。