createSession
isSession
createSessionStorage
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(Node.js)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 中间件那样)在你的 loader
和 action
方法中使用“会话存储”对象(实现 SessionStorage
接口)。会话存储了解如何解析和生成 cookie,以及如何在数据库或文件系统中存储会话数据。
Remix 附带了一些针对常见场景的预构建会话存储选项,以及一个用于创建自定义存储的选项
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(Node.js)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 仅在调用 action
时重新调用 loader
。
由于嵌套路由,可以调用多个加载器来构建单个页面。当使用 session.flash()
或 session.unset()
时,你需要确保请求中的其他加载器不会读取它,否则你会遇到竞争条件。通常,如果你使用闪存,你希望单个加载器读取它,如果另一个加载器想要闪存消息,请为此加载器使用不同的键。
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 的选项,请参阅 cookie)以及一组用于管理会话数据的创建、读取、更新和删除 (CRUD) 方法。Cookie 用于持久化会话 ID。
createData
将在没有 cookie 中的会话 ID 时,从 commitSession
中调用,用于初始会话创建。readData
将在 cookie 中存在会话 ID 时,从 getSession
中调用。updateData
将在 cookie 中已存在会话 ID 时,从 commitSession
中调用。deleteData
从 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 中,请参阅 cookie),你可以使用 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.js)对于基于文件的会话,请使用 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 作为 Session 存储的优势在于,只有 Session 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
获取 Session 后,返回的 Session 对象包含一些方法和属性。
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
session.get("foo");
session.has("bar");
// etc.
}
session.has(key)
如果 Session 中存在具有给定 name
的变量,则返回 true
。
session.has("userId");
session.set(key, value)
设置 Session 值,以便在后续请求中使用。
session.set("userId", "1234");
session.flash(key, value)
设置一个 Session 值,该值在第一次读取时会被取消设置。之后,它就会消失。最常用于“闪存消息”和服务器端表单验证消息。
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
,都必须提交 Session。这与您可能习惯的不同,在某些类型的中间件中,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 值。
session.get("name");
session.unset()
从 Session 中删除一个值。
session.unset("name");
unset
都必须提交 Session。
export async function loader({
request,
}: LoaderFunctionArgs) {
// ...
return json(data, {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}