
NocoBase 认证体系解析BaseAuth 基类源码实现与自定义认证类型开发指南【免费下载链接】nocobaseNocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase导读BaseAuth是 NocoBase 认证体系中最核心的基类实现它以 JWT 为鉴权载体为用户认证类型的扩展提供了完整的默认能力登录、注册、注销、token 校验与续期、Cookie 会话管理。本文以 BaseAuth API 文档 为主体结合nocobase/auth包的源码实现系统讲解BaseAuth的类方法、JWT 底层机制与用户数据模型的关联方式并通过plugin-auth插件中BasicAuth的真实实现演示如何基于它开发自定义认证类型。读完本文你将掌握 NocoBase 认证插件的扩展套路并能独立实现用户名密码 自定义认证的完整认证流程。一、认证体系概览Auth 抽象类与 BaseAuth 的关系在 NocoBase 的认证架构中存在两层抽象Auth抽象类定义了完成用户认证所需的全部接口契约包括user、check()、signIn()、signUp()、signOut()、syncCookies()等源码位于 packages/core/auth/src/auth.ts。BaseAuth基类继承自Auth补全了基于 JWT 的具体实现是大多数自定义认证类型的推荐起点。// 来自 packages/core/auth/src/auth.ts export abstract class Auth implements IAuth { abstract user: Model; abstract check(): PromiseModel; // 抽象方法必须由子类实现 async signIn(): Promiseany {} async signUp(): Promiseany {} async signOut(): Promiseany {} }从代码可以看出Auth只声明了接口骨架signIn等方法默认是空实现。而BaseAuth将这些方法全部落地为可用的 JWT 鉴权逻辑。因此文档中的结论非常明确大多数情况下扩展用户认证类型应当继承BaseAuth没有必要直接继承Auth抽象类后者仅在需要完全自建鉴权协议时才值得考虑。AuthConfig 配置对象BaseAuth的构造函数接收的配置类型是AuthConfig与userCollection的交叉类型。其中AuthConfig在 auth.ts 中定义export type AuthConfig { authenticator: Authenticator; // 认证器数据模型应用内实际类型是 AuthModel options: { [key: string]: any; // 认证器相关配置如是否允许注册、注册表单字段等 }; ctx: Context; // 请求上下文 };三者各有分工属性类型说明authenticatorAuthenticator认证器数据模型在 auth-manager.ts 中定义为包含authType、options等字段的结构应用中的实际类型是AuthModeloptionsRecordstring, any认证器级配置BasicAuth正是从这里读取allowSignUp、signupForm等注册相关配置ctxContextKoa 请求上下文承载请求参数、Cookie、请求头、数据库访问入口等Auth的构造函数将这三项分别挂载到受保护属性上auth.ts子类在后续方法中可随时通过this.ctx、this.authenticator、this.options访问。二、构造函数绑定用户数据表BaseAuth的构造函数签名源码见 base/auth.tsconstructor( config: AuthConfig { userCollection: Collection; }, )与Auth不同BaseAuth额外要求传入userCollection——即用户数据表例如db.getCollection(users)。构造函数将其保存后通过get userRepository()访问器暴露对应 Repositoryget userRepository() { return this.userCollection.repository; }这一设计的意义在于所有基于BaseAuth的认证类型都天然与用户表绑定后续的validate()查找用户、checkToken()按 userId 查用户、signOut()清除用户缓存都复用同一个数据源。开发者扩展新认证类型时只需在构造函数中把用户表装配进来例如文档中的BasicAuth示例class BasicAuth extends BaseAuth { constructor(config: AuthConfig) { // 设置用户数据表 const userCollection config.ctx.db.getCollection(users); super({ ...config, userCollection }); } }这与仓库中plugin-auth插件 server/basic-auth.ts 的真实实现完全一致。三、user 访问器用户信息的存取约定BaseAuth的user访问器默认使用ctx.state.currentUser对象存取用户信息set user(user: Model) { this.ctx.state.currentUser user; } get user() { return this.ctx.state.currentUser; }ctx.state是 Koa 约定俗成的请求级状态容器。将当前用户放在这里意味着同一请求生命周期内的后续中间件、资源处理器都可以通过ctx.state.currentUser共享登录用户。鉴权中间件在check()通过后会执行ctx.auth.user user见 auth-manager.ts正是通过这个 setter 把用户注入状态。四、check()基于 token 的鉴权与续期4.1 checkToken()token 校验主流程check()是鉴权入口其核心逻辑在checkToken()方法中完成base/auth.ts流程如下提取 token通过ctx.getBearerToken()从Authorization请求头取 token若缺失抛 401 错误错误码EMPTY_TOKEN解码校验调用this.jwt.decode(token)验证签名与有效期。若 token 过期记录tokenStatus expired并尝试无签名验证提取 payload若签名非法抛 401错误码INVALID_TOKEN查询用户从 payload 中取userId通过缓存包装cache.wrap(this.getCacheKey(userId), ...)查询用户表缓存 key 格式为auth:${userId}。用户不存在则抛 401NOT_EXIST_USER角色注入若 payload 携带roleName写入ctx.headers[x-role]供后续 ACL 权限判断使用黑名单检查调用this.jwt.blacklist.has(jti ?? token)若 token 已被拉黑如注销后抛 401BLOCKED_TOKEN会话过期检查针对临时 tokentemp结合tokenController.getConfig()的sessionExpirationTime判断会话是否超时密码变更失效若用户表存在passwordChangeTz密码修改时间戳且早于 token 签发时间iat说明 token 签发在改密之前强制重新登录续期判定token 过期后若在expiredTokenRenewLimit宽限期内允许续期但text/event-stream流式请求不允许续期返回SKIP_TOKEN_RENEW。该流程涉及的 JWT 配置项来自tokenController的配置tokenExpirationTime、sessionExpirationTime、expiredTokenRenewLimit在测试用例 base-auth.test.ts 中可以看到典型取值token 有效期 30 分钟、会话有效期 1 天、过期宽限 15 分钟。4.2 check()续期与换发新 tokencheck()base/auth.ts在checkToken()判定 token 已过期且仍处于宽限期时执行真正的续期动作const renewedResult await this.tokenController.renew(jti); const expiresIn Math.floor(tokenPolicy.tokenExpirationTime / 1000); const newToken this.jwt.sign( { userId: user.id, roleName, temp, signInTime, iat: Math.floor(renewedResult.issuedTime / 1000) }, { jwtid: renewedResult.jti, expiresIn }, ); this.ctx.res.setHeader(x-new-token, newToken); this.setAuthCookies(newToken, tokenPolicy.sessionExpirationTime);续期成功后新 token 通过响应头x-new-token与认证 Cookie 双通道下发前端可在后续请求中无缝换用新 token实现无感续期。syncCookies()方法base/auth.ts则负责在拿到x-new-token后同步刷新浏览器 Cookie。五、signIn() / signUp() / signOut()完整的会话生命周期5.1 signIn()登录并签发 tokensignIn()base/auth.ts的调用链是signIn→validate()子类实现的具体校验→signNewToken()→setAuthCookies()setSessionCookies()async signIn() { let user: Model; try { user await this.validate(); // 由子类实现判断账号密码等凭据 } catch (err) { this.ctx.throw(err.status || 401, err.message, { ...err }); } if (!user) { this.ctx.throw(401, { message: ..., code: AuthErrorCode.NOT_EXIST_USER }); } const token await this.signNewToken(user.id); const maxAge await this.getSessionCookieMaxAge(); this.setAuthCookies(token, maxAge); await this.setSessionCookies(user, maxAge); return { user, token }; }signNewToken()先通过tokenController.add({ userId, authenticator })登记 token 会话获得jti、signInTime再用 JWT 签名payload 中包含userId、temp: true、iat、signInTime等字段base/auth.ts。5.2 登录后的 Cookie 体系signIn()成功后写入三类 Cookiebase/auth.tsauthTokenJWT 本体用于后续请求鉴权authenticator记录当前使用的认证器名称供下次请求选择认证器csrfToken随机 24 字节的 base64url 随机串配合非安全方法POST/PUT/DELETE 等的X-CSRF-Token请求头校验防止 CSRF 攻击中间件实现见 auth-manager.tsrole自动解析用户的默认角色并写入 Cookie。角色解析优先级为ctx.state.currentRole→X-Role请求头 →rolesUsers表中default: true的记录 → 用户关联角色的首个。5.3 signOut()注销并拉黑 tokenasync signOut(): Promiseany { this.clearAuthCookies(); // 清除全部认证 Cookie const token this.ctx.getBearerToken(); if (!token) return; const { userId } await this.jwt.decode(token); await this.ctx.app.emitAsync(cache:del:roles, { userId }); await this.ctx.cache.del(this.getCacheKey(userId)); return await this.jwt.block(token); // 将 token 加入黑名单 }注销不仅清除 Cookie还会通过事件cache:del:roles使角色缓存失效、删除用户缓存并调用jwt.block(token)将 token 加入黑名单到期自动清理从源头阻断已注销 token 的复用。signOut接口动作还会额外广播auth:signOut事件见 packages/core/auth/src/actions.ts供其他插件监听。5.4 signUp()注册接口约定Auth中signUp()默认为空实现BaseAuth也未提供默认注册逻辑而是将其作为可覆写的方法留给子类。BasicAuth即实现了完整的注册流程详见下文并受认证器配置allowSignUp开关控制。5.5 validate()子类必须实现的鉴权核心validate()是BaseAuth中唯一需要子类覆写才能工作的关键方法基类默认实现直接返回nullbase/auth.ts此时signIn()会因!user抛出NOT_EXIST_USER错误——这正是 base-auth.test.ts 中测试用例所验证的行为。它的职责是根据请求参数校验用户凭据校验通过返回用户 Model失败则抛 401 异常。六、JWT 底层实现JwtService 与密钥管理BaseAuth的所有 token 操作都委托给this.jwtJwtService它由AuthManager在构造时创建auth-manager.ts。核心行为见 base/jwt-service.tsexport class JwtService { constructor(protected options: JwtOptions) { const { secret, expiresIn } options; this.options { secret, expiresIn: expiresIn || process.env.JWT_EXPIRES_IN || 7d, }; } sign(payload, options?) { ... } // 签发expiresIn 为 never 时按 1000 年处理 decode(token): PromiseJwtPayload { ... } // 用 secret 校验并解码 async block(token) { ... } // 解析 exp 与 jti写入黑名单 }几个关键实现事实默认过期时间未显式配置时取环境变量JWT_EXPIRES_IN再缺省为7dexpiresIn: never语义被转换为1000y用于永不过期的长期 token 场景密钥来源优先级auth-manager.ts显式传入的jwt.secret→ 已存在的storage/apps/main/jwt_secret.dat文件32 字节权限 0600→ 环境变量APP_KEY排除your-secret-key、test-key等占位值→ 随机生成 32 字节并持久化到上述文件。仅当显式设置UNSAFE_USE_DEFAULT_JWT_SECRETtrue时才允许直接使用APP_KEY作为密钥。七、实战BasicAuth —— 基于 BaseAuth 的官方实现plugin-auth插件中的 BasicAuth 是BaseAuth最典型的子类实践也是文档示例的完整版本建议作为自定义认证开发的参考模板。7.1 覆写 validate()用户名/邮箱 密码校验async validate() { const ctx this.ctx; const { account, // Username or email email, // Old parameter, compatible with old api password, } ctx.action.params.values || {}; if (!account !email) { ctx.throw(400, ctx.t(Please enter your username or email, { ns: namespace })); } const filter email ? { email } : { $or: [{ username: account }, { email: account }] }; const user await this.userRepository.findOne({ filter }); if (!user) { ctx.throw(401, ctx.t(The username/email or password is incorrect, please re-enter, { ns: namespace })); } const field this.userCollection.getFieldPasswordField(password); const valid await field.verify(password, user.password); if (!valid) { ctx.throw(401, ctx.t(The username/email or password is incorrect, please re-enter, { ns: namespace }), { internalCode: INCORRECT_PASSWORD, user, }); } return user; }这段实现演示了BaseAuth子类的典型写法从ctx.action.params.values读取表单参数 → 通过this.userRepository查询用户 → 用用户表的PasswordField字段校验密码 → 返回用户。注意密码校验走的是数据库字段的field.verify()而非在认证层明文比对。7.2 覆写 signUp()注册流程BasicAuth.signUp()的逻辑包括读取认证器配置options.public.allowSignUp决定是否允许注册不允许时抛 403按signupForm配置校验注册字段至少强制 username 或 email 其一校验用户名格式复用BaseAuth.validateUsername()规则为 1~50 位、不含 /字符测试见 base-auth.test.ts校验密码与确认密码一致最后只将表单配置的字段与密码写入users表。7.3 扩展能力找回密码与重置密码BasicAuth还在BaseAuth基础上额外实现了lostPassword()与resetPassword()前者通过通知管理插件notification-manager发送含重置链接的邮件链接中携带一次性resetToken由authManager.jwt.sign签发过期时间取resetTokenExpiresIn分钟后者校验 token 未被拉黑且有效后重设密码并将使用过的 token 加入黑名单防止重放。这展示了BaseAuth派生类扩展业务方法的空间——基类只约束认证协议具体业务能力完全由子类自由发挥。八、认证类型注册与鉴权中间件完成自定义认证类后还需通过AuthManager.registerTypes()注册到认证类型注册表auth-manager.ts并提供title类型显示名等配置。请求进入时AuthManager.middleware()auth-manager.ts按以下顺序定位认证器并执行鉴权优先读取authKey请求头默认X-Authenticator指定的认证器其次读取authenticatorCookie最后回退到AuthManagerOptions.default指定的默认认证器通过skipCheck()判断公开资源、关闭 ACL 等场景是否跳过鉴权需要鉴权时调用ctx.auth.check()成功后把用户注入ctx.auth.user。认证相关动作signIn/signUp/signOut/check/syncCookies由 actions.ts 统一暴露为资源动作其中signIn还会通过isTrustedOrigin校验请求来源防止跨站登录登录 CSRF。九、开发自定义认证类型的推荐步骤结合文档与源码完整地扩展一个认证类型需要四步继承BaseAuth在构造函数中通过config.ctx.db.getCollection(users)装配用户表并调用super()实现validate()从ctx.action.params.values提取凭据校验通过返回用户 Model否则抛 401如有需要同时覆写signUp()注册类型在插件加载时调用ctx.app.authManager.registerTypes(authType, { auth: CustomAuth, title })其中authType需要全局唯一配置认证器在管理后台创建对应类型的认证器实例Authenticator其options会作为this.authenticator.options在运行时传入可用于控制注册开关、表单字段等行为。BaseAuth之所以值得作为扩展基类在于它把 token 签发、校验、续期、黑名单、Cookie 会话、用户缓存这些又复杂又容易出错的公共逻辑全部沉淀下来让开发者只需聚焦validate()这一个业务点。若你计划实现手机号验证码、OAuth 第三方登录等新认证方式从复制 BasicAuth 的结构开始是最稳妥的路径。【免费下载链接】nocobaseNocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考