JWT与Session对比

面试官问:"你们项目里用的是JWT还是Session?为什么这么选?"

候选人小王说:"我们用的JWT,因为它是无状态的,特别适合微服务..."

面试官追问:"那用户改密码后,你怎么让他的旧Token失效?"

小王愣了一下,说:"呃...等它过期?"

面试官继续:"如果用户被封禁,你需要立即让他下线,怎么做?"

小王开始支支吾吾...

这个场景太典型了。JWT和Session的之争,从来不是"谁更先进"的问题,而是"谁更适合你的业务场景"。

今天这篇文章,带你把两种方案彻底搞清楚。

从一个问题开始

想象你要管理一座图书馆的借阅系统:

方案A:给每本书装个追踪器

  • 读者借书时,追踪器记录"谁借了哪本书、什么时候借的"
  • 任何时候想查,只需要扫描追踪器

方案B:建立借阅台账

  • 读者借书时,在台账上登记
  • 任何时候想查,只需要翻台账

你觉得哪种方案更好?

这道题没有标准答案——图书馆的规模、借阅频率、管理需求不同,答案就不同。

JWT就是方案A(追踪器),Session就是方案B(台账)

【直观类比】

Session:传统台账模式

想象你去酒店住宿:

  1. 你到前台登记,领取一张房卡
  2. 房卡只是个"凭证",不存储任何信息
  3. 酒店前台有个大台账,记录着"哪个房间谁住、什么时候入住"
  4. 你刷卡进门,前台查一下台账,确认你有权限

Session的工作原理一模一样

  • Cookie里的Session ID就是那张房卡
  • 服务端的Session存储就是那个台账
  • 验证时查一下台账,就知道"你是谁、有没有权限"
💡

Session的核心是"以我为准"——所有的状态都存在服务端,台账在我手里,信任我就好。

JWT:自包含通行证模式

想象你去演唱会:

  1. 你在网上买了票,票上印着你的名字、座位号、有效期
  2. 票本身就是个"完整凭证",不需要任何外部系统验证
  3. 检票员只需要看票上的信息,验一下真伪

JWT的工作原理就是这张演唱会门票

  • Token本身包含用户的身份信息和权限
  • 验证方只需要验证签名,不需要查数据库
  • Token流转到哪里,都能被验证
💡

JWT的核心是"自给自足"——Token自己证明自己,验证方不需要记住任何事。

两种模式的根本差异

传统台账 vs 自带防伪的票

台账(Session):
- 优点:随时可改、立即生效
- 缺点:需要同步、需要存储

自带票(JWT):
- 优点:无需存储、自包含
- 缺点:发出后难撤回、无法强制失效

Session认证原理

完整认证流程

sequenceDiagram
    participant U as 用户浏览器
    participant S as 服务器
    participant R as Redis(Session存储)
    
    Note over U:1. 访问登录页面
    U->>S: POST /login {username, password}
    
    Note over S:2. 验证成功,生成Session
    S->>R: 设置 session:abc123 = {user_id, role, ...}
    S->>U: 3. 返回Cookie: SESSIONID=abc123
    
    Note over U:4. 后续请求自动带Cookie
    U->>S: GET /api/profile Cookie: SESSIONID=abc123
    
    Note over S:5. 验证Session
    S->>R: 获取 session:abc123
    R->>S: 返回用户数据
    
    Note over S:6. 处理请求
    S->>U: 返回用户信息
    
    Note over U:7. 点击登出
    U->>S: POST /logout
    S->>R: 删除 session:abc123
    S->>U: 清除Cookie

Session的内部结构

# 服务端存储的Session数据结构
session_data = {
    "session_id": "abc123xyz",
    "user_id": 10086,
    "username": "zhangsan",
    "role": "admin",
    "login_time": "2024-01-15 10:30:00",
    "ip_address": "192.168.1.100",
    "user_agent": "Mozilla/5.0...",
    "max_age": 7200,  # 2小时有效期
    "created_at": 1705289400,
    "updated_at": 1705289400
}

