Web 应用里,身份态从来都是港口通行证问题。
Remix 的 Session 管理很有代表性: 不是全局魔法中间件式黑盒,而是每个路由按需在 loader/action 中读取和提交。
简化示例:
// 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:
// 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:
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 很远的仪式。