练习题与复盘

这些练习围绕工作中高频出现的认证需求和故障。建议先独立完成,再回看前面的教程。

使用方式

每道题至少写出:

  1. 你的设计或代码。
  2. 为什么这样做。
  3. 失败时返回什么状态码。
  4. 存在哪些安全边界。
  5. 如何测试成功和失败路径。

练习 1:区分四个概念

分别说明 Base64URL、SHA-256、Argon2id、HS256 的目的,并回答:

验收重点:不要把编码、加密、哈希和签名混为一谈。

练习 2:实现注册接口

实现 POST /register

进一步思考:两个相同用户名同时注册,为什么“先查询再写入”仍不够?MySQL 应增加什么约束?

练习 3:实现登录接口

实现 POST /login

测试:正确密码、错误密码、不存在账号、异常哈希字符串。

练习 4:升级旧密码哈希

在密码验证成功后调用 argon2.needsRehash()

思考:为什么升级时不需要用户再次提供密码?

练习 5:实验 decode()verify()

  1. 使用 jwt.sign() 生成 Token。
  2. 使用 jwt.decode() 读取 Payload。
  3. 手动修改 Payload 中的角色并重新进行 Base64URL 编码。
  4. 分别调用 decode()verify()

记录结果并解释为什么 decode() 不能用于身份认证。

练习 6:expiresIn 单位陷阱

分别签发:

{ expiresIn: 120 }
{ expiresIn: "120" }
{ expiresIn: "120s" }
{ expiresIn: "2m" }

使用 decode() 仅用于实验查看 iatexp,计算每种 Token 的有效期。解释无单位字符串的风险。

练习 7:Bearer Token 解析器

编写 readBearerToken() 并测试:

Authorization: Bearer abc
Authorization: bearer abc
Authorization: Bearer
Authorization: Basic abc
Authorization: Bearer abc extra
Authorization:    Bearer    abc

定义每个输入应该返回 Token 还是 null

练习 8:认证中间件

实现中间件并满足:

禁止使用 anyjwt.decode() 进行认证。

练习 9:认证与授权

实现:

分别设计未登录、Token 无效、普通用户访问管理员接口时的状态码,并解释 401403

练习 10:Refresh Token Rotation

实现 /token/refresh

测试连续使用旧 Token 两次时,第二次必须失败。

练习 11:处理并发刷新

假设同一个 Refresh Token 同时发出两个请求:

这是前后端分离系统中非常高频的问题。

练习 12:单设备和全部设备退出

实现:

回答:为什么退出后旧 Access Token 可能仍可使用?怎样实现即时失效,代价是什么?

练习 13:修改密码后的会话策略

设计 POST /change-password

练习 14:排查“总是返回 401”

列出并逐项排查:

要求日志不能打印完整 Token。

练习 15:CommonJS 与导入问题

结合当前 tsconfig.json 回答:

练习 16:设计数据库表

usersauth_sessions 编写 MySQL DDL,至少包含:

解释 password_hashrefresh_token_hash 的长度选择。

练习 17:安全评审

评审下面的代码,找出尽可能多的问题:

app.post("/login", async (req, res) => {
  const user = users.find((item) => item.username === req.body.username);

  if (!user) {
    return res.status(404).json({ message: "账号不存在" });
  }

  if (user.password !== req.body.password) {
    return res.status(401).json({ message: "密码错误" });
  }

  const token = jwt.sign(
    {
      ...user,
      password: req.body.password,
    },
    "123456"
  );

  console.log("login", req.body, token);
  res.json({ token });
});

至少从密码存储、信息泄露、JWT Payload、Secret、过期时间、日志、输入校验、账号枚举和限流角度分析。

练习 18:生产化清单

把内存案例升级为 MySQL 版本,并补充:

不要一次引入复杂认证框架。先让每个安全决策都能被自己解释,再考虑抽象。

复盘问题

完成后,尝试不看文档回答:

  1. argon2.verify()jwt.verify() 同名,但分别在验证什么?
  2. 为什么 JWT 签名不是加密?
  3. 为什么 Access Token 应短期,而 Refresh Token 需要服务端状态?
  4. 为什么高熵 Refresh Token 可以使用 SHA-256 摘要,而密码不可以?
  5. 为什么退出登录后 Access Token 不一定立即失效?
  6. 为什么 verify() 成功后仍要检查 Payload 字段类型?
  7. 为什么生产刷新逻辑必须考虑并发?