09 - 鉴权与权限及状态码

核心概念

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/todos body 里传 userId → 可伪造他人身份
  • 改后userId 只从 token 解析的 req.userId

4. HTTP 状态码

状态码分三大类,客户端据此决定下一步动作(重试、跳转登录、展示错误提示等):

类别 范围 含义
2xx 成功 请求被正常处理
4xx 客户端 请求有问题(参数、权限等)
5xx 服务端 服务器内部出错,通常不是用户能修的
常见的状态码
状态码 含义 典型场景(本项目)
200 OK GET 列表/单条、PUT 更新、登录成功(Express 默认,可省略 .status(200)
201 Created POST /api/auth/registerPOST /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

踩过的坑

  1. migration 加 NOT NULL 字段:表里有旧 User 数据时会失败 → 先清关联表数据或分步 migrate
  2. 404 / 403 顺序existing?.userId !== req.userId 写在 !existing 前面 → 不存在也返回 403
  3. DELETE 漏所有权校验:只查存在就删 → 任意登录用户可删别人的 Todo
  4. middleware catchjwt.verify 失败应返回 401,不要 next(error) 进 500
  5. 响应泄露 passwordprisma.user.createselect 排除 password;登录返回 user 对象不含 password