createSessionisSessioncreateSessionStoragecreateCookieSessionStoragecreateMemorySessionStoragecreateFileSessionStorage (node)createWorkersKVSessionStorage (Cloudflare Workers)createArcTableSessionStorage (architect, Amazon DynamoDB)session.has(key)session.set(key, value)session.flash(key, value)session.get()session.unset()会话是网站的重要组成部分,它允许服务器识别来自同一用户的请求,尤其是在服务器端表单验证或页面上没有 JavaScript 的情况下。会话是许多允许用户“登录”的网站的基本构建块,包括社交网站、电子商务网站、商业网站和教育网站。
在 Remix 中,会话是在每个路由的基础上(而不是像 express 中间件那样)使用“会话存储”对象(它实现了 SessionStorage 接口)在您的 loader 和 action 方法中管理的。会话存储了解如何解析和生成 Cookie,以及如何将会话数据存储在数据库或文件系统中。
Remix 为常见场景提供了几种预构建的会话存储选项,以及一个用于创建您自己的会话存储选项的函数
createCookieSessionStoragecreateMemorySessionStoragecreateFileSessionStorage (node)createWorkersKVSessionStorage (Cloudflare Workers)createArcTableSessionStorage (architect, Amazon DynamoDB)createSessionStorage 自定义存储这是一个 cookie 会话存储的示例
// 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 中设置您的会话存储对象,以便所有需要访问会话数据的路由都可以从同一位置导入(另请参阅我们的 路由模块约束)。
会话存储对象的输入/输出是 HTTP Cookie。getSession() 从传入请求的 Cookie 标头中检索当前会话,commitSession()/destroySession() 为传出响应提供 Set-Cookie 标头。
您将使用方法来访问 loader 和 action 函数中的会话。
登录表单可能如下所示
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。
由于嵌套路由,可以调用多个加载器来构建单个页面。当使用 session.flash() 或 session.unset() 时,您需要确保请求中的其他加载器不会想要读取它,否则您将遇到竞争条件。通常,如果您使用 flash,您会希望有一个加载器读取它,如果另一个加载器想要 flash 消息,请为该加载器使用不同的键。
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。
commitSession 调用 createDatagetSession 调用 readDatacommitSession 调用 updateDatadeleteData 从 destroySession 调用以下示例展示了如何使用通用数据库客户端执行此操作
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",
},
});
createData 和 updateData 的 expires 参数是 cookie 本身过期且不再有效时的相同 Date。 您可以使用此信息自动从数据库中清除会话记录以节省空间,或确保您不返回任何旧的、过期的 cookie 的数据。
createCookieSessionStorage对于纯粹基于 cookie 的会话(其中会话数据本身与浏览器一起存储在会话 cookie 中,请参阅 cookies),您可以使用 createCookieSessionStorage()。
cookie 会话存储的主要优点是您不需要任何额外的后端服务或数据库即可使用它。 它在某些负载均衡场景中也可能很有益。 但是,基于 cookie 的会话可能不会超过浏览器允许的最大 cookie 长度(通常为 4kb)。
缺点是您几乎必须在每个加载器和操作中都执行 commitSession。 如果您的加载器或操作以任何方式更改了会话,则必须提交它。 这意味着如果您在操作中执行 session.flash,然后在另一个操作中执行 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",
},
});
请注意,其他会话实现将唯一的会话 ID 存储在 cookie 中,并使用该 ID 在真实来源(内存、文件系统、数据库等)中查找会话。 在 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 (architect, 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 };
在使用 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),
},
});
}
现在我们可以在加载器中读取消息。
flash 时,都必须提交会话。 这与您可能习惯的不同,在某些类型的中间件会自动为您设置 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");
unset 都必须提交会话
export async function loader({
request,
}: LoaderFunctionArgs) {
// ...
return json(data, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}