Keystone 6 密码认证完整指南:使用 createAuth() 构建登录体系
2026/9/24 16:47:07 网站建设 项目流程
  • 后端

【免费下载链接】keystone

The superpowered headless CMS for Node.js — built with GraphQL and React

项目地址:https://gitcode.com/gh_mirrors/key/keystone
点击查看免费下载

导读

本文围绕@keystone-6/auth包中的createAuth()函数,系统讲解如何在 Keystone 6 中为应用接入基于password字段的认证能力:从withAuth包装config()的接入方式,到必选/可选配置项、自动注入的 GraphQL API、Admin UI 登录页,再到sessionData的会话数据注入原理。读完本文,你将能够独立为一个 Keystone 系统配置「账号密码 + 会话 + 访问控制」的完整认证链路。

认证机制总览:createAuth 与 withAuth

Keystone 允许通过@keystone-6/auth包中的createAuth()函数,将系统扩展为支持针对某个列表(List)上的password字段进行认证。其核心用法如下:

import { config, list } from '@keystone-6/core' import { text, password, checkbox } from '@keystone-6/core/fields' import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ // Required options listKey: 'User', identityField: 'email', secretField: 'password', // Additional options sessionData: 'id email', }) export default withAuth( config({ lists: { User: list({ fields: { email: text({ isIndexed: 'unique' }), password: password(), isAdmin: checkbox(), }, }), }, session: { /* ... */ }, }) )

createAuth返回一个名为withAuth的函数,用它包裹你的config()即可。这个包装函数会修改配置对象,向系统中注入额外字段、额外 GraphQL query/mutation 以及自定义 Admin UI 功能。并且createAuth必须与 session 配置 配合使用。

从源码看,withAuth的注入动作在 packages/auth/src/index.ts 中完成,具体包括:

  • 校验配置合法性(throwIfInvalidConfig);
  • 通过getAdditionalFiles向 Admin UI 构建产物写入 signin 页面文件;
  • 通过extendGraphqlSchema组合注入认证相关的 GraphQL schema(schema.ts);
  • authSessionStrategy包装你传入的 session strategy,实现session.data的自动填充。

需要说明的是:@keystone-6/auth只是基于 Keystone 底层 API 的一种「有主见」的默认实现。如果你想自定义认证流程(例如集成 OAuth),完全可以参照它 fork 一份自己的实现,会话管理同样如此。

必选配置项

认证系统的核心能力是:提供一个 GraphQL mutation 用于认证用户并开启会话,以及在 Admin UI 中提供登录页面。三个必选配置项如下:

配置项说明约束
listKey用于认证的列表名称列表必须真实存在,否则withAuth会抛出withAuth cannot find the list "..."错误
identityField作为身份标识的字段名该字段必须设置{ isIndexed: 'unique' }
secretField作为秘密(口令)的字段名必须是password()字段类型

最小配置示例:

import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', secretField: 'password', })

字段约束的源码依据:在 packages/auth/src/schema.ts 中,getSchemaExtension会检查identityField在列表的WhereUniqueInput中是否可被 String/ID 唯一检索,若否则抛出错误,提示你应该给该字段添加isIndexed: 'unique';同时 getBaseAuthSchema.ts 通过getPasswordFieldKDF校验secretField必须是合法的 password 字段,否则抛出${listKey}.${secretField} is not a valid password field.

GraphQL API

开启认证后,以下元素会被加入 GraphQL API:

type Mutation { authenticateUserWithPassword( email: String! password: String! ): UserAuthenticationWithPasswordResult! } type Query { authenticatedItem: AuthenticatedItem } union AuthenticatedItem = User union UserAuthenticationWithPasswordResult = | UserAuthenticationWithPasswordSuccess | UserAuthenticationWithPasswordFailure type UserAuthenticationWithPasswordSuccess { sessionToken: String! item: User! } type UserAuthenticationWithPasswordFailure { message: String! }

命名规则:上述 GraphQL 名称并非硬编码。从 index.ts 中的 getAuthGqlNames 可以看出,mutation 名由authenticate${GraphQL单数名}WithPassword拼接而来(对应listKey: 'User'时即authenticateUserWithPassword),union 及各类型同理。

