RedwoodJS 认证实战:使用 dbAuth 与 @requireAuth 保护你的博客后台
2026/9/23 8:27:43 网站建设 项目流程
  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

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

本篇技术指南基于 Redwood 官方教程第 4 章"Authentication"展开,完整演示如何在 Redwood 应用中把博客管理后台迁移到/admin路径、通过内置的 dbAuth(自托管认证)实现登录注册,并利用<Private>路由组件、@requireAuth/@skipAuth指令、useAuthHook 与SESSION_SECRET会话密钥来精细化控制页面与 GraphQL 数据层的访问权限。学完本文,你将掌握 Redwood "默认安全"(secure by default)的认证体系,能独立为任意 Redwood 应用接入登录、注销、找回密码与用户信息展示。

准备后台:把管理页面迁移到/admin

教程中博客应用的四个后台路由(新建、编辑、详情、列表)最初都以/posts开头,普通访客和博主混在同一个 URL 空间里并不合理。把管理界面统一放到/admin前缀下,是常见且合理的组织方式。由于路由的name不变,所有由 scaffold 生成、基于命名路由(named routes)的<Link>都无需改动——这是 Redwood 命名路由设计带来的红利。

以 TypeScript 项目为例,修改 web/src/Routes.tsx(JavaScript 项目为web/src/Routes.js):

import { Router, Route, Set } from '@redwoodjs/router' import ScaffoldLayout from 'src/layouts/ScaffoldLayout' import BlogLayout from 'src/layouts/BlogLayout' const Routes = () => { return ( <Router> <Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost"> <Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" /> <Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" /> <Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" /> <Route path="/admin/posts" page={PostPostsPage} name="posts" /> </Set> <Set wrap={BlogLayout}> <Route path="/article/{id:Int}" page={ArticlePage} name="article" /> <Route path="/contact" page={ContactPage} name="contact" /> <Route path="/about" page={AboutPage} name="about" /> <Route path="/" page={HomePage} name="home" /> </Set> <Route notfound page={NotFoundPage} /> </Router> ) } export default Routes

访问http://localhost:8910/admin/posts,scaffold 生成的页面即可正常渲染。但路径改变只解决"组织问题",不解决"安全问题"——任何人都能直接输入这个 URL 篡改博客内容。这正是接下来引入认证的原因。

认证(Authentication)与授权(Authorization)

Redwood 官方对这两个高频混淆概念给出清晰界定:

  • 认证(Authentication):确认"你是谁",通常通过邮箱 + 密码登录,或借助 Auth0 等第三方身份提供商完成。
  • 授权(Authorization):在用户已经认证的前提下,判断"你是否被允许做某件事",一般涉及角色(roles)与权限检查,在访问某个 URL 或功能之前进行。

本章教程只聚焦认证;授权的完整实现(RBAC,基于角色的访问控制)见 教程第 7 章。

Redwood 开箱即用提供两条认证路径:

  1. 自托管(Self-hosted):用户凭证存储在你自己数据库里,也就是本教程使用的dbAuth,无需任何第三方注册,上手最快。
  2. 第三方托管(Third-party hosted):用户凭证由第三方保管,Redwood 提供多种集成(详见 认证文档),包括 Auth0、Clerk、Netlify Identity、Netlify GoTrue-JS、Magic、Nhost、Firebase 的 GoogleAuthProvider、Supabase、SuperTokens、WalletConnect 等。

无论哪条路径,最终你都得到一个"已认证用户"对象,可在应用的 web 端和 api 端同时访问。

安装 dbAuth 后端

Redwood 用生成器(generator)完成认证的大部分样板工作:一条命令安装 dbAuth 所需的后端组件,另一条命令生成登录、注册、忘记密码页面。

在项目根目录执行:

yarn rw setup auth dbAuth

过程中会询问两件事:

  • 是否覆盖已有文件/api/src/lib/auth.{js,ts}——选择yes。新应用默认生成的这个文件只是空壳(用于让@requireAuth等指令可运行),现在要用真实的实现替换它。
  • 是否启用 WebAuthn 支持——本教程选择no,WebAuthn 是独立的功能模块,教程用不到。