# 存在Redis中的样子
redis.set("sess:abc123xyz", json.dumps(session_data), ex=7200)

Session的存储方案

存储介质优点缺点适用场景
内存(进程内)无额外依赖、最快不能跨进程共享、重启丢失单机测试
Redis高性能、支持集群需要额外服务生产环境首选
数据库持久化、可靠性能相对较低小规模应用
Memcached高性能不支持复杂数据结构简单Session

JWT认证原理

完整认证流程

sequenceDiagram
    participant U as 用户浏览器
    participant A as 认证服务器
    participant S as 业务服务器
    
    Note over U:1. 登录请求
    U->>A: POST /login {username, password}
    
    Note over A:2. 验证成功,签发Token
    A->>A: 生成JWT (包含用户信息)
    A->>U: 3. 返回 {access_token, refresh_token}
    
    Note over U:4. 携带Token访问API
    U->>S: GET /api/profile<br/>Authorization: Bearer eyJhbG...
    
    Note over S:5. 验证Token签名
    S->>S: 使用密钥验证Signature<br/>检查 exp 过期时间
    S->>U: 6. 返回用户信息
    
    Note over U:7. Token快过期了
    U->>A: POST /refresh<br/>{refresh_token}
    
    Note over A:8. 验证RefreshToken
    A->>A: 检查RefreshToken有效
    A->>U: 9. 返回新的 access_token

JWT的数据结构

# JWT的Header(头部)
header = {
    "alg": "HS256",  # 签名算法
    "typ": "JWT"     # 类型
}

# JWT的Payload(载荷)
payload = {
    "sub": "10086",           # 用户ID
    "name": "zhangsan",       # 用户名
    "role": "admin",          # 角色
    "iat": 1705289400,        # 签发时间
    "exp": 1705293000,        # 过期时间(15分钟后)
    "jti": "unique-token-id"  # Token唯一标识
}

# JWT的Signature(签名)
# HMACSHA256(
#   base64UrlEncode(header) + "." + base64UrlEncode(payload),
#   secret_key
# )

签名的两种算法

算法类型密钥分发适用场景安全性
HS256对称签名和验证用同一个密钥微服务内部、单体应用团队内部可信
RS256非对称私钥签名、公钥验证开放API、跨服务可暴露公钥给第三方

核心对比:Session vs JWT

存储位置

维度SessionJWT
身份信息存储服务端(Redis/数据库)客户端(Token中)
Session ID存储CookieCookie/LocalStorage/Header
验证依赖服务端存储 + Session IDToken签名验证

性能对比

Session验证流程:
请求 → 解析Cookie → 查询Redis → 返回数据

    1次Redis查询

JWT验证流程:
请求 → 解析Token → 验证签名 → 检查过期 → 返回数据

    纯本地计算(无网络开销)

结论:JWT在验证环节没有网络开销,理论上性能更好。但Session的Redis查询通常只需要0.1~1ms,实际差距微乎其微。

扩展性对比

Session扩展(需要Session共享):
用户1 → 负载均衡器 → 服务A(查Redis)→ 返回
用户2 → 负载均衡器 → 服务B(查Redis)→ 返回
用户3 → 负载均衡器 → 服务C(查Redis)→ 返回

                      共享同一个Redis

JWT扩展(无状态):
用户1 → 负载均衡器 → 服务A(本地验证)→ 返回
用户2 → 负载均衡器 → 服务B(本地验证)→ 返回
用户3 → 负载均衡器 → 服务C(本地验证)→ 返回

                    不需要任何共享存储
💡

JWT的"无状态"特性让它天然适合水平扩展,但Session通过共享存储(Redis)也能很好地扩展。

撤销能力对比

这是两种方案最大的差异点:

操作SessionJWT
用户主动登出✅ 删除服务端Session,立即生效⚠️ 只能等Token过期
管理员封禁用户✅ 直接删除Session,立即生效⚠️ 只能等Token过期
用户改密码✅ 使其Session失效⚠️ 只能等Token过期
踢出所有设备登录✅ 清除用户所有Session⚠️ 无法统一处理

JWT的撤销解决方案

# 方案1:Token黑名单
# 把被撤销的Token ID存入Redis
redis.setex(f"blacklist:{jti}", ttl_remaining, "1")

# 验证时先查黑名单
def verify_jwt(token):
    payload = decode_token(token)
    if redis.exists(f"blacklist:{payload['jti']}"):
        raise TokenRevokedError()
    return payload

# 方案2:Token版本号
# 用户表增加 version 字段
user = db.get_user(user_id)
expected_version = user['token_version']

# Token Payload中包含 version
payload = decode_token(token)
if payload['version'] != expected_version:
    raise TokenExpiredError()

# 用户改密码时递增版本号
db.update_user(user_id, token_version=user['token_version'] + 1)
⚠️

黑名单方案会引入网络查询,一定程度上违背了JWT"无状态"的优势。如果你需要频繁撤销,Session可能是更好的选择。

适用场景分析

什么时候选Session?

场景原因
需要立即撤销权限用户封禁、账号冻结、主动登出
需要存储大量用户数据用户信息经常变化、不适合放Token
团队规模较小不想管理复杂的密钥体系
需要单点登录(SSO)Session更易实现跨域认证
审计要求高需要记录所有会话的详细活动

典型场景: -企业内部管理系统(需要管理员随时封禁用户) -金融系统(监管要求能随时冻结账户) -多设备登录管理(需要查看/踢出特定设备)

什么时候选JWT?

场景原因
微服务架构需要跨服务共享身份、无状态扩展
开放API/第三方集成第三方无法访问你的Redis
短期授权临时访问令牌、OAuth Access Token
一次性认证邮箱验证、密码重置链接
高频低延迟验证频率极高,Redis查询成为瓶颈

典型场景: -微服务间的API调用(服务A验证后调用服务B) -移动端App认证(不想依赖服务端Session存储) -第三方OAuth授权(Access Token流转给第三方)

混合方案:短期Token + Refresh Token

为什么需要混合方案?

纯粹JWT的问题:

  • Token太短:用户体验差,频繁刷新
  • Token太长:安全性差,被盗后损失大

纯粹Session的问题:

  • 依赖服务端存储
  • 水平扩展需要Session共享

混合方案结合两者优点:

sequenceDiagram
    participant U as 用户
    participant A as 认证服务器
    participant S as 业务服务器
    participant R as Redis
    
    Note over U,A:登录时
    U->>A: 登录 {username, password}
    A->>R: 存储 RefreshToken (7天)
    A->>U: 返回 AccessToken (15分钟)
    
    Note over U,S:正常访问
    U->>S: GET /api {AccessToken}
    S->>S: 验证通过
    S->>U: 返回数据
    
    Note over U,A:AccessToken过期
    U->>A: POST /refresh {RefreshToken}
    A->>R: 验证RefreshToken
    A->>U: 返回新AccessToken
    
    Note over U,A:主动登出
    U->>A: POST /logout
    A->>R: 删除RefreshToken
    A->>U: 清除AccessToken
    
    Note over A:用户被封禁
    A->>R: 删除用户RefreshToken
    Note over U,S:下次请求时
    S->>S: AccessToken验证失败<br/>要求重新登录

Refresh Token的设计要点

# Refresh Token的特征
refresh_token_payload = {
    "sub": "10086",              # 用户ID
    "type": "refresh",           # Token类型
    "device_id": "device-abc",   # 设备标识(可选)
    "jti": "refresh-token-id",   # 唯一ID
    "iat": 1705289400,
    "exp": 1705894200,           # 7天过期
}

