createSession
isSession
createSessionStorage
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(Node.js, Deno)createWorkersKVSessionStorage
(Cloudflare)createArcTableSessionStorage
(架构师,Amazon DynamoDB)session.has(key)
session.set(key, value)
session.flash(key, value)
session.get(key)
session.unset(key)
¥Sessions
会话是网站的重要组成部分,它允许服务器识别来自同一个人的请求,尤其是在服务器端表单验证或页面上没有 JavaScript 时。会话是许多允许用户使用 "登录" 的网站的基本构建块,包括社交、电商、商业和教育网站。
¥Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites.
在 Remix 中,会话是在每个路由的基础上进行管理的(而不是像 Express 中间件那样),在 loader
和 action
方法中使用 "会话存储" 对象(实现了 SessionStorage
接口)进行管理。会话存储了解如何解析和生成 Cookie,以及如何将会话数据存储在数据库或文件系统中。
¥In Remix, sessions are managed on a per-route basis (rather than something like express middleware) in your loader
and action
methods using a "session storage" object (that implements the SessionStorage
interface). Session storage understands how to parse and generate cookies and how to store session data in a database or filesystem.
Remix 提供了几个针对常见场景的预构建会话存储选项,以及一个自定义会话存储选项:
¥Remix comes with several pre-built session storage options for common scenarios and one to create your own:
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(Node.js, Deno)
createWorkersKVSessionStorage
(Cloudflare)
createArcTableSessionStorage
(架构师,Amazon DynamoDB)
¥createArcTableSessionStorage
(Architect, Amazon DynamoDB)
使用 createSessionStorage
自定义存储
¥custom storage with createSessionStorage
¥Using Sessions
这是一个 cookie 会话存储的示例:
¥This is an example of a cookie session storage:
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.server.ts
中设置会话存储对象,以便所有需要访问会话数据的路由都可以从同一位置导入(另请参阅我们的 路由模块约束)。
¥We recommend setting up your session storage object in app/sessions.server.ts
so all routes that need to access session data can import from the same spot (also, see our Route Module Constraints).
会话存储对象的输入/输出是 HTTP Cookie。getSession()
从传入请求的 Cookie
标头中检索当前会话,而 commitSession()
/destroySession()
为传出响应提供 Set-Cookie
标头。
¥The input/output to a session storage object are HTTP cookies. getSession()
retrieves the current session from the incoming request's Cookie
header, and commitSession()
/destroySession()
provide the Set-Cookie
header for the outgoing response.
你将使用方法在 loader
和 action
函数中访问会话。
¥You'll use methods to get access to sessions in your loader
and action
functions.
登录表单可能看起来像这样:
¥A login form might look something like this:
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.server";
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 = {
// Read and unset the flash message set by the route action.
error: session.get("error"),
};
return json(data, {
headers: {
// Commit the updated session data.
"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) {
// Set a single-use flash message to be read by the route loader.
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>
);
}
然后注销表单可能看起来像这样:
¥And then a logout form might look something like this:
import {
getSession,
destroySession,
} from "../sessions.server";
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 Gotchas
由于存在嵌套路由,因此可以调用多个加载器来构建单个页面。使用 session.flash()
或 session.unset()
时,你需要确保请求中没有其他加载器想要读取该加载器,否则你将遇到竞争条件。通常,如果你使用的是 Flash,则需要使用单个加载器来读取它;如果另一个加载器需要闪现消息,请为该加载器使用不同的键。
¥Because of nested routes, multiple loaders can be called to construct a single page. When using session.flash()
or session.unset()
, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically, if you're using flash, you'll want to have a single loader read it; if another loader wants a flash message, use a different key for that loader.
每次修改会话数据时,都必须执行 commitSession()
操作,否则你的更改将会丢失。这与你习惯的做法不同,某些类型的中间件会自动为你提交会话数据。
¥Every time you modify session data, you must commitSession()
or your changes will be lost. This is different from what you might be used to, where some type of middleware automatically commits session data for you.
createSession
TODO:
isSession
如果对象是 Remix 会话,则返回 true
。
¥Returns true
if an object is a Remix session.
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。
¥Remix makes it easy to store sessions in your own database if needed. The createSessionStorage()
API requires a cookie
(for options for creating a cookie, see cookies) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.
当 cookie 中不存在会话 ID 时,commitSession
会在初始会话创建时调用 createData
。
¥createData
will be called from commitSession
on the initial session creation when no session ID exists in the cookie
当 cookie 中存在会话 ID 时,getSession
会调用 readData
。
¥readData
will be called from getSession
when a session ID exists in the cookie
当 cookie 中已存在会话 ID 时,commitSession
会调用 updateData
。
¥updateData
will be called from commitSession
when a session ID already exists in the cookie
deleteData
从 destroySession
调用
¥deleteData
is called from destroySession
以下示例展示了如何使用通用数据库客户端执行此操作:
¥The following example shows how you could do this using a generic database client:
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);
},
});
}
然后你可以像这样使用它:
¥And then you can use it like this:
const { getSession, commitSession, destroySession } =
createDatabaseSessionStorage({
host: "localhost",
port: 1234,
cookie: {
name: "__session",
sameSite: "lax",
},
});
createData
和 updateData
的 expires
参数与 Date
相同,Date
表示 cookie 本身过期且不再有效。你可以使用此信息自动从数据库中清除会话记录,以节省空间,或确保不会返回任何旧的、过期的 Cookie 数据。
¥The expires
argument to createData
and updateData
is the same Date
at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space or to ensure that you do not otherwise return any data for old, expired cookies.
createCookieSessionStorage
对于纯粹基于 Cookie 的会话(会话数据本身存储在浏览器的会话 Cookie 中,请参阅 cookies),你可以使用 createCookieSessionStorage()
。
¥For purely cookie-based sessions (where the session data itself is stored in the session cookie with the browser, see cookies) you can use createCookieSessionStorage()
.
Cookie 会话存储的主要优势在于你无需任何额外的后端服务或数据库即可使用它。在某些负载均衡场景中,它也很有用。
¥The main advantage of cookie session storage is that you don't need any additional backend services or databases to use it. It can also be beneficial in some load-balanced scenarios.
然而,基于 Cookie 的会话不得超过浏览器 Cookie 大小限制 4k 字节。如果你的 Cookie 大小超过此限制,commitSession()
将抛出错误。
¥However, cookie-based sessions may not exceed browser cookie size limits of 4k bytes. If your cookie size exceeds this limit, commitSession()
will throw an error.
另一个缺点是,你需要在每个 loader 和修改会话的操作中更新 Set-Cookie
标头(这包括读取闪存的会话值)。使用其他策略,你只需设置一次会话 Cookie,因为它不存储任何会话数据,只存储用于在其他地方找到它的密钥。
¥The other downside is that you need to update the Set-Cookie
header in every loader and action that modifies the session (this includes reading a flashed session value). With other strategies you only need to set the session cookie once, because it doesn't store any session data, just the key to find it elsewhere.
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",
},
});
请注意,其他会话实现会在 Cookie 中存储唯一的会话 ID,并使用该 ID 在真实来源(内存、文件系统、数据库等)中查找会话。在 Cookie 会话中,Cookie 是真实来源,因此没有现成的唯一 ID。如果你需要在 Cookie 会话中跟踪唯一 ID,则需要通过 session.set()
自行添加 ID 值。
¥Note that other session implementations store a unique session ID in a cookie and use that ID to look up the session in the source of truth (in-memory, filesystem, DB, etc.). In a cookie session, the cookie is the source of truth so there is no unique ID out of the box. If you need to track a unique ID in your cookie session, you will need to add an ID value yourself via session.set()
.
createMemorySessionStorage
此存储将所有 Cookie 信息保存在服务器内存中。
¥This storage keeps all the cookie information in your server's memory.
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, Deno)对于文件支持的会话,请使用 createFileSessionStorage()
。文件会话存储需要文件系统,但这在大多数运行 Express 的云服务提供商上应该都已提供,可能需要一些额外的配置。
¥For file-backed sessions, use createFileSessionStorage()
. File session storage requires a file system, but this should be readily available on most cloud providers that run express, maybe with some extra configuration.
基于文件 (File-Backed) 的会话的优势在于,只有会话 ID 存储在 cookie 中,其余数据则存储在磁盘上的常规文件中,非常适合数据量超过 4k 字节的会话。
¥The advantage of file-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a regular file on disk, ideal for sessions with more than 4k bytes of data.
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)对于支持 Cloudflare Workers 键值对 的会话,请使用 createWorkersKVSessionStorage()
。
¥For Cloudflare Workers KV backed sessions, use createWorkersKVSessionStorage()
.
基于键值对 (KV) 的会话的优势在于,只有会话 ID 存储在 cookie 中,其余数据则存储在全局复制的低延迟数据存储中,从而支持极高的读取量。
¥The advantage of KV-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store supporting exceptionally high read volumes.
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
(架构师,Amazon DynamoDB)¥createArcTableSessionStorage
(Architect, Amazon DynamoDB)
对于支持 Amazon DynamoDB 的会话,请使用 createArcTableSessionStorage()
。
¥For Amazon DynamoDB backed sessions, use createArcTableSessionStorage()
.
DynamoDB 支持的会话的优势在于,只有会话 ID 存储在 Cookie 中,其余数据则存储在全局复制、低延迟的数据存储中,从而支持极高的读取量。
¥The advantage of DynamoDB-backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store supporting exceptionally high read volumes.
# 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 };
¥Session API
使用 getSession()
检索会话后,返回的会话对象包含一些方法来读取和更新检索到的会话数据:
¥After retrieving a session with getSession()
, the returned session object has a handful of methods to read and update the retrieved session data:
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
session.get("foo");
session.unset("bar");
// etc.
await commitSession(session);
}
commitSession()
操作,否则你的更改将会丢失。
commitSession()
时都必须执行 Set-Cookie
,否则更改将丢失。
session.has(key)
如果会话中包含具有给定 name
的变量,则返回 true
。
¥Returns true
if the session has a variable with the given name
.
session.has("userId");
session.set(key, value)
设置会话值以供后续请求使用:
¥Sets a session value for use in subsequent requests:
session.set("userId", "1234");
session.flash(key, value)
设置一个会话值,该值将在后续请求中首次读取时被取消设置。之后,它就消失了。对于 "闪现消息" 和服务器端表单验证消息最有用:
¥Sets a session value that will be unset the first time it is read in a subsequent request. After that, it's gone. Most useful for "flash messages" and server-side form validation messages:
session.flash(
"globalMessage",
"Project successfully archived"
);
session.get(key)
访问上一个请求的会话值:
¥Accesses a session value from a previous request:
session.get("name");
session.unset(key)
从会话中删除一个值。
¥Removes a value from the session.
session.unset("name");