命令执行后会创建若干文件,并在终端打印一些收尾说明,下一步就是按其提示完成最后的定制。

创建 User 模型

dbAuth 依赖一个用户数据模型。博客应用目前还没有User模型,需要连同 dbAuth 必需的字段一起创建。打开api/db/schema.prisma,加入以下模型:

datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" binaryTargets = "native" } model Post { id Int @id @default(autoincrement()) title String body String createdAt DateTime @default(now()) } model Contact { id Int @id @default(autoincrement()) name String email String message String createdAt DateTime @default(now()) } model User { id Int @id @default(autoincrement()) name String? email String @unique hashedPassword String salt String resetToken String? resetTokenExpiresAt DateTime? }

其中idnameemail是业务字段(email@unique约束保证唯一),另外四个字段完全由 dbAuth 接管:

  • hashedPassword:用户密码与salt组合后经哈希算法处理的结果,数据库中绝不明文存密码。
  • salt:一段与密码混合的随机唯一字符串,用于抵御彩虹表攻击(rainbow table attacks)。
  • resetToken:用户忘记密码时,dbAuth 会在该字段写入一个 token,用户回来重置密码时必须携带它。
  • resetTokenExpiresAtresetToken的过期时间戳,超过该时间后 token 失效,用户需要重新走一遍"忘记密码"表单。

创建好模型后迁移数据库,迁移名称建议为 "create user":

yarn rw prisma migrate dev

至此数据库部分全部完成。从仓库中的 dbAuth 安装模板 auth.ts.template 可以看到,dbAuth 生成的getCurrentUser默认实现也正是以session.id去查db.user,只返回id字段——这与教程的模型约定完全对应。

私有路由:用<Private>锁住后台页面

在 api 端,dbAuth 安装时会在 SDL 层注入认证校验。以posts为例,重新加载/admin/posts时,因为@requireAuth指令的存在,未登录用户发起的 GraphQL 请求会被拒绝,数据不会返回——后台数据已经"半个"安全了。但更理想的做法是未登录用户根本看不到后台页面本身,而不是看到页面骨架却拿不到数据。

Redwood 路由层提供了<Private>组件(当前实现位于 router.tsx,源码中要求必须指定字符串类型的unauthenticated属性,否则会抛错提示 "You must specify anunauthenticatedroute when using PrivateSet")。用法是把需要登录才能访问的路由包进<Private>,并告诉它未认证用户该跳去哪条命名路由:

import { Private, Router, Route, Set } from '@redwoodjs/router' import ScaffoldLayout from 'src/layouts/ScaffoldLayout' import BlogLayout from 'src/layouts/BlogLayout' const Routes = () => { return ( <Router> <Private unauthenticated="home"> <Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost"> <Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" /> <Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" /> <Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" /> <Route path="/admin/posts" page={PostPostsPage} name="posts" /> </Set> </Private> <Set wrap={BlogLayout}> <Route path="/article/{id:Int}" page={ArticlePage} name="article" /> <Route path="/contact" page={ContactPage} name="contact" /> <Route path="/about" page={AboutPage} name="about" /> <Route path="/" page={HomePage} name="home" /> </Set> <Route notfound page={NotFoundPage} /> </Router> ) } export default Routes

这里unauthenticated="home"表示未登录访问被拒后跳转到首页。

借力 @skipAuth:重新放开公共数据

加完<Private>后再访问/admin/posts,后台页面确实进不去了——但连首页的博客文章也看不到了。原因在于首页和后台共用同一个posts查询:它在 posts.sdl.ts 中默认带有@requireAuth,全站所有使用该查询的地方都被锁死。而我们的真实需求是:未登录用户也能浏览首页文章

既然后台页面已经被<Private>保护,就可以放心地把只读查询改为@skipAuth

export const schema = gql` type Post { id: Int! title: String! body: String! createdAt: DateTime! } type Query { posts: [Post!]! @skipAuth post(id: Int!): Post @skipAuth } input CreatePostInput { title: String! body: String! } input UpdatePostInput { title: String body: String } type Mutation { createPost(input: CreatePostInput!): Post! @requireAuth updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth deletePost(id: Int!): Post! @requireAuth } `

