核心概念
1. 密码绝不能明文入库
客户端明文 password
→ bcrypt.hash(password, 10) // 注册时
→ 数据库存 $2b$10$... 哈希
登录时:
→ bcrypt.compare(明文, 库中哈希) // 不 decrypt,只比对
2. JWT 流程
登录成功
→ jwt.sign({ userId, email }, JWT_SECRET, { expiresIn: "7d" })
→ 返回 token 给客户端
后续请求
→ Header: Authorization: Bearer <token>
→ jwt.verify(token, JWT_SECRET)
→ 得到 userId,挂到 req.userId
JWT payload 放什么 :userId(必须)、email(可选)。绝不放 password。
JWT_SECRET :放 .env,不写死在代码里,不提交 Git。
3. 不信任客户端传的 userId
- 改前 :
POST /api/todosbody 里传userId→ 可伪造他人身份 - 改后 :
userId只从 token 解析的req.userId取
4. HTTP 状态码
状态码分三大类,客户端据此决定下一步动作(重试、跳转登录、展示错误提示等):
| 类别 | 范围 | 含义 |
|---|---|---|
| 2xx | 成功 | 请求被正常处理 |
| 4xx | 客户端 | 请求有问题(参数、权限等) |
| 5xx | 服务端 | 服务器内部出错,通常不是用户能修的 |
常见的状态码
| 状态码 | 含义 | 典型场景(本项目) |
|---|---|---|
| 200 | OK | GET 列表/单条、PUT 更新、登录成功(Express 默认,可省略 .status(200)) |
| 201 | Created | POST /api/auth/register、POST /api/todos 创建成功 |
| 204 | No Content | DELETE /api/todos/:id 删除成功(无响应体) |
| 400 | Bad Request | 缺少/格式错误的 body、密码太短、id 不是数字 |
| 401 | Unauthorized | 未带 token、token 无效/过期、邮箱或密码错误 |
| 403 | Forbidden | 已登录,但 Todo 不属于当前 userId(越权) |
| 404 | Not Found | Todo 不存在;或未匹配到任何路由(兜底中间件) |
| 409 | Conflict | 注册时邮箱已被占用(Prisma P2002 唯一约束冲突) |
| 500 | Internal Error | 未捕获异常,next(error) 进入全局错误处理中间件 |
用法原则
1. 成功时:按「有没有新建/删除资源」选 200 / 201 / 204
GET /api/todos → 200 + JSON 数组
POST /api/todos → 201 + 新建的 todo
PUT /api/todos/:id → 200 + 更新后的 todo
DELETE /api/todos/:id → 204,body 为空(res.status(204).send())
POST /api/auth/login → 200 + { token, user }(登录不算「创建资源」,用 200 即可)
2. 失败时:先区分「谁的错」
请求格式/参数不对 → 400(客户端改请求)
身份未证明/证明无效 → 401(客户端去登录或换 token)
身份有效但无权操作 → 403(客户端别碰这条资源)
资源本身不存在 → 404(客户端换 id 或放弃)
资源冲突(重复注册) → 409
服务端代码/DB 异常 → 500(客户端可提示「稍后重试」)
3. 401 vs 403(面试常问)
| 状态码 | 问的是 | 本项目中何时返回 |
|---|---|---|
| 401 | 「你是谁?」 | 无 Bearer、token 过期、邮箱密码错误 |
| 403 | 「你不能动这个」 | Todo 存在但不属于当前 userId |
4. 404 与 403 的判断顺序(重要)
单条资源接口(GET/PUT/DELETE /:id)必须先判 404(不存在) ,再判 403(越权):
ts
if (!existing) return res.status(404).json({ error: "Todo not found" });
if (existing.userId !== req.userId)
return res.status(403).json({ error: "Forbidden" });
顺序反了(先写 existing?.userId !== req.userId)会把「不存在的 id」误报成 403,还会泄露「这条 id 是否存在」的信息。
5. 响应体约定
- 4xx / 5xx:统一
{ error: "可读说明" },便于前端展示 - 401 登录失败:故意用
"Invalid email or password",不区分「邮箱不存在」和「密码错」,避免枚举用户 - 204:不要带 JSON body
- 500:生产环境勿把堆栈返回给客户端
6. Express 里怎么写
ts
return res.status(400).json({ error: "Title is required" }); // 早返回,带状态码
res.status(201).json(newTodo); // 创建成功
res.json(todos); // 省略 status,默认 200
res.status(204).send(); // 无内容
next(error); // 交给全局中间件 → 500
踩过的坑
- migration 加 NOT NULL 字段:表里有旧 User 数据时会失败 → 先清关联表数据或分步 migrate
- 404 / 403 顺序 :
existing?.userId !== req.userId写在!existing前面 → 不存在也返回 403 - DELETE 漏所有权校验:只查存在就删 → 任意登录用户可删别人的 Todo
- middleware catch :
jwt.verify失败应返回 401,不要next(error)进 500 - 响应泄露 password :
prisma.user.create用select排除 password;登录返回user对象不含 password