authenticateUserWithPassword

该 mutation 会校验提交的凭据,若合法则开启一个新的 session。它的参数名正是identityFieldsecretField的值。

mutation { authenticateUserWithPassword(email: "username@example.com", password: "password") { ... on UserAuthenticationWithPasswordSuccess { item { id email } } ... on UserAuthenticationWithPasswordFailure { message } } }

成功时:会话处理器会开启新会话,并把编码后的会话 cookie 数据作为sessionToken返回,已认证的用户对象作为item返回。

失败时:返回{ code: FAILURE, message: "Authentication failed." }。这个常量定义在 getBaseAuthSchema.ts,resolve实现中所有失败路径都返回它,调用方只能通过message感知失败,而无法区分「用户不存在」与「密码错误」。

值得关注的实现细节(见 getBaseAuthSchema.ts):

  • 认证查询通过context.sudo().db[listKey].findOne(...)执行,即绕过访问控制直接按identityField查找用户;
  • 当用户不存在或 secret 字段不是字符串时,会执行一次kdf.hash('simulated-password-to-counter-timing-attack'),用模拟哈希来抵消基于响应时间的用户枚举攻击
  • 密码校验使用 password 字段类型底层的 KDFcompare,确保存储的是哈希而非明文;
  • 校验通过后调用context.sessionStrategy.start({ data: { itemId: item.id }, context })启动会话。
authenticatedItem

该 query 基于session数据返回当前登录用户;没有会话时返回null(getBaseAuthSchema.ts)。此外,源码中还额外注入了endSessionmutation,用于结束当前会话(内部调用context.sessionStrategy.end({ context })),这也是从 GraphQL 层主动登出的标准途径。

Admin UI

启用认证后,Admin UI 会新增/signin登录页。未登录用户访问 Admin UI 会被重定向回/signin;该页面内部正是调用authenticateUserWithPasswordmutation 完成登录的。

实现细节:在 index.ts 中,authGetAdditionalFiles会把渲染好的pages/signin.jsconfig.ts写入 Admin UI 构建产物,signin 页面模板见 templates/signin.ts,页面组件实现位于 pages/SigninPage.tsx。同时(index.ts):

  • authPublicPages会把${basePath}/signin追加进ui.publicPages,使其成为公开页面;
  • 通过pageMiddleware包装:authMiddlewarewasAccessAllowed为假时返回{ kind: 'redirect', to: basePath + '/signin' },其余情况再交给用户自定义的pageMiddleware
  • 默认的isAccessAllowed逻辑是session !== undefined(index.ts),你可以像 examples/auth/keystone.ts 那样覆盖它,例如只允许管理员进入 Admin UI。

可选配置项

以下选项为认证系统添加额外功能,默认处于禁用/默认状态。

sessionData

sessionData用于在认证时设置自定义的session.data值。

认证 mutation 会在context.session对象上设置{ listKey, itemId }。但在做访问控制或使用 hooks 时,你往往需要比itemId更多的用户信息。配置sessionData后,系统会根据itemId查询对应字段并填充到session.data

其值是一段 GraphQL 查询字符串,指明要把哪些字段填充到session.data上:

import { createAuth } from '@keystone-6/auth' const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', secretField: 'password', sessionData: 'id name isAdmin', })

源码行为sessionData的默认值是'id'(见 index.ts 的参数默认值)。填充动作发生在authSessionStrategy.get中(index.ts):每次请求时先取底层 session,再用sudo context执行query[listKey].findOne({ where: { id: session.itemId }, query: sessionData })拉取数据——注意 types.ts 中明确标注了「WARNING: uses sudo to retrieve this data」,即该查询会绕过访问控制,因此不要往sessionData里塞你不想让用户看到的字段。查询失败或数据不存在时session会被置为undefined(等价于未登录)。

同时,schema.ts 会在启动阶段把sessionData拼成query($id: ID!) { item(where: { id: $id }) { ${sessionData} } }交给 GraphQL 解析与校验:语法错误会提示「the sessionData option in your createAuth usage is likely incorrect」,校验错误则会直接列出具体报错,帮助你在开发期尽早发现问题。

