mcpField
R
remix-v2-meta-sessions

remix-v2-meta-sessions

github.com/existential-birds开发8540 次安装11 周前更新
代码仓库

关于 remix-v2-meta-sessions

用于审查 Remix v2 元数据、会话、身份验证和 CSRF 处理的参考技能,说明特定版本的 API,并为路由元数据和受保护请求提供决策步骤。

主要功能

  1. 01Remix v2 meta 描述符指导
  2. 02父级元数据合并方式
  3. 03会话提交与闪存消息处理
  4. 04loader 和 action 身份验证模式
  5. 05CSRF 风险与令牌指导

适用场景

  1. 01检查路由元数据行为
  2. 02核查会话持久化与闪存消息
  3. 03审查受保护的 loader 和 action
  4. 04评估变更请求的 CSRF 处理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill remix-v2-meta-sessions

安装后在 Claude Code 中输入/remix-v2-meta-sessions确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
auth-csrf.md
links.md
meta-v2.md
sessions.md

详细介绍

Remix v2 Meta、会话、身份验证和 CSRF

快速参考

v2 的 meta 返回描述符对象数组——而不是 v1 的对象结构。 在过时的代码库中,v1 风格的对象字面量仍可通过类型检查,但运行时不会渲染任何标签。

// app/routes/posts.$slug.tsx
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  if (!data?.post) return [{ title: "Not Found" }];
  return [
    { title: `${data.post.title} | My Blog` },
    { name: "description", content: data.post.excerpt },
    { property: "og:title", content: data.post.title },
    { tagName: "link", rel: "canonical", href: data.post.url },
  ];
};

采用安全默认值并支持密钥轮换的 Cookie 会话存储:

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

type SessionData = { userId: string };
type SessionFlashData = { error: string };

const SESSION_SECRET = process.env.SESSION_SECRET;
if (!SESSION_SECRET) throw new Error("SESSION_SECRET is required");

export const { getSession, commitSession, destroySession } =
  createCookieSessionStorage<SessionData, SessionFlashData>({
    cookie: {
      name: "__session",
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
      maxAge: 60 * 60 * 24 * 30,
      secrets: [
        SESSION_SECRET,
        ...(process.env.SESSION_SECRET_OLD ? [process.env.SESSION_SECRET_OLD] : []),
      ],
    },
  });

文档头部:meta 和 links

<Meta /> 和 <Links /> 必须放在 root.tsx 的 <head> 内; <ScrollRestoration />、<Scripts /> 和 <LiveReload /> 则应放在 <body> 末尾。缺少其中任一聚合组件,都可能导致“css doesn't load”(CSS 未加载)或“meta tags missing”(缺少 meta 标签),且不会出现编译错误。

// app/root.tsx
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

<Meta /> 和 <Links /> 的聚合方式不同。 <Links /> 会遍历 整个路由匹配链,并渲染每个匹配路由导出的 links ——叶子路由中声明的样式表会自动渲染,并在导航离开时卸载。<Meta /> 不会聚合;Remix 只会选取最后一个匹配路由的 meta 数组。若要在 meta 中继承父级内容,请显式对 matches 执行 flatMap:

import type { MetaFunction } from "@remix-run/node";
import type { loader as projectLoader } from "./project.$pid";

export const meta: MetaFunction<
  typeof loader,
  { "routes/project.$pid": typeof projectLoader }
> = ({ data, matches }) => {
  const parentMeta = matches.flatMap((m) => m.meta ?? []);
  const project = matches.find((m) => m.id === "routes/project.$pid")?.data;
  return [
    ...parentMeta,
    { title: `${data?.task.name} | ${project?.name}` },
  ];
};

MetaFunction 的第二个泛型(以路由 ID 为键)会为父级路由的 matches.find(...).data 提供类型。参见 references/meta-v2.md。

会话

每个变更响应都必须将 commitSession 附加为 Set-Cookie 头。Remix 不会自动提交;调用 session.set(...) 后返回普通的 json(data) 会悄无声息地丢弃更改。

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

session.flash(key, value) 只能读取一次;读取它的 loader 仍必须在读取后调用 commitSession,以清除 flash。参见 references/sessions.md。

身份验证:在 loader 中抛出 redirect

标准模式是使用 requireUserId(request) 辅助函数,对未通过身份验证的请求抛出 redirect()。抛出的响应会短路 loader;无需顶层 return。

// app/auth.server.ts
import { redirect } from "@remix-run/node";
import { getSession } from "./session.server";

