2、Token认证机制:JWT结构解析、Token签发与验证、Access Token与Refresh Token

Token 认证,说白了就是服务器给你发一张「数字通行证」。你带着它来,我就不用每次都查你密码了。JWT(JSON Web Token)是目前最主流的实现方式,我几乎在每个项目里都用它。

2.1 JWT 结构:三段式,一眼看懂

JWT 长什么样?我给你看个例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

看到那两个点了吗?JWT 就是由三部分组成的:Header、Payload、Signature。每部分都是 Base64URL 编码,然后用点连起来。

部分 内容 作用
Header 算法类型(如 HS256)和 Token 类型 告诉服务器怎么验签
Payload 用户 ID、角色、过期时间等声明 携带用户身份信息
Signature 对前两部分的签名 防止数据被篡改

嗯,这里要注意:Payload 是 Base64 编码,不是加密。谁都能解码看到里面的内容。所以千万别把密码、信用卡号这类敏感信息放进去。我在项目中见过有人把用户手机号明文放进去,结果前端一解码全暴露了——这其实是个低级错误。

2.2 Token 签发:服务端说了算

签发 Token 的过程,其实就是服务端生成一个带签名的 JSON 对象。我习惯用 Node.js 的 jsonwebtoken 库,代码很简洁:

const jwt = require('jsonwebtoken');

const payload = {
  sub: user.id,
  role: user.role,
  iat: Math.floor(Date.now() / 1000)
};

const secret = process.env.JWT_SECRET;

const token = jwt.sign(payload, secret, { expiresIn: '15m' });

这里有几个关键点:

  • secret:签名密钥,必须放在服务端环境变量里,绝对不能硬编码
  • expiresIn:过期时间,Access Token 一般设 15-30 分钟
  • iat:签发时间,用于计算 Token 是否过期
⚠️ 我曾经踩过的坑:有一次我把 JWT_SECRET 写死在代码里,还提交到了 Git 仓库。结果代码泄露后,攻击者用这个密钥伪造了管理员 Token。从那以后,我强制团队用环境变量 + 密钥轮换策略,每个月换一次密钥。

2.3 Token 验证:每次请求都要过一遍

验证 Token 是服务端中间件的核心工作。流程其实很简单:

  1. 从请求头中提取 Authorization: Bearer <token>
  2. 用同样的密钥和算法验证签名
  3. 检查过期时间
  4. 解析 Payload 获取用户信息

代码示例:

const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供 Token' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token 已过期' });
    }
    return res.status(403).json({ error: 'Token 无效' });
  }
};

你想想看,如果每次请求都去查数据库验证用户身份,那性能得多差?JWT 验证是纯计算操作,毫秒级完成,这才是它最大的优势。

2.4 Access Token vs Refresh Token:双 Token 策略

为什么需要两个 Token?原因很简单:安全性和用户体验的平衡。

Access Token 有效期短(15 分钟),即使泄露了,攻击者也只能用一小会儿。但用户不可能每 15 分钟就重新登录一次,太烦人了。所以就有了 Refresh Token。

特性 Access Token Refresh Token
有效期 短(15-30 分钟) 长(7-30 天)
存储位置 内存 / 短期缓存 HttpOnly Cookie / 安全存储
传输方式 请求头 Bearer 仅刷新接口使用
泄露风险 低(过期快) 高(需额外保护)

工作流程是这样的:

  • 登录时,服务端同时签发 Access Token 和 Refresh Token
  • 客户端用 Access Token 访问资源
  • Access Token 过期后,客户端用 Refresh Token 换取新的 Access Token
  • Refresh Token 过期后,用户需要重新登录
💡 我个人习惯:Refresh Token 一定要绑定设备信息(如 User-Agent、IP 段)。如果检测到 Refresh Token 在陌生设备上使用,立即作废所有 Token,强制用户重新登录。这能有效防止 Token 被盗用后的横向扩散。

2.5 核心流程:一张图说清楚

下面这张图展示了双 Token 认证的完整流程。我建议你仔细看看,尤其是「刷新」那条路径——很多新手在这里翻车。

双 Token 认证流程 双 Token 认证流程 客户端 登录接口 资源接口 刷新接口 用户数据库 1. 用户名+密码 2. 验证身份 3. 身份有效 4. 返回双 Token 5. 携带 Access Token 6. 返回资源数据 Access Token 过期 7. 携带 Refresh Token 8. 返回新 Access Token 💡 关键点:Refresh Token 只在刷新接口使用,不参与常规请求。 如果 Refresh Token 也过期了,用户必须重新登录。

2.6 避坑指南:我踩过的那些雷

做了这么多年认证系统,有些坑我记忆犹新:

  • Token 不设过期时间:我曾经接手过一个项目,Access Token 有效期设了 30 天。结果一个员工离职后,他的 Token 还能用——这等于把公司大门钥匙给了外人。
  • Refresh Token 不做轮换:每次刷新时,应该同时签发新的 Refresh Token,旧的立即作废。否则一个 Refresh Token 泄露,攻击者能一直用。
  • 签名算法用错:HS256 是对称算法,RS256 是非对称。如果多个服务需要验证 Token,用 RS256 更安全——私钥只有一个,公钥可以公开。
  • Payload 塞太多数据:JWT 每次请求都要传输,Payload 越大,网络开销越大。我见过有人把整个用户对象塞进去,一个 Token 好几 KB——完全没必要。

核心原则:Access Token 只存「谁」和「有什么权限」,不存「详细信息」。用户详情需要时再查数据库,或者用缓存。