# 存储在Redis中的Refresh Token
redis.setex(
    f"refresh:{user_id}:{device_id}",
    7 * 24 * 3600,  # 7天
    json.dumps({
        "token_jti": "refresh-token-id",
        "created_at": 1705289400,
        "user_ip": "192.168.1.100",
        "user_agent": "..."
    })
)

# 限制每个用户的Refresh Token数量(防止多点登录滥用)
def limit_refresh_tokens(user_id, max_devices=5):
    keys = redis.keys(f"refresh:{user_id}:*")
    if len(keys) > max_devices:
        # 删除最早的
        oldest = get_oldest_token(keys)
        redis.delete(oldest)

边界与特例

Token存放位置

存储位置优点缺点攻击方式
Cookie (HttpOnly)防止XSS读取、自动发送受CSRF攻击CSRF
Cookie (HttpOnly + Secure)防止XSS + 仅HTTPS受CSRF攻击CSRF
LocalStorage不受CSRF影响容易被XSS读取XSS
SessionStorage页面关闭即清除不能跨Tab共享XSS
💡

Web应用推荐:Access Token放Authorization Header,Refresh Token放HttpOnly Cookie。 移动端推荐:存放在系统级Keychain/Keystore中。

并发登录处理

需求:限制同一账号同时在线的设备数量

Session方案

def login(user_id, session_id):
    # 查找该用户已有的Session
    existing_sessions = redis.smembers(f"user_sessions:{user_id}")
    
    # 如果超过限制,删除最早的
    if len(existing_sessions) >= MAX_CONCURRENT_LOGINS:
        oldest = redis.zrange(f"user_login_times:{user_id}", 0, 0)[0]
        redis.delete(f"sess:{oldest}")
        redis.srem(f"user_sessions:{user_id}", oldest)
    
    # 创建新Session
    redis.sadd(f"user_sessions:{user_id}", session_id)
    redis.zadd(f"user_login_times:{user_id}", {session_id: time.time()})

JWT方案

def login(user_id):
    # 检查当前Refresh Token数量
    current_tokens = redis.keys(f"refresh:{user_id}:*")
    
    if len(current_tokens) >= MAX_CONCURRENT_LOGINS:
        # 删除最早的
        oldest = redis.zrange(f"user_refresh_times:{user_id}", 0, 0)[0]
        redis.delete(oldest)
        redis.zremrangebyscore(f"user_refresh_times:{user_id}", 0, oldest_score)
    
    # 签发新Token
    access_token = create_access_token(user_id)
    refresh_token = create_refresh_token(user_id, device_id)
    
    return access_token, refresh_token

跨域认证

Session的跨域问题

  • 浏览器同源策略限制Cookie跨域发送
  • 需要配置CORS或使用JSONP
  • SSO(单点登录)需要中心化Session存储

JWT的跨域优势

  • Token放在Header中,不受Cookie同源限制
  • 跨域请求只需带Authorization头
  • 更适合前后端分离架构
# 前端跨域携带JWT
fetch('https://api.example.com/user', {
    headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
    }
})

常见误区

误区1:JWT比Session更安全

错误。安全性取决于具体实现,不是认证方案本身。

  • Session如果Redis被黑,所有会话泄露
  • JWT如果密钥泄露,所有Token可被伪造
  • 两者都可能被XSS攻击窃取

正确的做法:根据业务需求选方案,然后做好安全防护。

误区2:JWT Payload是加密的

错误。JWT的Header和Payload只是Base64编码,任何人都能解码看到内容:

import base64
import json

token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

# 任何人都能解码
header = json.loads(base64.urlsafe_b64decode(token.split('.')[0] + "=="))
payload = json.loads(base64.urlsafe_b64decode(token.split('.')[1] + "=="))

print(header)  # {"alg":"HS256","typ":"JWT"}
print(payload) # {"sub":"1234567890","name":"John Doe"}
⚠️

不要在JWT Payload中存储敏感信息,如密码、身份证号、银行卡号。如果必须存储,先加密。

误区3:Token放URL参数里没问题