典型用法:把sessionData配成'isAdmin'后,就可以在列表的access中这样判断:

const isAdmin = ({ session }: { session?: Session }) => Boolean(session?.data.isAdmin)

关于sessionData与 operation/filter/item 三级访问控制如何组合使用,参见 访问控制指南。

与 Session 配置协同:完整可运行示例

createAuth必须与config.session一起使用,否则withAuth会抛出TypeError: Missing .session configuration(index.ts)。官方示例项目 examples/auth 给出了开箱即用的完整配置:

import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { config } from '@keystone-6/core' import { statelessSessions } from '@keystone-6/core/session' import { createAuth } from '@keystone-6/auth' import { type Session, lists } from './schema' import type { TypeInfo } from './generated/keystone/types' // WARNING: 生产环境必须更换此密钥 const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --' // 会话 cookie 有效期,单位为秒;示例中设为 1 小时 const sessionMaxAge = 60 * 60 const { withAuth } = createAuth({ listKey: 'User', // 存放用户的列表 identityField: 'name', // 身份字段,通常为用户名或邮箱 secretField: 'password', // 秘密字段,必须是 password 字段类型 sessionData: 'isAdmin', // 把 isAdmin 注入会话数据 }) export default withAuth<TypeInfo<Session>>( config<TypeInfo>({ db: { provider: 'sqlite', prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: process.env.DATABASE_URL ?? 'file:./keystone-example.db', }), }), async onConnect(context) { // 开发辅助:首次启动自动创建初始管理员账号(生产环境请勿使用) ;(async () => { const sudoContext = context.sudo() if ((await sudoContext.db.User.count()) !== 0) return const password = crypto.getRandomValues(new Uint8Array(16)).toHex() await sudoContext.db.User.createOne({ data: { name: 'admin', password, isAdmin: true } }) console.log(`Created initial user: admin / ${password}`) })().catch(error => console.error('Failed to create initial user:', error)) }, }, lists, ui: { // 仅管理员可进入 Admin UI isAccessAllowed: context => { return context.session?.data?.isAdmin ?? false }, }, session: statelessSessions({ maxAge: sessionMaxAge, secret: sessionSecret, }), }) )

这里用到了statelessSessions,它属于 Session 配置文档 中的两种会话策略之一:

  • 无状态会话(statelessSessions):所有会话数据都存放在加密 cookie 中,无需服务端存储;
  • 有状态会话(storedSessions):cookie 只存放 session ID,通过store参数提供的set/get/delete接口在服务端存取数据。

两种策略的 cookie 均使用@hapi/iron加密,常用参数包括:secret(必填,至少 32 个字符)、maxAge(默认 8 小时)、secure(默认NODE_ENV === 'production')、pathdomainsameSite(默认'lax')等。认证 mutation 的start/end正是通过这些 session strategy 完成会话的开启与结束。

测试验证

仓库为认证功能提供了自动化测试覆盖,可作为行为验证与参考:

  • tests/api-tests/auth.test.ts 与 tests/api-tests/auth-header.test.ts:覆盖认证 mutation 的完整流程;
  • tests/examples-smoke-tests/auth.test.ts:对 examples/auth 示例做冒烟测试;
  • tests2/access.*.test.ts 系列测试则覆盖了会话与访问控制的组合场景。

相关资源

  • Authentication and Access Control 指南:认证 + 会话 + 访问控制的组合使用教程;
  • Session 配置 API:statelessSessions/storedSessions完整参数说明;
  • examples/auth:为任务管理启动项目添加密码认证的官方示例;
  • @keystone-6/auth 源码:createAuth/withAuth的完整实现,可作为自定义认证方案的起点。
  • 后端

【免费下载链接】keystone

The superpowered headless CMS for Node.js — built with GraphQL and React

项目地址:https://gitcode.com/gh_mirrors/key/keystone
点击查看免费下载
上一篇:CSDN博客下载器:终极指南教你如何免费快速保存技术文章
下一篇:如何快速安装网盘直链下载助手:新手完整指南

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

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

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

立即咨询