JWT 从入门到实战:一张“网络身份证”的奇妙旅行

0 阅读8分钟

今天我们要聊的 JWT(JSON Web Token),就是给你的 API 装上一把智能锁,并且配上一张“网络身份证”。学完这篇,你就能轻松实现登录认证、接口保护这些真实项目必备的功能。


一、🤔 JWT 到底是什么?先看“长相”

在开始之前,我们先来认识一下 JWT 的“长相”。一个典型的 JWT 长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

是不是看着像一串乱码?别慌,它其实是有规律的——由两个点号(.)分成三段

  • 第一部分:头部(Header)
  • 第二部分:载荷(Payload)
  • 第三部分:签名(Signature)

你可以把 JWT 想象成一张智能门禁卡

  • 卡上印着你的名字和照片(载荷)
  • 有防伪水印,一撕就坏(签名)
  • 卡上有有效期,过期自动失效(过期时间)

二、🎯 为什么我们需要 JWT?

在 JWT 出现之前,传统的 Session 认证方式是:

  1. 用户登录成功,服务器在内存/数据库里存一份 Session 数据
  2. 服务器返回一个 Session ID 给客户端(存在 Cookie 里)
  3. 下次请求客户端带上这个 ID,服务器去查 Session 数据

这样做的痛点很明显 :

  • 服务器有状态:如果部署多台服务器,Session 数据必须共享(不然用户可能在A服务器登录了,请求打到B服务器就不认识了)
  • 扩展性差:用户量一大,Session 存储就成了瓶颈
  • 不适合移动端:App 里处理 Cookie 比较麻烦

JWT 的方案完全不同 :

  • 服务器不存储任何 session 数据
  • 用户信息直接编码在 JWT 里,服务器只要验证签名就知道你是谁
  • 服务器可以轻松横向扩展——任何一台服务器只要拿到公钥/密钥,都能验证 JWT

这就是所谓的 “无状态认证”——服务器不需要记住你,你每次来都自己带着身份证。


三、🧩 JWT 的三段式结构(简单版)

3.1 头部(Header)

头部是个 JSON 对象,通常长这样 :

{
  "alg": "HS256",  // 签名算法,常见的有 HS256、RS256
  "typ": "JWT"      // 令牌类型
}

3.2 载荷(Payload)

载荷是 JWT 的主体,里面放着你想要传递的信息 :

{
  "sub": "1234567890",    // 标准声明:用户ID
  "name": "张三",          // 自定义声明:用户名
  "role": "admin",        // 自定义声明:角色
  "iat": 1516239022,      // 签发时间
  "exp": 1516242622       // 过期时间
}

⚠️ 重要提醒:载荷只是 Base64Url 编码,不是加密!任何人都可以解码看到里面的内容 。所以绝对不要在载荷里放密码、密钥等敏感信息。

3.3 签名(Signature)

签名是 JWT 的防伪标识,它的生成公式是 :

签名 = 算法( Base64(Header) + "." + Base64(Payload), 密钥 )

验证方收到 JWT 后,会用同样的方式重新计算签名,如果结果一致,说明令牌没有被篡改过 。


四、🚀 JWT 的完整工作流程

咱们用一个真实的场景来理解 JWT 是怎么“旅行”的 :

第1步:用户登录(获取令牌)

sequenceDiagram
    participant 客户端
    participant 认证服务器
    客户端->>认证服务器: POST /login (用户名+密码)
    认证服务器->>认证服务器: 验证用户身份
    认证服务器->>客户端: 返回 JWT 令牌

第2步:客户端存储令牌

客户端收到 JWT 后,通常存储在:

  • Web 端:localStorage 或内存中
  • App 端:安全存储区

第3步:访问受保护资源

sequenceDiagram
    participant 客户端
    participant 资源服务器
    客户端->>资源服务器: GET /api/users (Header: Authorization: Bearer <JWT>)
    资源服务器->>资源服务器: 验证签名、检查过期时间
    资源服务器->>客户端: 返回用户数据

注意这里的 Header 格式是固定的 :

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

五、💻 实战:用 Node.js 实现 JWT 认证

理论说了这么多,咱们直接上手写代码!这里我们用最常见的 jsonwebtoken 库 。

5.1 安装依赖

npm install jsonwebtoken
# TypeScript 用户还可以装类型定义
npm install @types/jsonwebtoken -D

5.2 生成令牌(登录接口)

const jwt = require('jsonwebtoken');

// 登录接口
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // 1. 验证用户名密码(实际项目会查数据库)
  if (username === 'admin' && password === '123456') {
    
    // 2. 定义要放在 JWT 里的用户信息
    const payload = { 
      userId: 1001,
      username: 'admin',
      role: 'admin'
    };
    
    // 3. 生成 JWT(设置1小时过期)
    const secretKey = 'your-secret-key';  // 实际应该从环境变量读取
    const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
    
    // 4. 返回令牌
    res.json({ 
      success: true,
      token: token,
      expiresIn: 3600 
    });
  } else {
    res.status(401).json({ success: false, message: '用户名或密码错误' });
  }
});

5.3 验证令牌(中间件)