export async function requireUserId(request: Request): Promise<string> {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");
  if (!userId) {
    const url = new URL(request.url);
    const redirectTo = `${url.pathname}${url.search}`;
    throw redirect(`/login?redirectTo=${encodeURIComponent(redirectTo)}`);
  }
  return userId;
}

绝不要在 React 组件内部限制路由访问——受保护组件仍会进行 SSR,并向未通过身份验证的用户发送 HTML/loader 数据。参见 references/auth-csrf.md。

CSRF

Remix 没有内置的 CSRF 防护。 同源 <Form> 提交完全依赖你为会话 Cookie 设置的 SameSite 值。 SameSite=Lax 会在所有当前浏览器中阻止跨站 POST 导航携带 Cookie。(Chrome 曾在 2020 年短暂支持 2 分钟的“Lax+POST”窗口——该功能已于 2021 年移除。)真正的 Lax 与 Strict 权衡在于子域接管:使用 Lax 时,被攻陷的子域可以携带凭据发起顶层 GET 导航;使用 Strict 时,来自外部站点的深层链接导航会丢失会话。出于正当的跨站需求使用 SameSite=None 的应用(OAuth 弹窗、iframe 嵌入)完全没有 Cookie 层面的 CSRF 防护。 建议使用 remix-utils/csrf,并配备专用签名 Cookie——绝不要复用会话 Cookie。手动调用 fetch("/api/x", { method: "POST" }) 会绕过 AuthenticityTokenInput,因此任何未调用 csrf.validate(request) 的 action 都是攻击者的入口。

闸门(决策顺序)

按顺序回答。通过表示条件为真;选择同一行上的 API,然后停止。

这个 meta 标签应放在哪里?
  1. 它是否适用于整个站点(charset、viewport、默认 OG 图片)?
    • 通过 → 在 root.tsx 的 <head> 中直接使用 JSX。这样可避免 v2 不合并的意外行为,并防止重复标签。停止。
    • 未通过 → 第 2 步。
  2. 它是否仅适用于特定路由(title、description、canonical、JSON-LD)?
    • 通过 → 在叶子路由文件中使用 export const meta;如果需要父级值,请使用 matches.flatMap((m) => m.meta ?? [])。停止。
身份验证检查:loader、action 还是辅助函数?
  1. 这是必须保护的 GET(页面渲染)吗?
    • 是 → 在 loader 顶部调用 await requireUserId(request)。结束。
  2. 这是必须保护的 POST/PUT/DELETE 变更操作吗?
    • 是 → 在 action 顶部调用 await requireUserId(request),并调用 await csrf.validate(request)。结束。
  3. 退出登录?
    • 是 → 只能使用 action,绝不要使用 loader。指向 loader 的 <Link to="/logout"> 可通过 <img src="/logout"> 发起 CSRF 攻击。请使用 <Form method="post" action="/logout">。结束。
CSRF 令牌存放在哪里?
  1. 你在使用 remix-utils/csrf 吗?
    • 是 → 使用专用的 createCookie("csrf", { ... }) Cookie,与会话 Cookie 分开。CSRF 值是经过签名的字符串;会话值是序列化对象——重复使用同一个 Cookie 会导致验证时报错。结束。
    • 否 → 转到第 2 步。
  2. 没有 CSRF 库?
    • 是 → 说明威胁模型;会话 Cookie 必须设置 sameSite: "strict",并在每个 action 中验证 Origin 标头。建议改用 remix-utils/csrf。

其他文档

  • Meta v2:关于描述符类型、通过 matches 合并父级数据、JSON-LD,以及 v1→v2 迁移中的注意事项,请参阅 references/meta-v2.md。
  • Links:关于样式表、预加载、dns-prefetch,以及 <Links /> 的父级聚合行为,请参阅 references/links.md。
  • Sessions:关于 createCookieSessionStorage 配置、commitSession/destroySession 模式、Flash 消息以及数据库支持的会话,请参阅 references/sessions.md。
  • 身份验证和 CSRF:关于 requireUserId 辅助函数、登录/退出登录 action、remix-auth 以及 remix-utils/csrf 的配置,请参阅 references/auth-csrf.md。

v1 与 v2 快速对比

注意事项v1v2
meta 返回形式对象 { title, description }数组 [{ title }, { name, content }]
父级 meta 合并自动合并(每个键以后写入的值为准)不合并;仅使用最后一个匹配的路由
父级数据的 meta 参数parentsDatamatches(需手动使用 flatMap)
OG 标签{ "og:title": "..." } 简写{ property: "og:title", content: "..." }
迁移标记v2_meta: true future flagN/A(v2 默认)

评论

支持 Markdown