Wasp 认证实战:用用户名密码为 TodoApp 添加完整用户体系
2026/9/16 0:17:27 网站建设 项目流程

Wasp 认证实战:用用户名密码为 TodoApp 添加完整用户体系

【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp

本篇指南以 Wasp 官方教程(web/versioned_docs/version-0.18/tutorial/07-auth.md)为主线,完整讲解如何为 Wasp 应用接入用户名 + 密码的一站式认证:从数据建模、配置声明、登录注册页面,到页面级访问控制、按用户隔离数据的 Queries/Actions,以及登出功能。读完你将掌握 Waspauth配置块的每一个字段、authRequiredcontext.user的底层工作原理,并能直接照搬到自己的多用户应用中。

Wasp 将认证做成了框架的一等公民:你不需要手写密码哈希、会话管理、JWT 中间件或表单校验,只需在配置里声明“我要用哪种认证方式”,Wasp 就会在代码生成阶段为你产出完整的认证后端与可用的 UI 组件。本文结合仓库中完整的 TodoApp 示例(examples/tutorials/TodoApp)与生成器模板源码(waspc/data/Generator/templates),逐层拆解认证的接入步骤与底层实现。

接入认证前的任务清单

要让应用拥有用户体系,需要依次完成以下工作:

  • 创建User实体(Entity)。
  • 在 Wasp 配置中启用Username and Password认证。
  • 添加登录(login)与注册(signup)页面。
  • 将主页面设为需要认证(authRequired)。
  • UserTask实体之间建立关联。
  • 修改 Queries 与 Actions,让用户只能看到和修改自己的任务。
  • 添加登出(logout)按钮。

注意一个版本差异:本教程文档(v0.18)使用的是经典.wasp声明文件语法(app TodoApp { ... }),而当前仓库中的完整示例已升级为 TypeScript spec 新语法(examples/tutorials/TodoApp/main.wasp.ts,声明wasp: { version: "0.26.0" })。两者的声明内容是等价的,下文会同时给出两种写法,方便对照。

第一步:创建 User 实体

Wasp 负责管理认证本身,因此认证相关的实体(如AuthAuthIdentitySession)会由 Wasp 在后台自动创建,你完全不需要手动维护。你只需要定义一个User实体,用来记录“哪条任务属于哪个用户”:

// ... model User { id Int @id @default(autoincrement()) }

在仓库的完整示例中,这个实体定义在 examples/tutorials/TodoApp/schema.prisma,数据源使用 SQLite(provider = "sqlite",URL 取自DATABASE_URL环境变量),并保留了 Wasp 要求的prisma-client-jsgenerator。

第二步:在 Wasp 中启用认证

接下来在 Wasp 配置中声明全栈认证。经典.wasp语法如下:

app TodoApp { wasp: { version: "{latestWaspVersion}" }, title: "TodoApp", head: [ "<link rel='icon' href='/favicon.ico' />", ], auth: { // Tells Wasp which entity to use for storing users. userEntity: User, methods: { // Enable username and password auth. usernameAndPassword: {} }, // We'll see how this is used in a bit. onAuthFailedRedirectTo: "/login" } }

等价的新版 TypeScript spec 写法(见 examples/tutorials/TodoApp/main.wasp.ts):

import { action, app, page, query, route } from "@wasp.sh/spec"; export default app({ name: "TodoApp", wasp: { version: "0.26.0" }, title: "TodoApp", head: ["<link rel='icon' href='/favicon.ico' />"], auth: { userEntity: "User", methods: { usernameAndPassword: {}, }, onAuthFailedRedirectTo: "/login", }, spec: [ route("RootRoute", "/", page(MainPage, { authRequired: true })), route("SignupRoute", "/signup", page(SignupPage)), route("LoginRoute", "/login", page(LoginPage)), query(getTasks, { entities: ["Task"] }), action(createTask, { entities: ["Task"] }), action(updateTask, { entities: ["Task"] }), ], });

auth配置块中的三个字段含义如下:

字段作用本示例取值
userEntity指定用于存储用户的实体,Wasp 会把它与自动生成的Auth实体关联起来User
methods启用哪些认证方式,usernameAndPassword: {}表示开启用户名密码登录{ usernameAndPassword: {} }
onAuthFailedRedirectTo未认证用户访问受保护页面时重定向到的路径"/login"

Wasp 还支持 Google、GitHub、email 等更多认证方式,配置方法是在methods中相应扩展。认证底层涉及的概念(AuthAuthIdentitySession等实体)可以参考认证实体文档。

迁移数据库

修改配置后必须更新数据库结构:

wasp db migrate-dev

执行这一步之后,Wasp 会自动为你生成以下能力(这正是“声明式认证”的核心价值):

  • 带登录、注册表单的 Auth UI;
  • 一个logout()action;
  • 一个 React 钩子useAuth()
  • 供 Queries 和 Actions 使用的context.user

底层机制:Wasp 是如何“无中生有”实现认证的

从生成器模板源码可以看到这些能力并非黑魔法,而是由 Wasp 在构建时生成的真实代码:

  • 登录处理:模板 waspc/data/Generator/templates/server/src/auth/providers/username/login.ts 展示了用户名密码登录的完整流程——先用createProviderId('username', fields.username)构造提供商 ID,再通过findAuthIdentity查找AuthIdentity,用verifyPassword校验密码哈希(失败统一抛出createInvalidCredentialsError(),避免泄露“用户是否存在”这类信息),校验通过后调用createSession创建会话并执行登录前后钩子。
  • 密码安全存储:模板 waspc/data/Generator/templates/sdk/wasp/server/auth/utils.ts 中的sanitizeAndSerializeProviderData/ensurePasswordIsHashed负责在写入数据库前对密码做哈希处理(hashPassword),providerData以 JSON 序列化字符串保存在AuthIdentity上;重复注册同一用户名时会触发 Prisma 唯一约束错误P2002,被转换为 422HttpError
  • 会话中间件:模板 waspc/data/Generator/templates/sdk/wasp/server/core/auth.ts 是认证中间件——请求携带Authorization头时通过getSessionAndUserFromBearerToken解析会话并注入req.user/req.sessionId;没有 token 的请求会放行,由具体操作自行决定是否校验(对应代码中context.user可能为null的情况)。

第三步:添加登录与注册页面

表单由 Wasp 自动生成,我们只需声明承载它们的页面。在.wasp文件中定义路由与页面:

// ... route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { component: import { SignupPage } from "@src/SignupPage" } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { component: import { LoginPage } from "@src/LoginPage" }

新版 spec 语法则在spec数组中追加两条路由(见 examples/tutorials/TodoApp/main.wasp.ts):

route("SignupRoute", "/signup", page(SignupPage)), route("LoginRoute", "/login", page(LoginPage)),

页面的 React 实现(TS 版,与仓库中 examples/tutorials/TodoApp/src/LoginPage.jsx 和 examples/tutorials/TodoApp/src/SignupPage.jsx 一一对应):

import { Link } from 'react-router-dom' import { LoginForm } from 'wasp/client/auth' export const LoginPage = () => { return ( <div style={{ maxWidth: '400px', margin: '0 auto' }}> <LoginForm /> <br /> <span> I don't have an account yet (<Link to="/signup">go to signup</Link>). </span> </div> ) }

注册页与之几乎相同,只是换成SignupForm并链接到/login

import { Link } from 'react-router-dom' import { SignupForm } from 'wasp/client/auth' export const SignupPage = () => { return ( <div style={{ maxWidth: '400px', margin: '0 auto' }}> <SignupForm /> <br /> <span> I already have an account (<Link to="/login">go to login</Link>). </span> </div> ) }

LoginFormSignupForm均从wasp/client/auth导入,它们是 Wasp 生成的、与所选认证方式匹配的现成表单组件。如果你使用 TypeScript,还可以用 Wasp 的类型安全Link组件与routes对象替代字符串路径,详见类型安全链接文档。

第四步:让主页面要求认证

我们不希望未登录用户访问主页面(他们无法创建任务),因此把主页面设为私有:

// ... page MainPage { authRequired: true, component: import { MainPage } from "@src/MainPage" }

新版 spec 语法中对应为:

route("RootRoute", "/", page(MainPage, { authRequired: true })),

设置authRequired: true后,未认证用户会被重定向到onAuthFailedRedirectTo指定的/login;同时,页面的 React 组件会额外获得一个userprop:

import type { AuthUser } from 'wasp/auth' export const MainPage = ({ user }: { user: AuthUser }) => { // Do something with the user // ... }

authRequired 的实现原理

authRequired之所以生效,是因为 Wasp 在生成代码时用包装组件包裹了你的页面。模板 waspc/data/Generator/templates/sdk/wasp/client/app/pages/createAuthRequiredPage.jsx 展示了它的完整逻辑:

  • 通过useAuth()读取当前用户与加载状态;
  • 状态为successuser存在时,把user作为 prop 渲染原始页面;
  • user为空时渲染<Navigate to="{onAuthFailedRedirectTo}" replace />,实现重定向到登录页;
  • 加载中显示全屏Loader,出错则显示错误提示组件。

也就是说,“页面受保护 + 自动重定向 + 自动注入 user”三者是由这一层包装统一完成的。

验证:注册用户并观察数据库

现在可以测试了:访问应用主页面/会被重定向到/login,进入注册页创建一个账号后,会回到主页面并看到 Todo 列表。接着用 Prisma Studio 查看数据库:

wasp db studio

你会看到数据库中除了Task之外,多了User表;同时还能看到AuthAuthIdentitySession这些 Wasp 自动创建的内置模型——它们正是密码哈希与登录会话的载体,教程阶段你无需关心其内部结构,好奇的话可以阅读认证实体文档。

不过此时有个问题:换不同的用户登录并创建任务,所有用户看到的是同一份任务列表。这是因为我们还没有把 Queries/Actions 改成按用户隔离数据。这正是下一步要做的。

第五步:定义 User 与 Task 的关联

先建立用户与任务的一对多关系(Prisma 标准的关系声明,此处不复述其通用规则,直接看本项目的写法):

// ... model User { id Int @id @default(autoincrement()) tasks Task[] } model Task { id Int @id @default(autoincrement()) description String isDone Boolean @default(false) user User? @relation(fields: [userId], references: [id]) userId Int? }

完整定义见 examples/tutorials/TodoApp/schema.prisma。修改实体后照例需要迁移数据库:

wasp db migrate-dev

关于useruserId?可选标记:教程特意让这两个字段可选,以便保留数据库中已有的、尚未分配用户的旧任务。但这在真实项目中并不推荐——它允许出现“不属于任何人的任务”这种异常状态。正确做法是写一次数据迁移来处理存量任务(哪怕只是全部删除),本教程为了简洁才选择可空字段。

第六步:让操作校验认证并隔离用户数据

接下来改造getTasks查询与createTask/updateTask两个 action:一方面拒绝未认证请求,另一方面只操作当前登录用户的任务。

import type { Task } from 'wasp/entities' import { HttpError } from 'wasp/server' import type { GetTasks } from 'wasp/server/operations' export const getTasks: GetTasks<void, Task[]> = async (args, context) => { if (!context.user) { throw new HttpError(401) } return context.entities.Task.findMany({ where: { user: { id: context.user.id } }, orderBy: { id: 'asc' }, }) }
import type { Task } from 'wasp/entities' import { HttpError } from 'wasp/server' import type { CreateTask, UpdateTask } from 'wasp/server/operations' type CreateTaskPayload = Pick<Task, 'description'> export const createTask: CreateTask<CreateTaskPayload, Task> = async ( args, context ) => { if (!context.user) { throw new HttpError(401) } return context.entities.Task.create({ data: { description: args.description, user: { connect: { id: context.user.id } }, }, }) } type UpdateTaskPayload = Pick<Task, 'id' | 'isDone'> export const updateTask: UpdateTask< UpdateTaskPayload, { count: number } > = async (args, context) => { if (!context.user) { throw new HttpError(401) } return context.entities.Task.updateMany({ where: { id: args.id, user: { id: context.user.id } }, data: { isDone: args.isDone }, }) }

仓库中的完整 JS 版本见 examples/tutorials/TodoApp/src/queries.js 与 examples/tutorials/TodoApp/src/actions.js。要点解读:

  • context.user与 401context.user由认证中间件(server/core/auth.ts)注入;未登录时它为null,此时抛出HttpError(401)拒绝访问。HttpErrorwasp/server导入。
  • 查询隔离findManywhere中加user: { id: context.user.id },确保只能查到自己的任务。
  • 创建归属createTask通过user: { connect: { id: context.user.id } }把新任务挂到当前用户名下。
  • 为什么用updateMany:由于 Prisma 的限制,update不允许在where中指定关联字段(user.id),所以updateTask改用updateMany,并用iduser.id双重条件保证只能更新自己的任务。updateMany返回{ count },这也是其返回类型被声明为{ count: number }的原因。

完成这些改动后,每个用户都拥有彼此不可见的独立任务列表。可以用wasp db studio多建几个用户和任务验证:

第七步:添加登出按钮

最后加上登出功能,让用户能退出登录:

// ... import { logout } from 'wasp/client/auth' // ... const MainPage = () => { // ... return ( <div> {/* ... */} <button onClick={logout}>Logout</button> </div> ) }

logoutwasp/client/auth导入,是由 Wasp 生成、随认证系统一起就绪的 action。完整页面(含任务列表、创建表单与登出按钮)见 examples/tutorials/TodoApp/src/MainPage.jsx。

至此,一个可用的认证系统已完整落地,TodoApp 正式成为多用户应用:注册、登录、受保护页面、按用户隔离的数据、登出,全部由声明式配置 + 少量业务代码驱动。

小结与后续方向

回顾整个接入过程,Wasp 认证的核心心法是“声明而非实现”:你只需定义User实体、声明auth配置、放置两个页面组件,密码哈希、会话、中间件、表单 UI 便全部由代码生成器产出(可在 waspc/data/Generator/templates 中看到这些生成产物的模板源码)。在数据层面,authRequired对应 createAuthRequiredPage.jsx 的包装组件,context.user对应 server/core/auth.ts 的会话解析中间件,密码安全与登录流程对应 server/auth/utils.ts 与 username/login.ts。

下一步可以探索的方向:

  • 使用 Starter Templates 快速启动新项目;
  • 用 Web Sockets 把应用改造成实时应用;
  • 参考认证概览了解 OAuth、邮箱等其他认证方式,以及useAuth()等更多客户端 API 的细节。

【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp

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

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

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

立即咨询