☰
Papermark 实践指南:让 Server Actions 像 API 路由一样完成认证(Authenticate Server Actions Like API Routes)
2026/10/3 8:14:15 网站建设 项目流程
  • 后端
  • 前端
  • 企业应用

【免费下载链接】papermark

Papermark is the open-source DocSend alternative and secure data rooms with built-in analytics and custom domains.

项目地址:https://gitcode.com/GitHub_Trending/pa/papermark
点击查看免费下载

Server Actions 是 Next.js App Router 中标记了"use server"的函数,它们在网络层被暴露为公开端点,与 API 路由具有同等的暴露面——任何人都可以直接调用,绕过页面渲染、布局守卫与中间件。本篇指南以 Papermark 仓库的工程实践为背景,系统讲解为什么必须在每个 Server Action 内部自行完成认证与授权,并给出正确的代码模式、输入校验顺序,以及 Papermark 在认证层面对应的统一实现(with-session-team.ts)作为参考。读完本文,你将掌握"防御式 Server Action"的完整写法,并理解 Papermark 中 RBAC 权限模型与数据房间级(dataroom)授权是如何落到每个 mutation 之上的。

为什么 Server Actions 必须被当作公开端点对待

在 App Router 中,任何导出并标记"use server"的异步函数,都会自动生成一个可供客户端调用的 HTTP 端点(POST到对应的 action 路径)。这意味着:

  • 它们不仅能被页面内的<form action={...}>或startTransition触发;
  • 攻击者可以直接构造请求调用该端点,完全跳过 UI、跳过中间件(middleware)、跳过布局或页面级别的守卫。

Next.js 官方文档对此有明确论断,这也是本规则(server-auth-actions.md)的核心依据:

"Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."

(将 Server Actions 与面向公网的 API 端点同等对待,并在执行变更前验证用户是否有权执行。)

因此,认证(authentication,你是谁)与授权(authorization,你能不能做)必须写在 Server Action 的函数体内部,绝不能依赖外层任何隐式保护。这也是本规则被标记为CRITICAL(严重)的原因:一旦缺失,等于把服务端的数据变更操作裸奔在公网之上,任何人都能触发删除、修改等破坏性 mutation,造成未授权访问。

反模式:缺少认证的 Server Action

下面的写法是典型的错误示范——函数内部完全没有认证检查:

'use server' export async function deleteUser(userId: string) { // Anyone can call this! No auth check await db.user.delete({ where: { id: userId } }) return { success: true } }

问题一目了然:deleteUser被公开暴露后,任何人都可以通过构造请求直接删除任意userId对应的用户。无论页面是否对当前用户隐藏了这个操作入口,攻击者都能绕过 UI 直接命中底层端点。这正是"中间件/页面守卫不足以保护 Server Action"的实证。

正确模式:在 action 内部完成认证与授权

安全的写法必须把校验逻辑放进 action 内部,按"先认证、再授权、最后变更"的顺序执行:

'use server' import { verifySession } from '@/lib/auth' import { unauthorized } from '@/lib/errors' export async function deleteUser(userId: string) { // Always check auth inside the action const session = await verifySession() if (!session) { throw unauthorized('Must be logged in') } // Check authorization too if (session.user.role !== 'admin' && session.user.id !== userId) { throw unauthorized('Cannot delete other users') } await db.user.delete({ where: { id: userId } }) return { success: true } }

这段代码包含两层防线:

  1. 认证层:verifySession()解析当前会话(在 Papermark 中对应getServerSession(authOptions)),拿不到有效会话立即抛出unauthorized,杜绝匿名调用。
  2. 授权层:即便已登录,也并非所有用户都有权执行同一 mutation——普通用户只能删除自己的账号(session.user.id !== userId即拒绝),只有管理员才能处理其他用户的删除请求。

值得注意的是,unauthorized抛出的应是 Next.js 识别的错误对象(对应 401/403 语义),而不是泛化的Error,这样上层错误处理可以准确映射到状态码,避免把认证失败错误地渲染成 500。

先校验输入,再认证,最后变更:完整的防御顺序

Server Action 的入参同样来自不可信的客户端,因此必须遵循"输入校验先行"的原则。推荐使用 Zod 等 schema 校验库对入参做白名单式校验,只有通过校验后才进入认证与授权流程:

'use server' import { verifySession } from '@/lib/auth' import { z } from 'zod' const updateProfileSchema = z.object({ userId: z.string().uuid(), name: z.string().min(1).max(100), email: z.string().email() }) export async function updateProfile(data: unknown) { // Validate input first const validated = updateProfileSchema.parse(data) // Then authenticate const session = await verifySession() if (!session) { throw new Error('Unauthorized') } // Then authorize if (session.user.id !== validated.userId) { throw new Error('Can only update own profile') } // Finally perform the mutation await db.user.update({ where: { id: validated.userId }, data: { name: validated.name, email: validated.email } }) return { success: true } }

这里展示了一条可复用的安全流水线,四步缺一不可:

步骤作用示例
1. 校验输入拒绝畸形、超长或类型错误的数据updateProfileSchema.parse(data),对userId校验uuid格式
2. 认证确认调用者身份verifySession()返回null即拒绝
3. 授权确认调用者有权操作目标资源仅允许session.user.id === validated.userId
4. 执行变更只有通过全部关卡才触达数据库db.user.update(...)

入参一律声明为unknown并先经 schema 解析,保证后续代码拿到的都是经过约束的强类型数据;而parse校验失败会直接抛出 ZodError,从源头拦截注入与数据污染。同时注意email使用z.string().email()、字符串长度使用min(1).max(100)的显式边界,这些约束应与数据库 schema 保持一致,避免"客户端能传、服务端存不下"的边界问题。

落地参考:Papermark 的"像 API 路由一样认证"实现

当前仓库的主体仍是 Pages Router + API 路由架构,而规则所倡导的"端点内自证身份"思想,在 Papermark 中已有非常成熟的对应实现——即统一会话认证包装器 with-session-team.ts。它把"先认证、再授权"的流程固化为一层可复用的高阶函数,可以作为你将 Server Actions 升级为同等安全等级时的设计蓝本。

认证层:基于 NextAuth 的会话解析

Papermark 的认证配置集中在 auth-options.ts,通过PrismaAdapter接入数据库,并注册了 Google、LinkedIn、邮件验证码(EmailProvider)、Passkey(Hanko)以及 SAML(BoxyHQ Jackson)等多类 provider。包装器在每次请求进入时调用:

const session = await getServerSession(authOptions); // App Router 分支 // 或 Pages Router 分支: const session = await getServerSession(req, res, authOptions);

这正是文档示例中verifySession()的真实形态——任何端点(未来包括 Server Action)都必须先取得合法会话,否则在resolveSessionTeam的第一步就被拦截:

if (!session || !session.user) { return { status: 401, message: "Unauthorized" }; }

授权层:RBAC 权限动词 + 数据房间级 entitlement

Papermark 的授权并非简单的"登录即可",而是两层模型:

  • 第一层:角色到权限动词的映射("能做什么"),定义在 permissions.ts。PermissionAction覆盖datarooms.read/write、documents.read/write、links.read/write、analytics.read/team、members.write、tokens.write、webhooks.write、domains.write、branding.read/write、sso.write等细粒度动词;getPermissionsByRole为ADMIN、MANAGER、MEMBER、DATAROOM_MEMBER四类角色返回各自的权限集合,其中未知名角色默认返回空集(默认拒绝)。
  • 第二层:数据房间级 entitlement("能访问哪些房间"),定义在 entitlements.ts。DATAROOM_MEMBER这类受限角色通过getAllowedDataroomIds从prisma.userDataroom读取被显式分配的数据房间列表,再经canAccessDataroom校验目标dataroomId是否在授权列表内。

两层模型与规则文档中的授权示例(session.user.role !== 'admin' && session.user.id !== userId)形成同构:先验证"你是谁",再验证"你能对哪个资源做什么"。withTeam包装器把所有门禁(会话、团队成员资格、requiredRoles、requiredPermissions、requiredPlan、房间 entitlement)集中到 resolveSessionTeam 一处执行,并对DATAROOM_MEMBER采取"默认拒绝"策略——被包装的路由若未显式声明所需权限,受限角色一律 403,从而把"防止越权"从依赖每个处理器自觉,变成了可审计的强制约束。

从包装器到 Server Action 的迁移要点

对照文档规则与上述实现,若在 Papermark 的 App Router 模块中引入 Server Actions,安全的形态应当是:把resolveSessionTeam中的同一套校验(getServerSession→ 团队成员查询 → 角色/权限/计划门禁 → 房间 entitlement)内联进每个"use server"函数,而不是只依赖withTeam包装 API 路由;因为 Server Action 没有请求上下文包装,必须自己在函数体内重建这一防线。换句话说:Papermark 现有的认证与授权逻辑可以直接复用,只是执行位置必须从"路由包装层"下沉到"action 函数内部"。

实践检查清单

在合并任何包含 Server Action 的代码前,逐项确认:

  • 每个"use server"函数体内都有明确的会话认证(如getServerSession/verifySession),拒绝匿名调用;
  • 认证之后还有资源级授权(如角色检查、资源归属校验、或 Papermark 的权限动词 + dataroom entitlement);
  • 入参先经 Zod schema 校验(parse而非safeParse后忽略),类型与长度约束对齐数据库 schema;
  • 变更类操作(删除、更新、转移)绝不依赖中间件、布局或页面守卫作为唯一防线;
  • 错误抛出具语义的 401/403 错误,而非笼统的 500;
  • 破坏性操作(删除、冻结等)在 Papermark 中额外用requiredRoles限制为 ADMIN/MANAGER,与withTeam的默认拒绝策略保持一致。

把 Server Actions 当作公开 API 端点来写——认证与授权永远放在函数内部,输入永远先校验,变更永远最后执行。这是 Next.js 官方安全建议,也是 server-auth-actions.md 中 impact 为CRITICAL的根本原因:防线若不在 action 内部,就等于没有防线。

  • 后端
  • 前端
  • 企业应用

【免费下载链接】papermark

Papermark is the open-source DocSend alternative and secure data rooms with built-in analytics and custom domains.

项目地址:https://gitcode.com/GitHub_Trending/pa/papermark
点击查看免费下载
上一篇:gh0stzk dotfiles中的Scratchpad功能:快速临时应用的实用工作流优化
下一篇:$name --

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询