错误。URL参数会被记录在:

  • 浏览器历史记录
  • 服务器日志
  • Referer头(跳转来源)
  • 代理服务器日志

正确做法:使用Authorization Header或Cookie传递Token。

误区4:Token永远不设置过期

严重错误。没有过期时间的Token,一旦泄露,攻击者就有永久访问权限。

合理过期时间建议

Token类型过期时间说明
Access Token15分钟~1小时短期访问
Refresh Token1天~7天续期凭证
API Token(服务端间)1小时~24小时根据业务调整
临时验证Token5~30分钟邮箱验证、密码重置

误区5:Session一定比JWT慢

不准确。Session多一次Redis查询,但通常只需要0.1~1ms。JWT的签名验证是纯CPU计算,现代CPU每秒能验证数十万次。

真正的性能差距:只有在极高的QPS(10万+)下,JWT的"无网络开销"优势才明显。普通应用中,两者的性能差距可以忽略不计。

记忆技巧

口诀

Session:台账模式,数据在我手,改了立即有 JWT:票根模式,数据随身走,发了收不回 短期Token保安全,长期Refresh来续命 需要撤销选Session,需要扩展选JWT

选型决策树

flowchart TD
    A[需要选认证方案] --> B{需要立即撤销权限?}
    B -->|是| C[需要精细化控制会话?] 
    B -->|否| D{微服务/开放API?}
    
    C -->|是| E[选Session]
    C -->|否| F[短期JWT<br/>+RefreshToken]
    
    D -->|是| G[选JWT]
    D -->|否| H{团队规模小?]
    
    H -->|是| I[选Session<br/>简单易维护]
    H -->|否| J[评估后决定<br/>考虑混合方案]
    
    E --> K[配合Redis存储]
    F --> L[短期Access<br/>长期Refresh]
    G --> M[无状态扩展<br/>RS256签名]

对比速查表

维度SessionJWT
存储位置服务端客户端
验证方式查Session存储本地签名验证
扩展性需要Session共享天然支持水平扩展
撤销能力✅ 立即生效⚠️ 需要黑名单/版本号
Token大小小(Session ID)大(完整Token)
传输开销中等(Token较大)
适用场景需要强控制微服务/开放API

实战检验

检验1:设计一个登录系统

需求

  • 用户名密码登录
  • 限制同一账号最多3个设备同时在线
  • 支持主动登出所有设备

推荐方案:混合方案

# 登录逻辑
def login(username, password):
    user = verify_credentials(username, password)
    
    # 检查当前设备数
    current_devices = redis.keys(f"refresh:{user.id}:*")
    if len(current_devices) >= 3:
        # 删除最早的设备
        oldest = get_oldest_device(current_devices)
        redis.delete(oldest)
    
    # 生成Token
    access_token = create_access_token(user.id, expires_in=900)  # 15分钟
    refresh_token = create_refresh_token(user.id, device_id, expires_in=86400)  # 1天
    
    # 存储RefreshToken
    redis.setex(f"refresh:{user.id}:{device_id}", 86400, json.dumps({
        "created_at": time.time()
    }))
    
    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "expires_in": 900
    }

# 登出所有设备
def logout_all(user_id):
    devices = redis.keys(f"refresh:{user_id}:*")
    for device in devices:
        redis.delete(device)
    return {"message": "已登出所有设备"}

检验2:排查"用户登录后立即掉线"

排查思路

1. 检查Token是否正确返回
   - 浏览器开发者工具 → Network → 找登录请求 → 看响应头/响应体

2. 检查Token是否被正确存储
   - Chrome DevTools → Application → Cookies/LocalStorage

3. 检查请求是否带Token
   - Network → 请求头是否有 Authorization

4. 检查Token是否过期
   - jwt.io 解码Token → 看 exp 字段

5. 检查服务端验证逻辑
   - 签名算法是否正确
   - 密钥是否一致
   - 过期时间是否合理

延伸阅读