← 返回博客

JSON Web Tokens(JWT):身份验证指南

JWT 身份验证的完整指南。学习 JWT 结构、实现、安全最佳实践和令牌刷新策略。

Big JSON Team14 分钟阅读advanced
B

Big JSON Team

Technical Writer

Expert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.

14 分钟阅读

# JSON Web Tokens(JWT):身份验证指南

JWT(JSON Web Token)是一种紧凑、URL 安全的令牌格式,用于在各方之间安全地传输信息。它是现代 Web 应用中最流行的身份验证方式。

什么是 JWT?

JWT 是一个标准化的令牌格式(RFC 7519),用于:

  • 身份验证 - 验证用户身份
  • 授权 - 传递权限信息
  • 信息交换 - 安全地传输声明
  • 无状态 - 服务器无需存储会话

JWT 与传统会话

| 特性 | 会话 | JWT |

|------|------|-----|

| 存储 | 服务器 | 客户端 |

| 状态 | 有状态 | 无状态 |

| 扩展性 | 困难 | 容易 |

| 移动 | 需要 Cookie | 适合 API |

| 跨域 | 困难 | 简单 |

JWT 结构

JWT 由三部分组成,用点(.)分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

[Header].[Payload].[Signature]

1. 标头(Header)

标头声明令牌的类型和签名算法。

{

"alg": "HS256",

"typ": "JWT"

}

然后使用 Base64 编码:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
常见算法:

| 算法 | 说明 | 安全性 | 用途 |

|------|------|--------|------|

| HS256 | HMAC SHA-256 | 中等 | 简单应用 |

| HS512 | HMAC SHA-512 | 高 | 敏感数据 |

| RS256 | RSA SHA-256 | 高 | 公钥基础 |

| ES256 | ECDSA SHA-256 | 高 | 现代应用 |

2. 有效负载(Payload)

有效负载包含声明(claims)。

{

"sub": "1234567890",

"name": "张三",

"email": "zhang@example.com",

"iat": 1516239022,

"exp": 1516242622

}

Base64 编码:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuS6pOWTpSIsImVtYWlsIjoiemhhbmdAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjI0MjYyMn0
标准声明:

| 声明 | 说明 | 示例 |

|------|------|------|

| sub | 主题(用户 ID) | "user_123" |

| iss | 发行者 | "https://example.com" |

| aud | 受众 | "app_name" |

| exp | 过期时间(Unix 时间戳) | 1516242622 |

| iat | 发行时间 | 1516239022 |

| nbf | 生效前 | 1516239022 |

| jti | JWT ID | "unique-id" |

自定义声明:
{

"sub": "user_123",

"name": "张三",

"role": "admin",

"permissions": ["read", "write", "delete"],

"department": "工程部"

}

3. 签名(Signature)

签名使用标头中指定的算法对标头和有效负载进行签名。

签名过程:
SIGNATURE = HMACSHA256(

BASE64(HEADER) + "." + BASE64(PAYLOAD),

SECRET

)

目的:
  • 验证 - 确保令牌未被篡改
  • 真实性 - 证明令牌来自可信来源
  • 不可否认 - 发行者无法否认发行

JWT 工作流程

身份验证流程

1. 用户登录

