TTeamDesk 学习手册
本章目录 展开导航 ↓

07 · 登录认证:服务端如何知道你是谁#

第一阶段解决了“把工单保存下来”。第二阶段换一个问题:同一个系统来了几家公司,怎样确保每个人只处理自己有权访问的工单?先从登录开始,因为服务端必须先知道请求来自谁。

本章目标是走通一条链路:邮箱密码 → 认证接口 → 会话 Cookie → 后续请求验证 → 可信 userId。不需要自己写密码算法,也不用先背完所有认证术语。

1. 先把认证与授权分开#

假设你去写字楼办事:前台核验身份,证明你是小陈;门禁再判断你能否进入某家公司的办公室。

  • 认证 Authentication:这次请求对应哪个用户?
  • 授权 Authorization:这个用户能否在这个团队做这件事?

类比的边界是:服务端不是看一次页面就永远放行,而是对后续业务请求继续验证会话,并在访问资源时检查权限。登录后也可能被移出团队;身份还有效,不代表团队权限还存在。

事情 本项目负责位置 不应误解为
验证邮箱密码、管理会话 Better Auth 验证工单归属
拒绝未登录业务请求 protectedProcedure 已确认某个团队角色
查询成员关系、角色与工单归属 services/access.ts 与业务服务 相信前端传来的身份
隐藏不适用的按钮 React 页面 服务端权限保护

第二阶段功能提交是 7781bb8。当前源码又接上了草稿、分配和任务;本阶段主要阅读身份与权限相关部分,不需要切换历史提交或回滚数据库。

2. 找到登录的四个文件#

文件 负责什么 建议先找的符号
auth-form.tsx 收集注册 / 登录字段,显示结果 submit
auth-client.ts 浏览器认证客户端 createAuthClient
认证 route.ts 把 HTTP 请求交给认证库 getAuth().handler(request)
server/auth.ts 密码登录、数据库适配与会话配置 createAuth、getAuth

[...all] 是匹配多个路径片段的路由约定。注册、登录和退出请求都进入这组认证入口,由 Better Auth 继续分发。工单等业务仍走 /api/trpc,没有因为接入认证而全部搬到登录接口中。

3. 点击登录后发生什么#

打开 AuthForm.submit,当前登录调用是:

ts
// 源码节选:src/features/auth/auth-form.tsx
await authClient.signIn.email({
  email: values.email.trim().toLowerCase(),
  password: values.password,
});

前端清理邮箱两端空白并转小写,密码原样传递。不要随意对密码执行 trim(),空格可能是用户密码的一部分。

注册还会提交姓名,界面要求密码 10~128 字符;服务端配置明确设置 minPasswordLength: 10。前端规则用于尽早提示,服务端认证库才是接受请求的边界。不要因为表单有规则,就认为外部请求一定经过了它。

认证 HTTP 入口很薄:

ts
// 源码节选:src/app/api/auth/[...all]/route.ts
async function handler(request: Request) {
  return getAuth().handler(request);
}
export { handler as GET, handler as POST };

请求和响应交给库处理。应用不自行保存明文密码,也不手写“查邮箱后直接判成功”的登录逻辑。

AuthForm 会检查 result.error,认证失败显示通用提示;网络异常走 catch。所以“Promise 没抛错”不一定表示登录成功,还要看返回结果。

4. Better Auth 如何接到数据库#

服务端配置中的几个关键点:

ts
// 源码节选:src/server/auth.ts,省略其他选项
return betterAuth({
  appName: 'TeamDesk',
  secret,
  baseURL,
  database: prismaAdapter(db, { provider: 'postgresql' }),
  emailAndPassword: { enabled: true, minPasswordLength: 10 },
  session: { cookieCache: { enabled: false } },
});

prismaAdapter 让认证库通过 Prisma 保存用户、账号和会话;db 来自现有数据库连接,而不是另建一套用户数据库。

getAuth() 延迟读取 BETTER_AUTH_SECRET 与 BETTER_AUTH_URL、检查配置后创建实例,并在当前进程复用。构建或导入文件不需要立刻初始化数据库。认证密钥不是用户密码,也不是“管理员密码”;它是认证服务的私密配置。

密码散列由认证库负责。散列不是可解密的密码副本;登录时由库验证新输入是否匹配已保存的凭据。不要把散列值返回给浏览器或拿它当展示数据。