function authenticateToken(req, res, next) {
  // 1. 从 Header 里取出 token
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];  // Bearer TOKEN
  
  if (!token) {
    return res.status(401).json({ message: '未提供认证令牌' });
  }
  
  // 2. 验证 token
  const secretKey = 'your-secret-key';
  jwt.verify(token, secretKey, (err, user) => {
    if (err) {
      // 判断具体错误类型
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ message: '令牌已过期' });
      }
      return res.status(403).json({ message: '无效令牌' });
    }
    
    // 3. 把用户信息挂到请求对象上,后续中间件可以用
    req.user = user;
    next();
  });
}

5.4 保护接口

// 公开接口,不需要认证
app.get('/public', (req, res) => {
  res.json({ message: '这是公开数据' });
});

// 受保护接口,需要 JWT
app.get('/api/users', authenticateToken, (req, res) => {
  // 这里可以拿到上面挂载的 req.user
  console.log('当前用户:', req.user);
  
  res.json({ 
    message: '这是受保护的用户数据',
    user: req.user 
  });
});

5.5 刷新令牌机制

实际项目中,access token 过期时间通常很短(比如15分钟),同时会发放一个 refresh token(刷新令牌),用来在 access token 过期后获取新的 access token 。

// 生成 access token 和 refresh token
function generateTokens(userId) {
  const accessToken = jwt.sign(
    { userId, type: 'access' },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { userId, type: 'refresh' },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
  
  return { accessToken, refreshToken };
}

// 刷新 token 接口
app.post('/refresh-token', (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ message: '请提供 refresh token' });
  }
  
  // 验证 refresh token
  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ message: '无效的 refresh token' });
    }
    
    // 生成新的 access token
    const newAccessToken = jwt.sign(
      { userId: user.userId, type: 'access' },
      process.env.ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' }
    );
    
    res.json({ accessToken: newAccessToken });
  });
});

六、🔒 安全使用 JWT 的六大黄金法则

JWT 虽然好用,但用不好就会变成“豆腐渣工程”。下面这些安全建议来自 IETF 的官方最佳实践 :

⚠️ 法则1:严禁在 Payload 里存敏感信息

记住:Payload 是明文编码,不是加密!任何人都可以轻松解码 。

// ❌ 错误示范
const payload = { 
  userId: 1001,
  password: '123456',  // 千万别!
  creditCard: '4111111111111111' // 更不行!
};

// ✅ 正确做法:只放非敏感信息
const payload = {
  userId: 1001,
  role: 'user',
  sessionId: 'xxx'  // 如果需要敏感数据,存个 ID 让服务器去查
};

⚠️ 法则2:强制算法白名单,防“算法混淆攻击”

经典攻击手法:把 RS256(非对称)改成 HS256(对称),然后用公钥作为密钥验证——直接绕过签名验证!

// ✅ 安全做法:明确指定允许的算法
jwt.verify(token, secretOrPublicKey, { 
  algorithms: ['RS256', 'ES256']  // 只允许这些算法
});

⚠️ 法则3:必须验证所有标准声明

不要只看签名!exp(过期时间)、nbf(生效时间)、aud(受众)、iss(签发者)这些都要验证 。

jwt.verify(token, secretKey, {
  algorithms: ['HS256'],
  audience: 'my-api',        // 这个 token 是给我的吗?
  issuer: 'auth-server',     // 是可信的签发者发的吗?
  maxAge: '1h'               // 最多能接受多老的 token?
});

⚠️ 法则4:选择正确的算法(HS256 vs RS256)

  • HS256(对称加密):同一个密钥签名和验证
    • 优点:性能好,实现简单
    • 缺点:验证方也要知道密钥,不适合分布式系统
  • RS256(非对称加密):私钥签名,公钥验证
    • 优点:验证方只需要公钥,更安全;可以集中管理密钥
    • 推荐用于微服务/分布式架构

IETF 推荐算法:RS256 和 ES256 是首选 。

⚠️ 法则5:定期轮转密钥

长期使用同一密钥会增加泄露风险。生产环境应该支持 密钥轮转

// 通过 JWKS (JSON Web Key Set) 端点动态获取公钥
// 网关每5分钟拉取一次,新旧公钥并存,无缝轮转
const jwksUri = 'https://auth.example.com/.well-known/jwks.json';

⚠️ 法则6:设置短生命周期的令牌

  • Access token:15分钟到2小时
  • Refresh token:7天到30天,且应该可以撤销
// 敏感操作还要额外验证
if (req.user.role !== 'admin' && req.method !== 'GET') {
  return res.status(403).json({ message: '需要管理员权限' });
}

七、🎯 常见场景速查表

场景建议
单页应用(SPA)JWT 存内存,刷新 token 存 httpOnly cookie
移动 AppJWT 存安全存储区,用 refresh token 轮换
微服务用 RS256,所有服务共享公钥,独立验证
第三方 API遵循 RFC 9068 规范,JWT 格式的 Access Token
单点登录(SSO)用 JWT 传递用户信息,各子系统独立验证

八、📝 总结:一张“智能身份证”的自白

回顾一下 JWT 的“奇妙旅行”:

  1. 我是谁:一个三段式字符串,包含头部、载荷、签名
  2. 我干什么:让服务器不再存储 session,实现无状态认证
  3. 我怎么工作:登录时颁发,后续请求放在 Authorization Header 里
  4. 怎么用我安全:别放敏感信息,算法白名单,验证所有声明,定期轮转密钥

JWT 不是万能的,但在绝大多数 API 认证场景中,它都是简单又强大的选择。等你真正动手写几个接口,把 JWT 加进去保护起来,就会感受到那种“锁上门、发钥匙”的掌控感。