输入:用户名和密码

  • 服务器验证凭证
  • 生成 JWT 令牌
  • 返回令牌给客户端
  • 响应:{ "token": "eyJ..." }

  • 客户端存储令牌
  • 存储位置:localStorage、sessionStorage 或 Cookie

  • 未来请求中包含令牌
  • 标头:Authorization: Bearer eyJ...

  • 服务器验证令牌
  • 检查签名和过期时间

  • 访问被允许
  • 流程图

    ┌─────────────┐                      ┌─────────────┐
    

    │ 客户端 │ │ 服务器 │

    └─────────────┘ └─────────────┘

    │ │

    │ 1. 发送凭证 (用户名/密码) │

    ├──────────────────────────────────>│

    │ │

    │ 2. 验证凭证 │

    │ ├─┐

    │ │ │

    │ 3. 生成 JWT │<┘

    │ │

    │ 4. 返回 JWT 令牌 │

    │<──────────────────────────────────┤

    │ │

    │ 5. 存储令牌 │

    ├─┐ │

    │ │ │

    │<┘ │

    │ │

    │ 6. 请求 + JWT (Authorization 标头)

    ├──────────────────────────────────>│

    │ │

    │ 7. 验证令牌 │

    │ ├─┐

    │ │ │

    │ │<┘

    │ 8. 返回受保护资源 │

    │<──────────────────────────────────┤

    │ │

    Node.js 实现

    使用 jsonwebtoken 库

    安装:
    npm install jsonwebtoken dotenv express
    基本设置:
    import jwt from 'jsonwebtoken';
    

    import express from 'express';

    const app = express();

    const SECRET = process.env.JWT_SECRET || 'your-secret-key';

    // 生成令牌

    function generateToken(user) {

    const payload = {

    id: user.id,

    email: user.email,

    role: user.role

    };

    return jwt.sign(payload, SECRET, {

    expiresIn: '1h', // 1 小时后过期

    algorithm: 'HS256'

    });

    }

    // 验证令牌

    function verifyToken(token) {

    try {

    const decoded = jwt.verify(token, SECRET);

    return { valid: true, data: decoded };

    } catch (error) {

    return { valid: false, error: error.message };

    }

    }

    // 中间件:保护路由

    function protectRoute(req, res, next) {

    const authHeader = req.headers.authorization;

    const token = authHeader?.split(' ')[1]; // "Bearer TOKEN"

    if (!token) {

    return res.status(401).json({ error: '缺少令牌' });

    }

    const result = verifyToken(token);

    if (!result.valid) {

    return res.status(401).json({ error: '无效令牌', details: result.error });

    }

    req.user = result.data;

    next();

    }

    // 登录路由

    app.post('/login', (req, res) => {

    const user = { id: 1, email: 'user@example.com', role: 'user' };

    const token = generateToken(user);

    res.json({ token });

    });

    // 受保护的路由

    app.get('/profile', protectRoute, (req, res) => {

    res.json({ user: req.user });

    });

    app.listen(3000);

    令牌刷新

    // 生成访问令牌和刷新令牌
    

    function generateTokens(user) {

    const accessToken = jwt.sign(user, SECRET, {

    expiresIn: '15m' // 短期

    });

    const refreshToken = jwt.sign(user, REFRESH_SECRET, {

    expiresIn: '7d' // 长期

    });

    return { accessToken, refreshToken };

    }

    // 刷新路由

    app.post('/refresh', (req, res) => {

    const { refreshToken } = req.body;

    try {

    const user = jwt.verify(refreshToken, REFRESH_SECRET);

    const accessToken = jwt.sign(user, SECRET, { expiresIn: '15m' });

    res.json({ accessToken });

    } catch (error) {

    res.status(401).json({ error: '刷新令牌无效' });

    }

    });

    Python 实现

    安装:
    pip install PyJWT
    实现:
    import jwt
    

    from datetime import datetime, timedelta

    import os

    SECRET = os.environ.get('JWT_SECRET', 'your-secret-key')

    def generate_token(user):

    payload = {

    'id': user['id'],

    'email': user['email'],

    'iat': datetime.utcnow(),

    'exp': datetime.utcnow() + timedelta(hours=1)

    }

    return jwt.encode(payload, SECRET, algorithm='HS256')

    def verify_token(token):

    try:

    decoded = jwt.decode(token, SECRET, algorithms=['HS256'])

    return {'valid': True, 'data': decoded}

    except jwt.ExpiredSignatureError:

    return {'valid': False, 'error': '令牌已过期'}

    except jwt.InvalidTokenError:

    return {'valid': False, 'error': '无效令牌'}

    # Flask 中使用

    from flask import Flask, request, jsonify

    app = Flask(__name__)

    @app.route('/login', methods=['POST'])

    def login():

    user = {'id': 1, 'email': 'user@example.com'}

    token = generate_token(user)

    return jsonify({'token': token})

    @app.route('/profile', methods=['GET'])

    def profile():

    auth_header = request.headers.get('Authorization')

    if not auth_header:

    return jsonify({'error': '缺少授权头'}), 401

    token = auth_header.split(' ')[1]

    result = verify_token(token)

    if not result['valid']:

    return jsonify({'error': result['error']}), 401

    return jsonify({'user': result['data']})

    if __name__ == '__main__':

    app.run(debug=True)

    安全最佳实践

    1. 使用 HTTPS

    // ✓ 正确:HTTPS 上的令牌
    

    app.use((req, res, next) => {

    if (!req.secure && process.env.NODE_ENV === 'production') {

    return res.redirect('https://' + req.headers.host + req.url);

    }

    next();

    });

    2. 保护密钥

    # .env 文件(不要提交到 git)
    

    JWT_SECRET=your-super-secret-key-minimum-32-characters-long

    REFRESH_SECRET=another-secret-key-for-refresh-tokens

    # .gitignore

    .env

    .env.local

    3. 短过期时间

    // 访问令牌:15 分钟
    

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

    // 刷新令牌:7 天

    const refreshToken = jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' });

    4. 验证声明

    function verifyToken(token, expectedAudience) {
    

    try {

    const decoded = jwt.verify(token, SECRET);

    // 检查必要的声明

    if (decoded.aud !== expectedAudience) {

    throw new Error('受众不匹配');

    }

    if (!decoded.sub) {

    throw new Error('缺少主题');

    }

    return decoded;

    } catch (error) {

    return null;

    }

    }

    5. 令牌撤销

    // 简单的黑名单(使用 Redis 更好)
    

    const tokenBlacklist = new Set();

    function logout(token) {

    tokenBlacklist.add(token);

    }

    function verifyToken(token) {

    if (tokenBlacklist.has(token)) {

    throw new Error('令牌已被撤销');

    }

    return jwt.verify(token, SECRET);

    }

    // 使用 Redis 的更好方式

    import redis from 'redis';

    const client = redis.createClient();

    async function logout(token) {

    const decoded = jwt.decode(token);

    const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);

    await client.setex(blacklist:${token}, expiresIn, 'true');

    }

    async function isBlacklisted(token) {

    return await client.exists(blacklist:${token});

    }

    常见攻击和防护

    | 攻击 | 描述 | 防护 |

    |------|------|------|

    | 令牌盗窃 | XSS 攻击获取令牌 | 使用 HttpOnly Cookie、CSP |

    | 令牌篡改 | 修改有效负载 | 验证签名 |

    | 重放攻击 | 重用旧令牌 | 短过期时间、令牌撤销 |

    | 密钥泄露 | 密钥被暴露 | 定期轮换、强密钥 |

    | Jti 重复 | 重复令牌 ID | 检查 jti 唯一性 |

    JWT 调试

    解码 JWT(不验证)

    const token = 'eyJ...';
    

    const decoded = jwt.decode(token); // 不验证签名!

    console.log(decoded);

    在线工具

    访问 https://jwt.io 来检查和调试 JWT:

  • 粘贴令牌
  • 查看解码的内容
  • 验证签名
  • 日志和监控

    function verifyToken(token) {
    

    try {

    const decoded = jwt.verify(token, SECRET);

    console.log('✓ 令牌有效', decoded);

    return decoded;

    } catch (error) {

    console.error('✗ 令牌错误', error.message);

    return null;

    }

    }

    结论

    JWT 是现代 Web 应用的标准身份验证方法:

    • 无状态 - 服务器不需要存储会话
    • 可扩展 - 适用于分布式系统
    • 安全 - 使用签名和加密
    • 灵活 - 支持自定义声明

    遵循最佳实践,确保应用程序安全!

    Share:

    相关文章

    Read in English