全书目录

第三十一篇:Remix 完全指南 —— Web 原生航运港宇宙

十七、Session 和登录:船员通行证系统

1 分钟 223 字 第 580 / 962 个阅读单元

Web 应用里,身份态从来都是港口通行证问题。

Remix 的 Session 管理很有代表性: 不是全局魔法中间件式黑盒,而是每个路由按需在 loader/action 中读取和提交。

简化示例:

tsx
// app/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";

export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    httpOnly: true,
    path: "/",
    sameSite: "lax",
    secrets: ["a-very-secret-key"],
    secure: process.env.NODE_ENV === "production",
  },
});

export const { getSession, commitSession, destroySession } = sessionStorage;

登录 action:

tsx
// app/routes/_auth.login.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { getSession, commitSession } from "~/session.server";
import { verifyUser } from "~/models/user.server";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email"));
  const password = String(formData.get("password"));

  const user = await verifyUser(email, password);
  if (!user) {
    return { error: "账号或密码错误" };
  }

  const session = await getSession();
  session.set("userId", user.id);

  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

export default function LoginRoute() {
  return (
    <Form method="post">
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button type="submit">登录</button>
    </Form>
  );
}

受保护 loader:

tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");

  if (!userId) {
    throw redirect("/login");
  }

  return json({ userId });
}

要点是:

  • 登录态检查通常在 loader
  • 登录/退出通常在 action
  • Cookie 提交通过响应头完成
  • 很贴近 HTTP 本身

这就是 Remix 的一贯风格: 不发明一套离 Web 很远的仪式。