注意细节:首页列表用posts查询,而点击文章后的详情页用的是单条post查询——只放开posts仍会看到详情页报错,必须把post也一并@skipAuth

从仓库源码可以看到这两个指令的底层实现。@requireAuth在 requireAuth.ts 中通过createValidatorDirective创建,支持roles: [String]参数并调用src/lib/authrequireAuth@skipAuth在 skipAuth.ts 中同样是验证器指令,只是验证函数为空实现,即"放行"。createValidatorDirective本身在 makeDirectives.ts 中定义:解析指令 schema 的名称并注册为VALIDATOR类型,在字段值解析后执行校验函数。

教程也提醒了一个进阶设计问题:对posts/post直接@skipAuth看似省事,但若将来给 Post 增加publishStatus之类的字段,懂 GraphQL 的人就可能借此读到草稿数据。更稳妥的做法是为公开展示单独建端点(如publicPosts/publicPost),内置逻辑只返回最小化数据;默认的posts/post保留全量数据仅供后台使用。这样安全边界才足够清晰。

记住 Redwood 的设计哲学:默认安全(secure by default)——宁可让你意外暴露得太少,也不要暴露得太多。引入认证后,你会经常遇到这种"来回调整":某些页面或查询被默认锁住,需要重新评估并放行。

生成登录、注册与忘记密码页面

继续用生成器补齐前端页面:

yarn rw g dbAuth

该命令会创建 login、signup、forgot-password 三组页面,并在终端输出后续定制说明。访问http://localhost:8910/login即可看到登录页。首次使用没有用户,可点击登录按钮下方的链接(或直接访问http://localhost:8910/signup)进入注册页。

dbAuth 默认把第一个输入框标注为 "Username",但我们的用户名实际是邮箱地址(稍后可自行修改文案)。用邮箱和密码注册一个用户,点击 "Signup" 后会自动跳回首页——注册成功即自动登录(此行为可通过 signupHandler 配置 更改)。在SignupPage源码中可以看到跳转首页的代码(教程提示留意第 21 行附近)。此时再访问http://localhost:8910/admin/posts,后台文章列表就能正常加载了。

从 web 端实现 dbAuth.ts 可以看到,signup会向后端发送{ ...attributes, method: 'signup' }的请求,forgotPassword则发送{ username, method: 'forgotPassword' }——dbAuth 通过method字段区分不同的认证动作。

添加退出登录链接

登录后如何退出?方案是在BlogLayout中加一个全站可见的退出入口,并顺带显示当前登录用户是谁。Redwood 提供useAuthHook(完整 API 见 认证文档),可在任意组件中获取登录状态、用户信息并执行登出:

import { useAuth } from '@redwoodjs/auth' import { Link, routes } from '@redwoodjs/router' type BlogLayoutProps = { children?: React.ReactNode } const BlogLayout = ({ children }: BlogLayoutProps) => { const { isAuthenticated, currentUser, logOut } = useAuth() return ( <> <header> <div className="flex-between"> <h1> <Link to={routes.home()}>Redwood Blog</Link> </h1> {isAuthenticated ? ( <div> <span>Logged in as {currentUser.email}</span>{' '} <button type="button" onClick={logOut}> Logout </button> </div> ) : ( <Link to={routes.login()}>Login</Link> )} </div> <nav> <ul> <li> <Link to={routes.home()}>Home</Link> </li> <li> <Link to={routes.about()}>About</Link> </li> <li> <Link to={routes.contact()}>Contact</Link> </li> </ul> </nav> </header> <main>{children}</main> </> ) } export default BlogLayout

三个解构出的成员各自含义:

  • isAuthenticated:布尔值,表示当前是否已登录。
  • currentUser:应用持有的当前用户信息(内容由getCurrentUser决定,下文详述)。
  • logOut:销毁用户会话并登出的函数。

理解 getCurrentUser:currentUser 的内容从哪来

页面右上角显示 "Logged in as ..." 时可能发现邮箱是空的——这是因为getCurrentUser默认实现出于安全考虑只返回用户的id(还是那句"暴露得越少越好")。它位于api/src/lib/auth.{js,ts}

