会话
On this page

会话

¥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 中间件那样),在 loaderaction 方法中使用 "会话存储" 对象(实现了 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.

你将使用方法在 loaderaction 函数中访问会话。

¥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

  • deleteDatadestroySession 调用

    ¥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",
    },
  });

createDataupdateDataexpires 参数与 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 };

会话 API

¥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() 操作,否则你的更改将会丢失。

使用 Cookie 会话存储时,每次执行 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");
Remix v2.17 中文网 - 粤ICP备13048890号