当前 schema.prisma 有这些模型:

模型 / 数据表 含义 本阶段关注点
User / user 用户身份资料 用户 ID、邮箱、姓名
Account / account 账号认证凭据与提供方关联 密码散列所在的凭据记录
Session / session 已建立的登录会话 用户、令牌、过期时间
Verification / verification 认证库验证流程使用的记录结构 有表不代表已开启邮箱验证功能

一个用户可以拥有多个会话,例如两个浏览器分别登录。这里讨论的是当前会话退出,不应推导为“点一次退出就撤销所有设备的登录”。

登录成功后,服务通过响应的 Set-Cookie 建立浏览器会话凭证。后续同源请求,浏览器按 Cookie 规则携带它。

text
登录时:邮箱密码 → /api/auth → 验证凭据 → 保存会话 → Set-Cookie
之后:业务请求 + Cookie → 服务端验证会话 → 得到 userId → 检查业务权限

auth-client.ts 使用同源认证客户端;业务 tRPC 也访问相同站点。当前实现不要求你把登录令牌复制到 localStorage,再为每个请求手写 Authorization。

会话 Cookie 设置了 HttpOnly,页面 JavaScript 不能直接通过 document.cookie 读取其内容,但浏览器仍可按规则发送。HttpOnly 的含义不是“浏览器再也不会发出危险请求”,也不替代业务授权。

cookieCache.enabled: false 表示不依靠认证库的 Cookie 会话缓存跳过这条服务端验证路径;业务入口仍调用认证库读取有效会话。它不表示浏览器没有 Cookie,也不表示 TanStack Query 的页面缓存被关闭。

拓展阅读Session 与 JWT 应该怎么选?

本项目使用服务端会话记录,可以通过服务端状态使当前会话失效。JWT 是一种令牌格式,采用它也仍要设计过期、撤销和权限变更策略。不要把 Cookie 与 JWT 当成同一层的替代品:Cookie 是浏览器存取与发送数据的机制,JWT 是数据表示形式。本阶段先读懂现有方案,不需要改造认证架构。

6. tRPC 如何拿到可信身份#

在 业务 route.ts 找到:

ts
// 源码节选:每次进入请求上下文时验证会话
const session = await getAuth().api.getSession({ headers: req.headers });
return { db: getDb(), userId: session?.user.id ?? null };

这里的用户 ID 来自认证库对请求凭证的验证结果,不是从 req.body.userId 取来的。

随后 protectedProcedure 拒绝 ctx.userId 为空的业务请求,返回 UNAUTHORIZED。验证通过只意味着有身份;团队成员关系与角色要在服务层继续检查。

认证入口和业务入口都需要保护。当前业务 route 对带 Origin 的 POST 还检查它是否等于 BETTER_AUTH_URL;不一致直接返回 HTTP 403,尚未进入 tRPC 错误格式化。

不要把这段条件夸大成“所有请求必须有 Origin”:源码对没有该头的请求不会在这一行拒绝,后续仍须验证会话与业务权限。来源检查用于限制跨站请求,不能代替身份检查。

7. 本章验证与练习#

在自己的本地学习环境注册测试账号,密码至少 10 字符;正常退出,再用错误密码和正确密码分别登录。在 Network 中观察认证请求与 Set-Cookie,只确认存在和属性,不复制或传播真实令牌。

认证集成测试 已覆盖密码记录不同于明文、错误密码被拒绝、Cookie 含 HttpOnly、会话可读取、退出后旧会话失效。它验证了这些行为,不是对全部认证安全性的证明。如何执行与补充验收见第 12 章。

练习:如果把 userId 加到创建工单请求里,能否替别人创建?

动手练习查看参考答案

参考答案:不能据此获得别人的身份。当前 router 使用服务端 ctx.userId 创建服务,创建者来自这个上下文;客户端额外传来的身份字段不应参与身份判定。关键是服务端从会话建立身份,而非 UUID 是否真实存在。

下一章把“这个人是谁”与“这个人在团队中是什么角色”连接起来。

把知识变成自己的理解

完成本章练习,再继续下一站。

TEAMDESK HANDBOOK · 从一张工单,走进全栈开发

搜索教程

输入关键词,搜索全部章节。

阅读设置

外观
正文字号

从一张工单出发,读懂数据如何流转。

设置与进度仅保存在当前浏览器。