import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' import { db } from './db' export const getCurrentUser = async (session) => { return await db.user.findUnique({ where: { id: session.id }, select: { id: true }, }) } export const isAuthenticated = () => { return !!context.currentUser } export const hasRole = (roles) => { if (!isAuthenticated()) { return false } // ... 支持 string / string[] 两种角色形式与 currentUser.roles 的交叉匹配 } export const requireAuth = ({ roles } = {}) => { if (!isAuthenticated()) { throw new AuthenticationError("You don't have permission to do that.") } if (roles && !hasRole(roles)) { throw new ForbiddenError("You don't have access to do that.") } }

getCurrentUser是整个认证链的"魔法所在":它的返回值就是 web 端currentUser与 api 端context.currentUser的内容。对 dbAuth 而言,传入的唯一参数session里带着已登录用户的id,函数据此用 Prisma 查库。要在页面显示邮箱,只需把email加入select

export const getCurrentUser = async (session) => { return await db.user.findUnique({ where: { id: session.id }, select: { id: true, email: true }, }) }

仓库中的安装模板 auth.ts.template 给出了完整实现并带有一整段警示注释:凡是getCurrentUser返回的字段都会暴露给客户端(成为浏览器 Web Inspector 里可见的currentUser),新增字段前务必确认其安全性。模板还提示:如果用户模型或唯一字段不同,例如用db.profile.findUnique({ where: { email: session.id } }),需要同步调整模型访问器与唯一字段名。

另外,教程中最初看到的 "You don't have permission to do that." 报错,正是requireAuth()在未认证时抛出的AuthenticationError——@requireAuth指令的验证函数最终调用的就是它。

会话密钥 SESSION_SECRET

yarn rw setup auth dbAuth执行时还会顺手修改项目根目录的.env文件:追加一个名为SESSION_SECRET的环境变量,值为一长串随机字符。这是用户浏览器中会话 Cookie 的加密密钥

  • 永远不要共享它;
  • 永远不要把它提交进仓库(.env应在.gitignore中);
  • 每个部署环境都应重新生成独立的值。

需要新密钥时运行:

yarn rw g secret

该命令只在终端输出新值,需手动复制粘贴进.env。特别提醒:如果生产环境更换了这个密钥,所有用户会在下一次请求时被强制登出——因为他们浏览器里现有 Cookie 无法用新密钥解密,必须重新登录以换取新密钥加密的 Cookie。

总结:Redwood 认证工具箱

至此,一篇博客的完整认证闭环已搭建完毕。整套机制可归纳为三个层次的组合拳:

  1. 数据层(GraphQL):用@requireAuth锁住查询/变更,用@skipAuth放行公共数据。指令通过createValidatorDirective注册为验证器,在字段解析后执行校验(见 makeDirectives.ts)。
  2. 页面层(路由):用<Private unauthenticated="...">包裹整组后台路由,未登录自动重定向(实现见 router.tsx)。
  3. 组件层(UI):在任意组件中通过useAuth()isAuthenticatedcurrentUserlogOut,按需渲染不同内容或执行登出。

如果你只想限制某些组件、或组件中的某一段内容,而不想整页受控,直接从useAuth()isAuthenticated做条件渲染即可。

关于 dbAuth 的更完整能力(自托管认证的安装与配置),以及第三方身份提供商(Auth0、Supabase、Clerk 等)的接入方式,可继续阅读 Redwood 官方认证文档 与 第三方提供商安装章节。

附加练习:用 GraphQL Playground 验证权限

还记得第 3 章 Creating a Contact 末尾的 GraphQL Playground 练习吗?现在认证已就位,再运行一次之前对受保护字段的查询,就能看到@requireAuth抛出的权限错误。不过createContact这条变更因为用了@skipAuth依旧可以正常创建——这正是"公共能力放行、私有能力锁死"的直观验证。需要说明的是,目前通过 GraphQL Playground 模拟登录态体验并不友好,Redwood 团队仍在持续改进这一体验。

  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

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

相关推荐

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

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

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

立即咨询