Payload Auth 示例:在 Next.js 中构建完整前后端认证的实战指南
Payload 的 Auth 示例(examples/auth)演示了如何把 Payload 认证能力完整接入一个 Next.js 全栈应用:服务端通过 Local API(payload.auth)校验会话,前端通过 REST 或 GraphQL 双通道调用认证操作,配合角色化访问控制、CORS/CSRF 与 Cookie 安全配置,实现“管理员管后台、普通用户走前台”的典型权限模型。读完本文,你可以本地跑起这个示例,理解 users 集合的认证配置、字段钩子与访问函数的实现细节,并掌握生产环境构建部署的完整流程。
示例定位与代码地图
这个示例对应仓库中的 examples/auth/README.md,配套代码位于 examples/auth/src 目录。核心文件分布如下:
| 文件/目录 | 职责 |
|---|---|
| src/collections/Users.ts | 认证集合 users 的完整定义(auth 配置、access、字段、钩子) |
| src/collections/access/ | admins、adminsAndUser、anyone、checkRole 等访问控制函数 |
| src/collections/hooks/protectRoles.ts | 保护角色字段、保证每个用户至少有 user 角色的字段钩子 |
| src/collections/hooks/loginAfterCreate.ts | 注册后自动登录的 afterChange 钩子 |
| src/payload.config.ts | Payload 主配置(数据库、CORS/CSRF、secret 等) |
| src/migrations/seed.ts | 种子迁移,创建演示管理员账号 |
| src/app/(app)/_providers/Auth//_providers/Auth) | 前端认证上下文,封装 REST 与 GraphQL 两套调用通道 |
| src/app/(app)/login/LoginForm/index.tsx/login/LoginForm/index.tsx) | 登录表单,演示重定向与错误处理 |
示例包含 /login、/logout、/create-account、/recover-password、/reset-password、/account 等前台页面,以及 http://localhost:3000/admin 的后台面板,覆盖认证的全生命周期。
快速启动
第一步:从示例创建项目
npx create-payload-app --example auth
第二步:配置数据库
该示例使用 MongoDB 适配器(见 payload.config.ts 中的 mongooseAdapter),需要一个可访问的 MongoDB 实例。从 .env.example 复制配置,仓库中 examples/auth/.env.example 定义了 4 个环境变量:
# 数据库连接串
DATABASE_URL=mongodb://127.0.0.1/payload-example-auth
# 用于加密 JWT Token
PAYLOAD_SECRET=YOUR_SECRET_HERE
# 用于配置 CORS、格式化链接等(末尾不要加斜杠)
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# 用于跨子域共享 Cookie
COOKIE_DOMAIN=localhost
若 MongoDB 实例需要鉴权,直接把凭证拼进 DATABASE_URL。
第三步:启动服务并注入种子数据
pnpm dev
结合 package.json 可以看到,dev 脚本实际上是 pnpm seed && next dev,其中 seed 定义为 payload migrate:fresh——即启动前先执行一次全量迁移并重建数据库。当启动过程提示是否注入种子数据时,输入 y 并回车即可。种子迁移(seed.ts)会创建一个管理员账号:
await payload.create({
collection: 'users',
data: {
email: 'demo@payloadcms.com',
password: 'demo',
roles: ['admin'],
},
})
注意:seed 是破坏性操作,它会 drop 当前数据库并用种子模板重建。只适合新项目或可以接受数据丢失的场景。
第四步:访问应用
- 打开
http://localhost:3000访问前台首页; - 打开
http://localhost:3000/admin访问后台面板; - 使用演示凭证登录后台:
Email: demo@payloadcms.com、Password: demo。
Users 集合:认证核心
整个示例只有一个集合 users,但它是认证体系的全部核心。Users.ts 的关键配置如下:
export const Users: CollectionConfig = {
slug: 'users',
auth: {
tokenExpiration: 28800, // 8 小时
cookies: {
sameSite: 'none',
secure: true,
domain: process.env.COOKIE_DOMAIN,
},
},
access: {
read: adminsAndUser,
create: anyone,
update: adminsAndUser,
delete: admins,
unlock: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {
afterChange: [loginAfterCreate],
},
// fields 省略,下文展开
}
几个值得注意的设计点:
tokenExpiration: 28800:JWT 有效期 8 小时,过期后客户端需要通过Refresh Token操作续期;- Cookie 三件套:
sameSite: 'none'+secure: true+ 可配置的domain,这是为了支持前台站点与后台 API 分离部署(跨子域)时仍能安全携带认证 Cookie。COOKIE_DOMAIN环境变量在 payload.config.ts 之外的集合层被读取; saveToJWT:roles字段设置了该属性(见下文字段部分),角色会直接写入 JWT,服务端判断权限时无需查库;loginAfterCreate:注册后自动登录(见后文钩子部分)。
字段设计
集合字段体现了“认证所需字段全部显式声明”的思路:
email:email类型,required+unique,且字段级 access 限制为adminsAndUser(自己或管理员可读可改);password:password类型,后台编辑时提示“留空则保持当前密码”;resetPasswordToken/resetPasswordExpiration:隐藏字段,支撑忘记密码流程;firstName/lastName:基础资料;roles:select类型 +hasMany,取值admin/user,saveToJWT: true,字段级 access 仅管理员可读写,并挂上protectRoles钩子。
基于角色的访问控制
README 定义了两种角色:
admin:可访问 Payload 后台面板,可看到全部数据、执行全部操作;user:不能访问后台面板,只能基于自身进行受限操作。
具体实现分布在 src/collections/access/ 下的四个文件:
// checkRole.ts:判断用户是否拥有目标角色中的任意一个
export const checkRole = (allRoles: User['roles'] = [], user: User | null = null): boolean => {
if (user) {
if (allRoles.some((role) => user?.roles?.some((individualRole) => individualRole === role))) {
return true
}
}
return false
}
// admins.ts:仅管理员
export const admins: Access = ({ req: { user } }) => checkRole(['admin'], user)
// anyone.ts:任何人(用于注册)
export const anyone: Access = () => true
adminsAndUser 最有意思——它不只是布尔判断,而是返回查询约束,实现“管理员看全部、普通用户只看自己”:
export const adminsAndUser: Access = ({ req: { user } }) => {
if (user) {
if (checkRole(['admin'], user)) {
return true
}
return {
id: { equals: user.id }, // 普通用户只能读写自己的文档
}
}
return false
}
这套组合落到 users 集合上就是:任何人可注册(create: anyone)、登录后可读/改自己的账户(read/update: adminsAndUser)、只有管理员能删除用户或解锁账号(delete/unlock: admins)、只有管理员能进入后台管理用户(admin)。
protectRoles 钩子:角色防篡改
protectRoles.ts 挂在 roles 字段的 beforeChange 上,解决两个问题:新用户创建时自动带上 user 角色;非管理员永远改不了角色。
export const protectRoles: FieldHook<{ id: string } & User> = ({ data, req }) => {
const isAdmin =
req.user?.roles.includes('admin') || data.email === 'demo@payloadcms.com' // for the seed script
if (!isAdmin) {
return ['user']
}
const userRoles = new Set(data?.roles || [])
userRoles.add('user')
return [...userRoles]
}
从源码结构看,data.email === 'demo@payloadcms.com' 这一行是专门为 seed 脚本放行的:种子迁移在进程内创建管理员时,请求上下文里还没有登录用户,因此用邮箱白名单保证 admin 角色不被钩子覆盖。
loginAfterCreate 钩子:注册即登录
loginAfterCreate.ts 挂在集合的 afterChange 上,当 operation === 'create' 且请求体里同时带 email 和 password 时,自动调用 Local API 的 payload.login 完成登录,并把 token 和 user 附加到响应文档中:
if (operation === 'create') {
const { email, password } = body
if (email && password) {
const { token, user } = await payload.login({
collection: 'users',
data: { email, password },
req,
res,
})
return { ...doc, token, user }
}
}
return doc
这让前端注册页无需二次调用登录接口,一次请求即可完成“建账号 + 建会话”。
双 API 认证:Local API 与 HTTP
Payload 的认证能力同时暴露在服务端 Local API 和 HTTP(REST/GraphQL)两层,示例中两者都有真实落地。
Local API:服务端校验会话
在 Next.js 服务端组件中,通过 getPayload 拿到实例后用 payload.auth({ headers }) 从请求头解析当前用户。account/page.tsx/account/page.tsx) 的真实实现与 README 给出的片段一致:
import { headers as getHeaders } from 'next/headers.js'
import { getPayload } from 'payload'
import config from '../../../payload.config'
export default async function Account() {
const headers = await getHeaders()
const payload = await getPayload({ config })
const { permissions, user } = await payload.auth({ headers })
if (!user) {
redirect(
`/login?error=${encodeURIComponent('You must be logged in to access your account.')}&redirect=/account`,
)
}
return ...
}
未登录时重定向到 /login,并通过 error 与 redirect 查询参数传递提示信息;登录页会用 getSafeRedirect(payload/shared)处理回跳目标,见 LoginForm/index.tsx/login/LoginForm/index.tsx)。
HTTP:REST 与 GraphQL 操作端点
开启 auth 后,users 集合自动暴露一组认证操作端点:Me、Login、Logout、Refresh Token、Verify Email、Unlock、Forgot Password、Reset Password。最典型的调用是获取当前用户:
await fetch('/api/users/me', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
服务端没有 Local API 可用时(例如独立前端、边缘环境),这些 HTTP API 依然可用。
示例更进一步,在前端封装了双通道认证 Provider(_providers/Auth/index.tsx/_providers/Auth/index.tsx)):AuthProvider 接受 api: 'rest' | 'gql' 属性,create、login、logout、forgotPassword、resetPassword 每个操作都同时实现了 REST 与 GraphQL 两种调用方式,页面只需 useAuth() 取用,切换底层协议零成本。配套的两个底层客户端:
- rest.ts/_providers/Auth/rest.ts):统一
fetch封装,credentials: 'include'携带 Cookie,从响应体取user,遇到errors抛错; - gql.ts/_providers/Auth/gql.ts):向
/api/graphql发POST,同样的 Cookie 携带策略。
Provider 挂载时(useEffect)会立即调用 /api/users/me(或 GraphQL 的 meUser 查询)拉取当前用户写入 React Context,实现刷新页面后登录态自动恢复。
安全配置:CORS、CSRF 与 Cookies
README 强调该示例已配置 cors、csrf 与 cookies,确保后台与前台安全通信。对照源码,payload.config.ts 中有两处显式配置:
export default buildConfig({
admin: {
components: {
beforeLogin: ['@/components/BeforeLogin#BeforeLogin'], // 登录页自定义组件
},
},
collections: [Users],
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
db: mongooseAdapter({ url: process.env.DATABASE_URL || '' }),
secret: process.env.PAYLOAD_SECRET || '',
// ...
})
三者的分工:
cors:只允许NEXT_PUBLIC_SERVER_URL声明的前端源跨域访问 API,杜绝任意站点发起请求;csrf:同源/同域表单请求需要携带 CSRF Token,防止跨站请求伪造;cookies(集合层auth.cookies):sameSite: 'none'允许跨站携带、secure: true强制 HTTPS、domain: process.env.COOKIE_DOMAIN支持跨子域共享会话——这套组合正是为“前台站点域名与 API 域名分离”的部署形态设计的。
另外,PAYLOAD_SECRET 负责 JWT 的签名与加密,.env.example 中明确要求替换占位值,生产环境务必使用强随机密钥。
生产构建与部署
Payload 在生产环境需要构建并独立托管 Admin 面板,示例给出两步:
- 在项目根目录执行
pnpm build(等价yarn build/npm run build)触发payload build,生成./build目录下的生产级 admin bundle。对照 package.json,脚本为cross-env NODE_OPTIONS=--no-deprecation payload build; - 启动 Node.js 服务从
./build目录托管 Payload。仓库脚本表中该入口为"start": "next start",即pnpm start(README 中写作pnpm serve,以实际package.json脚本名为准)。
部署方面 README 给出了两条路径:集成 Next.js 的应用可直接部署到 Vercel;也可使用 Payload Cloud 从 GitHub 仓库一键部署生产实例;手动部署则参考 Payload 官方文档的部署章节。
小结
examples/auth 的价值在于它用最小集合展示了 Payload 认证的完整链路:配置层(auth.tokenExpiration、Cookie 策略、saveToJWT)决定会话与凭证形态;访问层(admins / adminsAndUser / checkRole)用“布尔 + 查询约束”两种返回值实现角色化权限;钩子层(protectRoles、loginAfterCreate)在数据流上守住角色完整性与注册即登录体验;API 层则让同一套认证能力在 Next.js 服务端(Local API)与浏览器(REST/GraphQL)两侧以不同姿势复用。理解这个示例后,你可以按同样骨架扩展自己的应用:替换字段、增加角色、按需收紧或放宽 access 函数,即可得到一套类型安全、前后端一致的认证系统。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00