完全指南:从 Cookie 会话到 WebAuthn 的源码级解析`)
后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载dbAuth 是 Redwood 框架内置的自托管认证方案它让您使用自己的数据库存储用户凭据、自建登录/注册/忘记密码页面全程不依赖任何第三方认证服务。本文以官方文档为主线结合redwoodjs/auth-dbauth-api等包的源码实现完整讲解 dbAuth 的会话原理、安装配置、全部配置项、密码安全存储机制以及 WebAuthn 无密码登录让您读完后能独立完成一套生产可用的自托管认证系统。dbAuth 是什么自托管认证的优势与代价Redwood 的 dbAuth 是一套自己掌控一切的认证方案它带来如下收益见 官方文档使用您自己的数据库存储用户凭据使用您自己的登录、注册、忘记密码页面也可用 Redwood 生成好的页面可自定义登录会话时长无外部依赖用户数据永远不离开您的服务器不会因用户数量产生额外费用或限额第三方服务故障不会影响您的站点而它唯一的潜在缺点同样也是使用自己的数据库存储用户凭据——您必须亲自对凭据存储安全负责。为此Redwood 遵循业界最佳实践用户密码经过加盐 哈希PBKDF2/scrypt后才落库明文密码绝不存储在任何地方仅在登录/注册阶段于客户端与服务器之间传输且应始终走 HTTPS日志器会清洗敏感参数如password后再输出数据库只保存重置令牌reset token的哈希值即使在迁移到第三方认证服务的场景下dbAuth 也是快速起步的绝佳选择——官方甚至提供了生成基础登录/注册页面的生成器。工作原理基于 Cookie 的会话机制dbAuth 依靠经典的 Cookie 判断用户是否登录。其完整流程为对应官方文档 How It Works 一节用户尝试登录时api 侧的一个 serverless 函数即api/src/functions/auth.ts检查是否存在给定用户名内部字段称为username实际可以是任何字段如邮箱的用户若找到该用户则校验数据库中加盐哈希后的密码是否匹配匹配成功后服务器向浏览器下发一个HttpOnly、Secure、SameSite的会话 Cookie内容为用户的 ID。Cookie 的内容本身是一个简单字符串但会用密钥做AES 加密详见下文源码解析用户发起 GraphQL 请求时服务器解密 Cookie确认其中包含的用户 ID 在数据库中仍然存在若存在则放行一旦检测到任何异常Cookie 无法正常解密或 Cookie 中的用户 ID 在数据库中不存在用户会被立即登出——通过让会话 Cookie 过期实现。源码佐证会话加密与解密在 shared.ts 中会话使用AES-256-CBC加密密钥取SESSION_SECRET环境变量的前 32 字节初始化向量IV为 16 字节随机数加密结果以加密数据|IV(base64)的格式写入 Cookieexport const encryptSession (dataString: string) { const iv crypto.randomBytes(16) const cipher crypto.createCipheriv( aes-256-cbc, (process.env.SESSION_SECRET as string).substring(0, 32), iv, ) let encryptedData cipher.update(dataString, utf-8, base64) encryptedData cipher.final(base64) return ${encryptedData}|${iv.toString(base64)} }对应的 decryptSession 会先按|分割出密文与 IV若不含|则回退到旧版 CryptoJS 算法解密兼容升级前的会话。解密失败时抛出SessionDecryptionError在 DbAuthHandler 的构造函数中被捕获并标记hasInvalidSession随后invoke()直接返回登出响应源码。从源码结构还可以看到会话内容实际上是JSON.stringify({ id: 用户ID }) ; CSRF Token见 _createSessionCookieString登录成功时还会额外下发一个auth-providerdbAuth的 Cookie_createAuthProviderCookieString。快速开始Setup 与数据库字段一条 CLI 命令即可完成 dbAuth 的安装不含登录/注册页面本身yarn rw setup auth dbAuth执行时您会被询问是否启用WebAuthn支持允许 TouchID、FaceID、USB 指纹扫描器等设备登录。若打算使用 WebAuthn在此输入y并阅读后续配置章节。您也可以为已有的 dbAuth 安装后续追加 WebAuthn。请仔细阅读安装后的提示信息其中包含为数据库添加哈希密码、盐字段的说明以及如何根据存储用户数据的表名配置认证 serverless 函数。核心内容如下不含 WebAuthn 附加选项完整内容以setup命令输出为准。为 User 模型添加字段model User { id Int id default(autoincrement()) email String unique hashedPassword String // ─┐ salt String // ─┼─ 添加这几行 resetToken String? // ─┤ resetTokenExpiresAt DateTime? // ─┘ }如果表中已有存量用户记录必须提供默认值否则 Prisma 会报错hashedPassword String default() salt String default()配置 authFields 字段映射Redwood 需要知道id与username字段的名称。以下示例使用id与email在api/src/functions/auth.js的authFields中配置此处也可指定hashedPassword、salt等字段的自定义名称authFields: { id: id, username: email, hashedPassword: hashedPassword, salt: salt, resetToken: resetToken, resetTokenExpiresAt: resetTokenExpiresAt, },调整 getCurrentUser要获取实际登录的用户请查看api/src/lib/auth.js中的getCurrentUser()。默认实现很简洁但若您的模型名或唯一 ID 字段名不同需要同步修改其中的查询调用代码上方有注释说明。该函数返回的对象会成为 web 侧的currentUser与 api 侧的context.currentUser因此应谨慎控制select出来的字段详见 auth.ts.template 的警告注释。生成 SESSION_SECRET安装过程会为您的.env生成一个SESSION_SECRET环境变量。该值不应纳入版本控制并且每个部署环境都应使用唯一的值。如果您需要一次性让所有用户下线将密钥改成新值即可。生成新密钥的命令yarn rw g secret需要简易的登录、注册、忘记密码页面官方同样提供了生成器yarn rw generate dbAuth注意如果您修改了hashedPassword与salt的字段名并且应用中有较多日志输出请参考 日志脱敏Redaction 文档将这些字段从日志中清洗掉。脚手架生成登录/注册/忘记密码页面不想从零手写页面直接运行yarn rw g dbAuth执行时同样会询问是否生成支持 WebAuthn 的 LoginPage 版本输入y并按提示配置即可。默认路由会让页面位于/login、/signup、/forgot-password、/reset-password改起来也很容易。再次检查安装后提示中需要修改的一处登录/注册成功后重定向到哪个页面。即便您想自己写页面也建议以生成的页面为起点——它们已经包含了提交登录凭据、注册字段到服务器处理所需的其余代码。配置详解DbAuthHandler 的全部选项dbAuth 几乎所有配置都集中在api/src/functions/auth.js中传给DbAuthHandler初始化的那个对象里模板文件 中每个键上方都有注释说明。下面是各重要选项的逐一解析。对应类型定义见 DbAuthHandler.ts 中的DbAuthHandlerOptions接口。allowedUserFieldsallowedUserFields: [id, email]大多数认证 handler 会接收一个user参数某些 handler 还会返回该user对象。作为安全措施allowedUserFields限定了这些对象中仅哪些属性对外可见防止敏感数据被意外泄漏给客户端。默认值为id和email可自行追加user上的任意属性。:::info 为什么这很重要signup与forgotPassword的 handler 会把返回值原样返回给客户端常用于展示验证邮件已发送到某邮箱之类的信息。若没有allowedUserFields白名单开发者很容易在 handler 中直接return user从而把hashedPassword和salt一并暴露——任何用户打开浏览器 Web Inspector 就能看到这些明文值 :::源码层面_sanitizeUser 会在返回前删除不在allowedUserFields中的全部字段默认白名单常量为[id, email]源码。login.enabled是否允许用户调用登录接口默认true需显式设为false才能关闭该流程login: { enabled: false }login.handler()如果除了用户名/密码正确就放行之外还想做额外校验可在login.handler()中追加逻辑。例如凭据正确但用户尚未验证邮箱时在此抛错并给出提示信息若允许登录则直接返回传入的唯一参数userlogin: { handler: (user) { if (!user.verified) { throw new Error(Please validate your email first!) } else { return user } } }从源码看登录流程是 _verifyUser 先校验用户名与密码然后调用login.handler(dbUser)最后要求 handler 返回的对象包含authFields.id字段否则抛出NoUserIdErrorlogin 方法。signup.enabled是否允许用户注册默认true需显式设为false才能关闭signup: { enabled: false }signup.handler()该函数负责真正在数据库中创建用户。它接收一个对象包含创建用户所需的全部字段username、hashedPassword、salt以及注册表单中额外字段组成的userAttributes对象signup: { handler: ({ username, hashedPassword, salt, userAttributes }) { return db.user.create({ data: { email: username, hashedPassword: hashedPassword, salt: salt, name: userAttributes.name, }, }) } }在signup.handler()被调用前dbAuth 会先检查用户名是否唯一若重复则抛错源码见 _createUser其中还会先对密码执行passwordValidation校验再调用hashPassword(password)生成哈希与盐最后才调用您的 handler。handler 内有三种返回方式决定注册如何继续一切正常且注册后应立即登录返回刚创建的用户用户可创建但不想自动登录返回一个字符串该字符串会由从useAuth()解构出来的signUp()函数返回无论何种原因不允许注册在该函数中抛出错误及要展示的消息。处理第 2 种情况的组件/页面代码示例const { signUp } useAuth() const onSubmit async (data) { const response await signUp({ ...data }) if (response.message) { toast.error(response.message) // 用户已创建但未登录 } else { toast.success(Welcome!) // 用户已创建并登录 navigate(routes.dashboard()) } }signup.passwordValidation()用于校验注册时提交的密码是否符合您的标准长度、复杂度等。默认返回true即密码总是有效dbAuth 内置校验保证密码非空白、非空字符串、非纯空格。您可以修改它来强制任何密码策略。密码有效返回true否则抛出PasswordValidationError并附带可选的原因说明signup: { passwordValidation: (password) { if (password.length 8) { throw new PasswordValidationError( Password must be at least 8 characters ) } if (!password.match(/[A-Z]/)) { throw new PasswordValidationError( Password must contain at least one capital letter ) } return true } }为了最佳用户体验建议在客户端也做同样的校验避免无效密码白白往返服务器但服务端校验能防止有人绕开页面程序化提交注册。forgotPassword.enabled是否允许用户通过调用forgotPassword请求新密码默认true需显式设为false才能关闭。关闭该流程时通常也应同时关闭resetPasswordforgotPassword: { enabled: false }forgotPassword.handler()当用户在忘记密码页面提交的用户名/邮箱能查到对应账户时此 handler 被调用该用户作为参数传入。在这里您需要把重置密码的链接发给用户最常见的是发邮件。链接默认格式为https://example.com/reset-password?resetToken${user.resetToken}如果您修改了路由中重置密码页面的路径或为resetToken数据库字段用了其他名称都需要在此同步修改https://example.com/reset-password?resetKey${user.resetKey}注意虽然用户表中只保存resetToken的哈希但仅在 handler 内部user.resetToken会是原始resetToken用于生成密码重置链接。源码佐证_forgotPassword 中令牌的生成逻辑为md5(uuidv4())后转 base64 并截取前 16 个字符落库时使用hashToken(token)即 SHA-256 哈希见 shared.ts而 handler 收到的token是明文令牌正好用于拼接重置链接。令牌有效期由forgotPassword.expires控制默认 24 小时即60 * 60 * 24秒。resetPassword.enabled是否允许用户通过forgotPassword得到的验证码重置密码默认true需显式设为false才能关闭。关闭时通常也应同时关闭forgotPasswordresetPassword: { enabled: false }resetPassword.handler()该 handler 在数据库中密码被成功修改后被调用。返回真值如return user会在密码修改后自动登录用户若希望用户回到登录页手动登录则return false并在 Reset Password 页面将用户重定向到登录页。源码中的 resetPassword 方法 还揭示了一个重要细节它同样会先执行signup.passwordValidation校验新密码并通过allowReusedPassword选项控制是否允许复用旧密码默认false时若新旧密码相同会抛出ReusedPasswordError重置成功后调用_clearResetToken清除数据库中的令牌哈希与过期时间。usernameMatch大小写不敏感的用户名匹配该配置允许在数据库查询时对用户名做大小写不敏感的检查。您需要同时为signup和login分别配置signup: { usernameMatch: insensitive }login: { usernameMatch: insensitive }默认情况下无需任何设置因为每种数据库对此特性的支持规则不同。请按下表为您的数据库选择正确的usernameMatchStringDB默认值usernameMatchString说明PostgresdefaultinsensitiveMySQLcase-insensitiveN/A默认已开启无需设置MongoDBdefaultinsensitiveSQLiteN/AN/A[不支持] 不敏感检查只能在单列级别定义Microsoft SQL Servercase-insensitiveN/A默认已开启无需设置源码实现位于 _getUserMatchCriteriaOptions未配置时直接用{ username: username }精确查询配置后转为 Prisma 的{ equals: username, mode: usernameMatchFlowOption }查询。Cookie 配置以下选项决定跟踪客户端授权状态的 Cookie 在浏览器中的存储方式。默认配置适用于大多数场景。如果您的 web 与 api 部署在不同域名需要做一些修改将SameSite设为None并添加 CORS 配置。cookie: { attributes: { HttpOnly: true, Path: /, SameSite: Lax, Secure: true, // Domain: example.com, }, // name: session_%port%, }Cookie 名称默认为session_%port%也可自定义其中%port%会被替换为 api 服务器实际运行的端口见 generateCookieName它读取redwood.toml的api.port配置。若同一主机上运行多个 Redwood 应用务必为它们设置不同的 Cookie 名称模板注释。CORS 配置如果 dbAuth 的 api 与 web 部署在不同域名您需要为 GraphQL 与 dbAuth 整体配置 CORS并额外启用几个选项以确保 XHR 请求能发送/接受凭据。完整说明请参阅 CORS 文档。在源码中DbAuthHandlerOptions.cors会通过createCorsContext创建 CORS 上下文并在invoke()中处理预检请求源码。错误消息自定义dbAuth 内置了若干错误消息包括用户名/邮箱未找到密码错误重置密码令牌已过期默认消息虽得体但未必贴合您站点的语气。您可以在api/src/functions/auth.js中为login、signup、forgotPassword、resetPassword各配置对象的errors属性中自定义这些消息。生成的文件中包含大量注释说明每条错误消息分别在什么情况下展示。环境变量Cookie Domain跨域 Cookie默认情况下会话 Cookie 不设置Domain属性浏览器会将其视为仅当前域名。如果您的站点横跨多个域名例如站点在example.com而 api 部署在api.example.com需要显式设置Domain以便 Cookie 在两个域间共享。在api/src/functions/auth.js配置中设置cookie.attributes.Domain值为站点根域名其所有子域也都能读取。例如cookie: { attributes: { HttpOnly: true, Path: /, SameSite: Lax, Secure: process.env.NODE_ENV ! development ? true : false, // highlight-next-line Domain: example.com }, // name: session_%port% }Session Secret Key会话密钥需要更换加密会话 Cookie 的密钥或部署到新环境每个部署环境都应有自己唯一的密钥时使用 CLI 工具生成新密钥yarn rw g secret注意输出的密钥不会自动追加到.env或任何文件它只是打印到屏幕上之后需要您自己放到正确的位置。:::warning .env 与版本控制.env文件已被 git 忽略不会提交到版本控制。另有一个.env.defaults文件设计为可安全提交、包含团队共享的简单环境变量。而会话 Cookie 的加密密钥不属于可共享变量 :::深入底层密码与令牌的安全存储实现官方文档强调遵循最佳实践存储凭据这里结合源码把具体实现讲透。密码哈希scrypt当前版本的 hashPassword 使用 Node 内置crypto.scryptSync默认参数为cost: 2 ** 1416384、blockSize: 8、parallelization: 1盐为 32 字节随机数的十六进制串。哈希结果以哈希值|cost|blockSize|parallelization的格式存储这样验证时可以提取参数、按相同强度重算比对extractHashingOptions。旧版兼容与自动迁移早期版本的legacyHashPassword源码使用 CryptoJS 风格的 PBKDF2SHA1、1 次迭代。_verifyPassword 在检测到旧格式哈希时会用新算法重新哈希并写回数据库实现无感迁移。重置令牌forgotPassword生成的明文令牌只在邮件链接中出现数据库中存储的是其 SHA-256 哈希hashToken从根本上杜绝了数据库泄露即可盗用重置链接的风险_findUserByToken 还校验令牌是否过期过期即清除并报ResetTokenExpiredError。启动校验_validateOptions 会在实例化时强制检查必须存在SESSION_SECRET、login.expires、四个流程的 handler以及 WebAuthn 与credentialModelAccessor必须成对出现。对应的错误类型定义在 errors.ts。WebAuthn无密码认证扩展简介与用户流程WebAuthn 是 W3C 与 FIDO 联合编写Google、Mozilla、Microsoft 等参与的规范定义了用公钥密码学替代密码进行用户认证的标准方式。通俗讲用户可以用 TouchID、FaceID、Windows Hello、Yubikey 等设备登录。下文把所使用的生物识别设备统称为设备。WebAuthn 流程包含两个阶段注册Registration用户首次为新设备建档一个用户可注册多台设备认证Authentication设备被识别后后续访问可直接用它登录。注册阶段的用户体验用户先以用户名/密码登录 → 被询问是否启用 WebAuthn → 若选择启用则弹出浏览器的扫描提示 → 若跳过则正常进入站点下次退出再登录时会再次收到启用提示。认证阶段的用户体验设备已注册时用户一进入登录页就会立即看到扫描提示若提示未出现或误取消可点击 Open Authenticator 重新唤起用户也可以改用用户名/密码登录。工作原理前后端交互注册Registration用户选择启用设备后前端向服务器请求注册选项一个包含服务器与用户信息的 JSON 对象域名、用户名应用收到数据后调用浏览器 API携带收到的选项启动生物识别读取器用户扫描指纹/面部后浏览器 API 返回一个设备 ID、公钥及若干用于服务器校验的字段这些数据被发送到服务器验证。验证通过后设备被保存到数据库的UserCredential表中表名可改服务器同时在用户浏览器上放置一个包含设备 ID随机字母数字串的 Cookie。认证Authentication如果上一步的 Cookie 存在web 侧知道该用户已注册过设备于是向服务器请求认证选项服务器根据 Cookie 中的凭据 ID 查找用户取出其历史注册的全部设备列表连同域名、用户名一起返回web 侧收到选项后调用浏览器 API。浏览器先检查服务器返回的设备列表是否包含当前设备若是则提示用户扫描指纹/面部若不在列表则引导用户回到用户名/密码登录设备 ID、公钥、用户信息与签名被发送到服务器服务器验证签名确实是用对应公钥加密的预期数据。验证通过后设置常规登录 Cookie与用户名/密码登录相同。两种情况下设备的实际扫描与匹配都由操作系统完成我们只关心能否从设备拿到一个凭据 ID 与公钥。浏览器支持截至 2022 年 7 月OSBrowserAuthenticatormacOSFirefoxYubikey Security Key NFC (USB)、Yubikey 5Ci、SoloKeymacOSChromeTouch ID、Yubikey Security Key NFC (USB)、Yubikey 5Ci、SoloKeyiOSAllFace ID、Touch ID、Yubikey Security Key NFC (NFC)、Yubikey 5CiAndroidChromeFingerprint Scanner、caBLEAndroidFirefoxScreen PIN配置步骤启用 WebAuthn 需要对代码库做四处修改添加UserCredential模型在api/src/functions/auth.js中添加配置选项在App.js的AuthProvider中添加client在登录流程中加入提示用户启用设备的界面。:::info 已自动完成的情况 如果您在安装 dbAuth 时选择了 WebAuthn并用--webauthn生成了 LoginPage那么以上步骤都已自动完成。按照安装后提示只需为User模型添加必需字段、创建UserCredential模型即可使用。如果最初未启用 WebAuthn现在想补上可以带--force标志重新运行 setup 与生成命令覆盖现有文件。您对文件的修改会被覆盖但通过 git diff 通常能快速迁移大部分改动。 :::Schema 更新需要为User模型添加两个字段并新建UserCredential模型来存储设备并与用户关联datasource db { provider sqlite url env(DATABASE_URL) } generator client { provider prisma-client-js binaryTargets native } model User { id Int id default(autoincrement()) email String unique hashedPassword String salt String resetToken String? resetTokenExpiresAt DateTime? // highlight-start webAuthnChallenge String? unique credentials UserCredential[] // highlight-end } // highlight-start model UserCredential { id String id userId Int user User relation(fields: [userId], references: [id]) publicKey Bytes transports String? counter BigInt } // highlight-end运行yarn rw prisma migrate dev将变更应用到数据库。:::warning 切勿让 GraphQL 暴露 UserCredential 顾名思义这个新模型包含用户的密钥凭据信息。不应通过在api/src/graphql中添加 SDL 文件让这些数据公开。另外如果您重新为User模型生成 SDL生成器会自动把credentials关联包含进去它对所有关联都这样处理。这会导致 API 服务器读取新 SDL 时在控制台报错警告UserSDL 引用了不存在的UserCredential类型因为没有userCredential.sdl.js定义它。若重新生成后看到此提示请从UserSDL 中删除下面这行credentials: [UserCredential]!:::Function Config函数配置接下来让 dbAuth 知道新字段与模型名称以及 WebAuthn 的行为方式高亮部分为新增import { db } from src/lib/db import { DbAuthHandler } from redwoodjs/api export const handler async (event, context) { // 其他 handler 配置…… const authHandler new DbAuthHandler(event, context, { db: db, authModelAccessor: user, // highlight-start credentialModelAccessor: userCredential, // highlight-end authFields: { id: id, username: email, hashedPassword: hashedPassword, salt: salt, resetToken: resetToken, resetTokenExpiresAt: resetTokenExpiresAt, // highlight-start challenge: webAuthnChallenge, // highlight-end }, cookie: { attributes: { HttpOnly: true, Path: /, SameSite: Lax, Secure: process.env.NODE_ENV ! development ? true : false, }, }, forgotPassword: forgotPasswordOptions, login: loginOptions, resetPassword: resetPasswordOptions, signup: signupOptions, // highlight-start webAuthn: { enabled: true, expires: 60 * 60 * 14, name: Webauthn Test, domain: process.env.NODE_ENV development ? localhost : server.com, origin: process.env.NODE_ENV development ? http://localhost:8910 : https://server.com, type: platform, timeout: 60000, credentialFields: { id: id, userId: userId, publicKey: publicKey, transports: transports, counter: counter, }, }, // highlight-end }) return await authHandler.invoke() }各选项含义如下credentialModelAccessor指定访问存储凭据模型的 accessor 名称。若模型名为UserCredential按 Prisma 命名约定此值就是userCredentialauthFields.challenge用户模型中存放 WebAuthn challenge 字符串的字段名。每当一次 WebAuthn 注册或认证请求开始时自动生成该字符串是浏览器请求确实来自该用户的又一道验证。一个用户同一时间只能进行一次 WebAuthn 请求/响应循环——也就是说用户不能在桌面浏览器弹出 TouchID 提示后又切到 iOS Safari 用 FaceID再回到桌面扫描指纹。最新的 WebAuthn 请求会覆盖进行中的旧请求webAuthn.enabled布尔值表示服务器是否响应 WebAuthn 请求。若决定停用 WebAuthn除在这里关闭外还要更新 LoginPage 停止提示webAuthn.expires允许用户持续使用指纹/面部扫描重新认证的秒数。一旦过期用户下次必须用用户名/密码认证之后 WebAuthn 会再次启用同样持续该时长。出于安全考虑您可以设定无操作一小时后强制登出但允许用户轻松用指纹/面部在接下来两周内重新认证类似 macOS 的 TouchID 会话机制。此例中可将login.expires设为60 * 60webAuthn.expires设为60 * 60 * 24 * 14webAuthn.name应用名称会显示在部分浏览器使用设备的提示中webAuthn.domain发起请求的域名仅 URL 的域名部分如app.server.com开发模式下为localhostwebAuthn.origin包含协议与端口的完整来源如https://app.server.com开发模式为http://localhost:8910webAuthn.type允许使用的设备类型见下节webAuthn.timeout等待设备被使用的毫秒数默认 60 秒webAuthn.credentialFieldsdbAuth 内部使用的期望字段名到模型中实际字段名的映射共 5 个字段id、userId、publicKey、transports、counter。WebAuthntype选项webAuthn.type可设为any、platform或cross-platformplatform仅允许嵌入式设备TouchID、FaceID、Windows Hellocross-platform仅允许第三方设备如 Yubikey USB 指纹读取器any平台设备与跨平台设备都允许。在部分浏览器中这会造成明显的 UX 差异。例如 macOS 的 Chrome 上内置 TouchID 的 MacBook Proany会同时出现添加新的 Android 手机展示 QR 码、USB 安全密钥第三方 USB 设备指纹扫描、此设备标准 TouchID 界面等选项界面最灵活但也最容易让用户困惑platform界面最简单用惯 TouchID/FaceID 的用户会立刻感到熟悉。注意此时仍可回退到电脑本身的账户密码密码与 TouchID 扫描会被计为同一台设备可交替使用cross-platform界面与any相同但没有此设备选项。any虽然最灵活但也最容易让用户困惑。若计划允许任意设备建议做 user-agent 检测并向用户解释各选项的实际含义。api 侧配置至此就绪。App.js 更新如果您用yarn rw g dbAuth --webauthn生成了登录/注册页面则以下改动已就位可直接使用 WebAuthn否则请继续阅读。首先导入WebAuthnClient并传给AuthProvider组件import { AuthProvider } from redwoodjs/auth // highlight-start import WebAuthnClient from redwoodjs/auth-dbauth-web/webAuthn // highlight-end import { FatalErrorBoundary, RedwoodProvider } from redwoodjs/web import { RedwoodApolloProvider } from redwoodjs/web/apollo import FatalErrorPage from src/pages/FatalErrorPage import Routes from src/Routes import ./scaffold.css import ./index.css const App () ( FatalErrorBoundary page{FatalErrorPage} RedwoodProvider titleTemplate%PageTitle | %AppTitle // highlight-start AuthProvider typedbAuth client{WebAuthnClient} // highlight-end RedwoodApolloProvider Routes / /RedwoodApolloProvider /AuthProvider /RedwoodProvider /FatalErrorBoundary ) export default App现在可以访问 WebAuthn client 提供的功能了。最简单的做法是运行yarn rw g dbAuth --webauthn生成新的 LoginPage哪怕是在一个全新的临时应用中然后拷贝所需片段或直接用其替换现有登录页。登录流程的要点是用户名/密码认证通过后需要停一步若浏览器支持 WebAuthn 则给用户注册设备的机会当用户带着webAuthnCookie 来到登录页时可直接展示认证提示而完全跳过用户名/密码表单。Redwood 生成的 LoginPage 模板已完整处理这些逻辑。WebAuthn Client API传给AuthProvider的client可以从useAuth()中解构出来const { isAuthenticated, client, logIn } useAuth()client提供四个与 WebAuthn 相关的函数client.isSupported()返回 Promiseresolve 为布尔值——当前浏览器是否支持 WebAuthnclient.isEnabled()返回布尔值——用户当前是否有webAuthnCookie即此设备已注册、可用于登录client.register()返回 Promise从服务器获取选项 → 弹出扫描指纹/面部提示 → 把结果提交给服务器。成功时 resolve 为{ verified: true }失败则抛出错误。用于设备尚未注册client.isEnabled()返回false的场景client.authenticate()返回 Promise同样执行取选项 → 扫描 → 提交流程成功 resolve 为{ verified: true }或抛错。用于设备已注册client.isEnabled()返回true的场景。从 web 侧源码dbAuth.ts可以看到登录、注册、忘记密码等调用通过 POST 到auth端点默认${RWJS_API_URL}/auth启用 SSR 中间件时则为/middleware/dbauth并携带method参数完成getToken结果带有 5 秒的缓存窗口TOKEN_CACHE_TIME。api 侧 _getAuthMethod 会依次从查询参数与 JSON body 中解析method并在 METHODS/VERBS 中校验方法与 HTTP 动词是否匹配登录、注册等为 POSTgetToken与 WebAuthn 选项获取为 GET。至此从基础的 Cookie 会话机制、完整的配置项解析到密码哈希存储与 WebAuthn 无密码登录的端到端流程您已掌握构建一套自托管认证系统的全部关键知识。更多细节可在生成的文件注释与redwoodjs/auth-dbauth-api、redwoodjs/auth-dbauth-web包源码中继续探索。赞分享后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载相关推荐web.py会话管理终极指南从Cookie到用户认证的完整教程web.py会话管理终极指南从Cookie到用户认证的完整教程 web.py是一个简单而强大的Python web框架其会话管理系统为开发者提供了完整的用户后端Web框架Epic Stack 认证体系实战指南会话、OAuth、WebAuthn Passkeys 与 TOTP 2FA 全解析Epic Stack 认证体系实战指南会话、OAuth、WebAuthn Passkeys 与 TOTP 2FA 全解析 Epic Stack 是一套开箱即用后端前端开发工具认证鉴权Litestar SessionAuth 会话认证后端完整指南从配置到源码原理Litestar SessionAuth 会话认证后端完整指南从配置到源码原理 导读 SessionAuth 是 Litestar 内置的基于会话Sessi后端Web框架上一篇RDFLib实战指南SPARQL查询语言详解与应用下一篇QQ空间说说批量导出完整指南把每一句话、每一张照片存进自己硬盘创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。