JWT 与 Session 完全指南:Web 登录态方案的工程选型

一个安全事故让我对用户登录这件做了无数遍的事彻底重新认识。系统用 JWT 做登录态,用户登录成功后端签发一个 token 之后每个请求带着它后端验签名就认人,这套方案用得很顺手文档照着抄几行代码就跑通。直到客服转来投诉:一个用户账号被盗他自己改了密码想把盗号的人踢下线,我去后台一通操作却发现做不到 —— 那个盗号者手里的 token 只要还没到过期时间就依然有效,用户改了密码旧 token 照样畅通无阻。梳理:HTTP 是无状态的服务器处理完一个请求就彻底忘掉下个请求哪怕同一个人在服务器眼里也是陌生人,登录态就是要造一种机制让服务器认出这些请求是同一个已登录的人,真正的分歧在发给用户的凭证里装什么 —— 选择 A 只装一个 ID 号码牌身份信息存服务端就是 Session-Cookie 服务器记得你,选择 B 直接装完整身份信息再盖防伪章服务端什么都不存就是 JWT 服务器不记得你但你把身份带在身上。Session-Cookie 登录成功服务器在自己这边建一条 session 记录给它一个随机 session ID 通过 Set-Cookie 塞进 Cookie 之后每个请求服务器拿 ID 去存储里查出记录,优点服务端完全可控想作废谁就删那条记录强制下线改密码踢登录轻轻松松凭证极小敏感信息不出服务器,代价服务端要存储加查询分布式下多台服务器要把 session 集中存 Redis 共享。JWT 是点隔成三段的字符串 Header 签名算法 Payload 装身份信息本身 Signature 用只有服务器知道的密钥算的防伪章,前两段只是 Base64 编码不是加密谁都能解开看 JWT 保证的不是看不见是改不了别往 Payload 放密码,登录后服务器签个 JWT 发给客户端自己什么都不存之后每个请求服务器不查任何库只验签名签名对且没过期就直接信 Payload,优点服务端无状态不用存天然适合分布式微服务任何一台有密钥就能独立验,代价签发出去就收不回。JWT 硬伤的根是无状态是双刃剑服务器什么都不存就没有一个地方可以标记这个 token 作废了,验 token 全程只验签名加看过期不查任何库所以 token 只要签名真且没过期就永远有效,用户改了密码服务器验 token 时根本不会去查密码改没改盗号者手里的旧 token 畅通无阻。工程折中别和无状态死磕:短命 access token 设 15 分钟把危险窗口压到最小加长命 refresh token 存在服务端可作废,踢人就作废 refresh token 用最多 15 分钟延迟换回可作废能力;token 黑名单记进 Redis 但等于给 JWT 加回了服务端存储。选型对系统问三个硬问题:需不需要精确即时控制登录态需要的话 Session-Cookie 更省心、是不是重度分布式微服务多端是则 JWT 无状态很香、用户量级大到 session 存储成负担吗。正确做法是大多数系统用折中方案 JWT 做 access token 加服务端 refresh token,以及技术选型里几乎没有更先进只有我更需要哪种好处更付得起哪种代价。

2023 年,一个安全事故让我对"用户登录"这件我做了无数遍的事,彻底重新认识了一遍。我们的系统用 JWT 做登录态——用户登录成功,后端签发一个 token,之后每个请求都带着它,后端验一下签名就认人。这套方案我用得很顺手,文档照着抄,几行代码就跑通了,我一度觉得"登录"这事我早就吃透了。直到那天,客服转来一个投诉:一个用户的账号被盗了,他自己改了密码,想把盗号的人踢下线。我去后台一通操作,却发现一个让我头皮发麻的事实——我【做不到】。那个盗号者手里的 token,只要还没到过期时间,就【依然有效】,我没有任何办法让它当场失效。用户改了密码,旧 token 照样畅通无阻。我当时盯着屏幕,一身冷汗:我用了这么久的 JWT,我居然从来不知道,它签发出去的令牌,我【收不回来】?接着我又翻出一串积压的疑问:为什么有的系统能"强制某个用户下线",我的不行?为什么我的 token 越来越大、每个请求都在变重?为什么有人说 JWT 不该存 localStorage?我一直以为登录方案就是"抄个 JWT 库",原来 Session 和 JWT 之间,藏着一整套我从没认真想过的工程权衡。这件事逼着我把 HTTP 为什么需要登录态、Session-Cookie 和 JWT 各自怎么工作、JWT 那个"收不回"的硬伤、以及真实项目里到底该怎么选,彻底理清了一遍。本文是这份梳理的完整复盘。

问题背景:一个"踢不掉的盗号者"

环境:一个 Web 系统,用 JWT 做登录态
事故经过:
- 用户账号被盗,用户自己改了密码,要求"把盗号的人踢下线"
- ★ 我去后台操作 —— 发现:做不到
- ★★ 盗号者手里那个 JWT,只要没到过期时间,就一直有效
- 用户改密码这个动作,对那个"已经签发出去的旧 token",
  毫无影响 —— 它照样能通过验证

★★ 我懵在这里:我用了这么久 JWT,竟不知道它签出去的
   令牌,服务端【收不回、也作废不了】?

连带翻出来的一串疑问:
- ★ 为什么有的系统能"强制指定用户下线",我的系统不能?
- ★ 为什么我的 token 越来越大,每个请求都在变重?
- ★ 为什么有人反复说"JWT 别存在 localStorage 里"?

★ 问题的根,不在"代码写错了",在我从没想清楚一件事:
  "登录态"这个东西,到底【存在哪里】 —— 存在服务端,
  还是存在那张令牌自己身上?这一个选择,决定了上面
  所有问题的答案。

★★ 两种主流方案,本质区别就一句话:
  - Session-Cookie:登录态【存在服务端】,Cookie 里只有
    一个"号码牌"。服务端是"记得"你的。
  - JWT:登录态【存在令牌自己身上】,服务端不存任何东西,
    每次靠验签来确认。服务端是"不记得"你的。
  本文要做的,就是把这两条路,以及怎么选,彻底讲透。

HTTP 是"无状态"的——所以才需要"登录态"

# === ★ 先想清楚:为什么登录这件事,会"有点麻烦" ===

# === ★ HTTP 协议,天生"健忘" ===
# ★ ★ HTTP 是【无状态】的:服务器处理完一个请求,就
#   把这个请求【彻底忘掉】。下一个请求来,哪怕是同一个
#   人发的,在服务器眼里,也是一个【素不相识的陌生人】。
# ★ 这个设计本身是优点(服务器简单、好扩展),但它带来
#   一个问题:用户在第 1 个请求里登录了,到第 2 个请求,
#   服务器【完全不记得】他登录过。

# === ★ "登录态"要解决的,就是这个"健忘" ===
# ★ ★ 所谓登录态(也叫会话/认证状态),就是要造出一种
#   机制,让服务器在面对一个又一个独立的请求时,能认出
#   "哦,这些请求,是同一个【已登录的】人发来的"。
# ★ 核心手段:登录成功后,发给客户端一张【凭证】。之后
#   客户端每个请求都带上它,服务器靠这张凭证来认人。

# === ★★ 真正的分歧:这张"凭证"里,装的是什么 ===
# ★ 所有方案,都绕不开这个根本选择 —— 发给用户的那张
#   凭证里,到底装什么:
# ★ ★ 选择 A:凭证里只装一个【ID(号码牌)】。真正的
#   身份信息(你是谁、什么权限)存在【服务端】。服务器
#   拿到号码牌,去自己的本子上查"这个号是谁"。
#   —— 这就是 Session-Cookie。
# ★ ★ 选择 B:凭证里【直接装上完整的身份信息】(你是
#   谁、什么权限、什么时候过期),并盖一个防伪的章。
#   服务器自己【什么都不存】,拿到凭证,验一下章是真的,
#   就直接读里面的信息。
#   —— 这就是 JWT。
# ★ ★★ A 是"服务器记得你",B 是"服务器不记得你、但你
#   把身份自己带在身上"。这一个分歧,衍生出两者所有的
#   优劣 —— 接下来两节,逐个拆。

# === 小结 ===
# ★ HTTP 天生【无状态】:服务器处理完一个请求就彻底忘掉,
#   下个请求哪怕同一个人发的在服务器眼里也是陌生人。
#   "登录态"就是要造一种机制,让服务器在一个个独立请求
#   里认出"这些是同一个已登录的人"——手段是登录后发给
#   客户端一张凭证,之后每个请求都带上它。★★ 真正的
#   分歧在这张凭证里装什么:选择 A 只装一个 ID 号码牌、
#   身份信息存服务端(Session-Cookie,服务器记得你);
#   选择 B 直接装完整身份信息再盖防伪章、服务端什么都
#   不存(JWT,服务器不记得你但你把身份带在身上)。这
#   一个分歧衍生出两者所有优劣。

方案一:Session-Cookie——登录态存在服务端

# === ★ Session-Cookie:服务器"记得"每一个登录的人 ===

# === ★ 它是怎么工作的 ===
# ★ ① 用户登录成功 -> 服务器在【自己这边】(内存 / Redis
#   / 数据库)创建一条 session 记录,内容是"这个用户是
#   谁、有什么权限",并给这条记录一个随机的、猜不到的
#   【session ID】。
# ★ ② 服务器把这个 session ID,通过 Set-Cookie 响应头,
#   塞进用户浏览器的 Cookie 里。★ 注意:Cookie 里【只有
#   那个 ID】,没有任何真实身份信息。
# ★ ③ 之后用户每个请求,浏览器【自动】带上这个 Cookie。
#   服务器拿到 session ID,去自己的存储里【查】出对应
#   的 session 记录,于是认出了用户。

# === ★ 它的优点 ===
# ★ ① ★★ 服务端完全可控:登录态是服务器自己存的,它想
#   作废谁,就【删掉那条 session 记录】 —— 那个用户下个
#   请求立刻失效。"强制下线""改密码踢登录",轻轻松松。
# ★ ② ★ 凭证极小:Cookie 里就一个 ID,请求很轻。
# ★ ③ ★ 身份信息不出服务器:敏感信息存在服务端,客户端
#   那个 ID 就算被看到,也读不出任何东西。

# === ★ 它的代价 ===
# ★ ① ★★ 服务端要【存储 + 查询】:每个在线用户,服务器
#   都得存一条 session,每个请求都要查一次。用户量巨大
#   时,这是一笔实打实的存储和性能开销。
# ★ ② ★ 分布式下要"共享 session":如果你有多台服务器,
#   用户这次请求落到 A、下次落到 B,B 上没有这条 session
#   就认不出人。解法:把 session 集中存到 Redis,所有
#   服务器都连它 —— 多一个公共依赖。
# ★ ③ ★ Cookie 天生跨域麻烦,纯 API / 多端(App)场景
#   不如 token 灵活。

# === 小结 ===
# ★ Session-Cookie:服务器"记得"每个登录的人。① 登录成功
#   服务器在自己这边(内存/Redis/DB)建一条 session 记录
#   存身份信息、给它一个随机 session ID;② 把 ID 通过
#   Set-Cookie 塞进浏览器 Cookie(只有 ID 没有真实身份);
#   ③ 之后每个请求浏览器自动带 Cookie,服务器拿 ID 去
#   存储里查出记录认出用户。★★ 优点:服务端完全可控,
#   想作废谁就删那条记录,强制下线/改密码踢登录轻轻松松;
#   凭证极小;敏感信息不出服务器。★ 代价:服务端要存储
#   +查询每个在线用户都占一条;分布式下多台服务器要把
#   session 集中存 Redis 共享;Cookie 跨域和多端不灵活。
// ★ Session-Cookie 方案(Node / Express 示意)
// ① 登录:在服务端建 session,Cookie 里只回一个 session ID
app.post('/login', async (req, res) => {
  const user = await checkPassword(req.body);
  if (!user) return res.status(401).send('账号或密码错误');

  const sid = crypto.randomUUID();              // ★ 随机、猜不到的 ID
  // ★★ 真实身份信息,存在【服务端】(这里用 Redis)
  await redis.set(`sess:${sid}`, JSON.stringify({
    userId: user.id, role: user.role,
  }), 'EX', 7 * 24 * 3600);
  // ★ Cookie 里只放那个 ID,httpOnly 让 JS 读不到它
  res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
  res.send('登录成功');
});

// ② 校验:拿 Cookie 里的 ID,去服务端【查】出身份
async function auth(req, res, next) {
  const sess = await redis.get(`sess:${req.cookies.sid}`);
  if (!sess) return res.status(401).send('未登录或已失效');
  req.user = JSON.parse(sess);
  next();
}

// ③ ★★ 强制下线 / 改密码踢登录:删掉那条 session 即可
async function forceLogout(sid) {
  await redis.del(`sess:${sid}`);   // ★ 该用户下个请求立刻失效
}

方案二:JWT——登录态自带在令牌里

# === ★ JWT:服务器"不记得"你,但你把身份带在身上 ===

# === ★ JWT 长什么样 ===
# ★ JWT(JSON Web Token)是一串用点隔成三段的字符串:
#   xxxxx.yyyyy.zzzzz
#  - ★ 第 1 段 Header:说明用了什么签名算法;
#  - ★ 第 2 段 Payload:★★ 装的就是身份信息本身 ——
#    用户 ID、角色、签发时间、过期时间(exp)等;
#  - ★ 第 3 段 Signature:★★ 签名 —— 用一个【只有服务器
#    知道的密钥】,对前两段算出来的"防伪章"。
# ★ ★★ 关键认知:前两段只是 Base64 编码,【不是加密】 ——
#   任何人都能解开看里面的内容。JWT 保证的不是"看不见",
#   是"改不了"(改了,签名就对不上)。所以别往 Payload
#   里放密码这种秘密。

# === ★ 它是怎么工作的 ===
# ★ ① 用户登录成功 -> 服务器把身份信息塞进 Payload,用
#   密钥签个名,拼成 JWT,发给客户端。★★ 服务器自己
#   【什么都不存】。
# ★ ② 客户端之后每个请求,带上这个 JWT。
# ★ ③ 服务器收到,★ 不查任何数据库 —— 它只用密钥【验
#   一下签名】:签名对、且没过期,就【直接信 Payload
#   里写的身份】。

# === ★ 它的优点 ===
# ★ ① ★★ 服务端无状态、不用存:省掉了 session 的存储和
#   每次查询。这是 JWT 最大的卖点。
# ★ ② ★★ 天然适合分布式 / 微服务:任何一台服务器,只要
#   有那个密钥,就能独立验证 token,不需要共享存储。
#   用户请求落到哪台都行。
# ★ ③ ★ 不依赖 Cookie,App、小程序、跨域 API 都好用 ——
#   token 放哪、怎么传,由你定。

# === ★ 它的代价(下一节专门讲那个最痛的)===
# ★ ① ★★ 签发出去就【收不回】 —— 这是硬伤,见下一节。
# ★ ② ★ token 体积比 session ID 大得多,且身份信息越多
#   它越大,每个请求都带着它,有流量成本。
# ★ ③ ★ Payload 是明文可见的,不能放敏感信息。

# === 小结 ===
# ★ JWT:服务器"不记得"你,但你把身份带在身上。它是点
#   隔成三段的字符串:Header(签名算法)、Payload(★★装
#   身份信息本身:用户 ID、角色、过期时间)、Signature
#   (用只有服务器知道的密钥算的防伪章)。★★ 前两段只是
#   Base64 编码不是加密,谁都能解开看,JWT 保证的不是
#   "看不见"是"改不了"——别往 Payload 放密码。工作方式:
#   登录后服务器签个 JWT 发给客户端、★★自己什么都不存,
#   之后每个请求带上它,服务器不查任何库只用密钥验签名,
#   签名对且没过期就直接信 Payload 里的身份。★★ 优点:
#   服务端无状态不用存、天然适合分布式微服务任何一台
#   有密钥就能独立验、不依赖 Cookie 多端好用。代价:
#   签发出去收不回(硬伤)、体积大有流量成本、明文可见。
# ★ JWT 方案(Python / PyJWT 示意)
import jwt, time

SECRET = '只有服务器知道的密钥'   # ★★ 泄露 = 任何人都能伪造身份

# ① 登录:签发 JWT —— 身份信息直接塞进 payload
def login(username, password):
    user = check_password(username, password)
    if not user:
        return None
    payload = {
        'uid': user['id'],
        'role': user['role'],
        'iat': int(time.time()),                  # 签发时间
        'exp': int(time.time()) + 2 * 3600,        # ★ 过期时间 2 小时
    }
    # ★★ 签名后服务器什么都不存,token 直接发给客户端
    return jwt.encode(payload, SECRET, algorithm='HS256')

# ② 校验:不查任何数据库,只验签名 + 看过期
def verify(token):
    try:
        # ★ 验签名 + 自动校验 exp;通过就直接信 payload
        return jwt.decode(token, SECRET, algorithms=['HS256'])
    except jwt.ExpiredSignatureError:
        return None        # ★ 过期了
    except jwt.InvalidTokenError:
        return None        # ★ 签名不对 —— 被篡改 / 伪造

JWT 的硬伤:签发了,就"收不回"

# === ★ 把本文那个事故的根,挖出来 ===

# === ★★ 硬伤的来源:无状态,是一把双刃剑 ===
# ★ JWT 最大的优点是"服务器什么都不存"。但你反过来想:
#   服务器【什么都不存】,就意味着它【没有一个地方,可以
#   去标记"这个 token 作废了"】。
# ★ ★ 服务器验 token,全程只做一件事:验签名 + 看 exp。
#   它【不查任何数据库】。所以一个 token,只要:① 签名
#   是真的(没被改);② 还没到 exp —— 它就【永远有效】。
# ★ ★★ 用户改了密码?服务器验 token 时根本不会去查"密码
#   改没改"。盗号者手里那个旧 token,签名真、没过期,
#   于是【畅通无阻】。这就是本文事故的全部真相。

# === ★ 由这个硬伤,衍生出几个现实难题 ===
# ★ 难题 1:★★ 无法主动作废单个 token(用户登出、改密码、
#   账号被盗)—— 你没有"删掉它"的地方。
# ★ 难题 2:★ 无法"强制某用户下线"。
# ★ 难题 3:★ 改了用户权限,旧 token 里还是【旧权限】,
#   要等它自然过期才更新。

# === ★★ 工程上的折中:别和无状态死磕,给它配个"补丁" ===
# ★ 折中 1 —— ★★ 短命的 access token + 长命的 refresh token:
#  - ★ access token:真正用来访问接口的,把 exp 设得
#    【很短】(如 15 分钟)。它收不回?没关系,它很快
#    就自己过期了 —— 把"危险窗口"压到最小。
#  - ★ refresh token:专门用来"换新 access token"的,
#    命长(如 7 天),但它【存在服务端】(像 session 一样
#    可作废)。access token 一过期,客户端拿 refresh token
#    去换一个新的。
#  - ★★ 要踢人下线?把那个 refresh token 在服务端作废。
#    那么最多 15 分钟后,他的 access token 过期、又换不到
#    新的 —— 他就被踢下线了。用"最多 15 分钟延迟",换回
#    了"可作废"的能力。
# ★ 折中 2 —— ★ token 黑名单:需要立刻作废某个 token 时,
#   把它的 ID 记进一个 Redis 黑名单(存到它自然过期为止)。
#   验 token 时多查一步黑名单。★★ 注意:这其实是给 JWT
#   重新加回了"服务端存储" —— 等于承认了纯无状态的代价。

# === 认知 ===
# ★ JWT 硬伤的根:无状态是双刃剑 —— 服务器什么都不存,
#   就意味着它没有一个地方可以标记"这个 token 作废了"。
#   ★★ 服务器验 token 全程只验签名+看 exp、不查任何库,
#   所以 token 只要签名真且没到 exp 就永远有效;用户改了
#   密码服务器验 token 时根本不会去查密码改没改,盗号者
#   手里的旧 token 畅通无阻 —— 这就是本文事故真相。衍生
#   难题:无法主动作废单个 token、无法强制下线、改了权限
#   旧 token 还是旧权限。★★ 工程折中别和无状态死磕:
#   ① 短命 access token(15 分钟,把危险窗口压到最小)+
#   长命 refresh token(存服务端、可作废),踢人就作废
#   refresh token,用最多 15 分钟延迟换回可作废能力;
#   ② token 黑名单记进 Redis —— 但这等于给 JWT 加回了
#   服务端存储,承认了纯无状态的代价。
# ★ 折中方案:短命 access token + 长命可作废 refresh token
import jwt, time, secrets

ACCESS_TTL = 15 * 60          # ★ access token 只活 15 分钟
REFRESH_TTL = 7 * 24 * 3600   # ★ refresh token 活 7 天

def issue_tokens(user, redis):
    access = jwt.encode({
        'uid': user['id'], 'role': user['role'],
        'exp': int(time.time()) + ACCESS_TTL,
    }, SECRET, algorithm='HS256')

    # ★★ refresh token 是个随机串,存在服务端 —— 因此可作废
    refresh = secrets.token_urlsafe(32)
    redis.set(f'refresh:{refresh}', user['id'], ex=REFRESH_TTL)
    return access, refresh

# ★ access 过期后,拿 refresh 换新的(换之前先查它还在不在)
def refresh_access(refresh, redis):
    uid = redis.get(f'refresh:{refresh}')
    if not uid:
        return None                       # ★ 已被作废 / 已过期
    user = load_user(uid)
    return issue_tokens(user, redis)[0]

# ★★ 踢用户下线 / 改密码:作废他的 refresh token
def force_logout(refresh, redis):
    redis.delete(f'refresh:{refresh}')    # ★ 最多 15 分钟后,他彻底出局

工程选型:你的系统到底该用哪一种

# === ★ 选型不靠"哪个新",靠对你的系统问几个硬问题 ===

# === ★ 问题 1:你需要"精确、即时地控制登录态"吗 ===
# ★ ★ 如果你的系统,强需求"随时踢人下线、改密码立即
#   失效、改权限立即生效"(后台管理系统、金融、企业
#   内部系统)—— ★ Session-Cookie 是更省心的选择,
#   作废登录态是它的天生能力。
# ★ 硬上纯 JWT,你会被那个"收不回"反复折磨。

# === ★ 问题 2:你是不是重度分布式 / 微服务 / 多端 ===
# ★ ★ 如果你有很多个服务、很多台机器,或者要同时服务
#   Web + App + 小程序 —— ★ JWT 的"无状态、任何节点
#   独立验证、不挑客户端"就非常香,省掉共享 session
#   的麻烦。

# === ★ 问题 3:你的用户量级,大到 session 存储成为负担吗 ===
# ★ 用户量不大 -> session 存 Redis 毫无压力,别为"省那点
#   存储"去用 JWT 把自己架到火上。
# ★ 海量用户 -> JWT 省去每请求一次的 session 查询,确实
#   有价值 —— 但配合短 token + refresh 折中方案一起上。

# === ★★ 最实在的答案:大多数系统,用"折中方案" ===
# ★ 纯 Session 和纯 JWT,都是两个极端。生产环境最常见、
#   也最稳的,是【两者的结合】:
#  - ★ 用 JWT 做那个【短命的 access token】 —— 拿它的
#    无状态优点,享受"大多数请求不用查库"。
#  - ★ 用一个【存在服务端的 refresh token】 —— 拿回
#    Session 那种"可作废、可控"的能力。
# ★ ★★ 这套组合,等于"日常高频校验走无状态(快),
#   作废控制走有状态(可控)" —— 两边的好处都占住了。

# === ★ 几条不分方案的铁律 ===
# ★ ① 令牌/Cookie 一定走 HTTPS,否则中途被截走就完了。
# ★ ② Cookie 设 httpOnly(JS 读不到,防 XSS 偷)、
#   secure、sameSite。
# ★ ③ JWT 的 Payload 是明文,★ 绝不放密码等敏感信息。
# ★ ④ 签名密钥要足够强、要保密 —— ★★ 它一泄露,任何人
#   都能伪造任意身份,整个认证体系崩塌。

# === 认知 ===
# ★ 选型对系统问三个硬问题:① 需不需要精确即时控制登录
#   态(随时踢人、改密码立即失效)——需要(后台/金融/
#   企业系统)Session-Cookie 更省心,硬上纯 JWT 会被
#   "收不回"反复折磨;② 是不是重度分布式微服务多端 ——
#   是则 JWT 无状态任何节点独立验不挑客户端很香;③ 用户
#   量级大到 session 存储成负担吗 —— 量不大别为省存储
#   用 JWT 把自己架火上,海量用户 JWT 才有价值但要配
#   折中方案。★★ 最实在的答案:大多数系统用折中方案 ——
#   JWT 做短命 access token(无状态、高频校验快)+ 存
#   服务端的 refresh token(拿回可作废可控能力)。铁律:
#   走 HTTPS、Cookie 设 httpOnly+secure+sameSite、JWT
#   Payload 明文不放敏感信息、签名密钥要强要保密(一泄露
#   任何人能伪造任意身份整个体系崩塌)。

命令速查

两种方案核心对比
=============================================================
维度              Session-Cookie        JWT
-------------------------------------------------------------
登录态存在哪      服务端(Redis/DB)     令牌自己身上
服务端要存储吗    要,每个在线用户一条   不用,无状态
每次请求要查库吗  要,查 session         不用,只验签名
能否主动作废      ★能,删记录即失效      ★不能,过期前一直有效
强制下线/踢人     ★轻松                 ★难,要配 refresh/黑名单
分布式友好        要共享 session 存储    ★天生友好,任意节点验
凭证大小          很小,就一个 ID        较大,身份越多越大
多端(App/小程序) Cookie 跨域不便        ★灵活,token 随便传

关键概念速查
-------------------------------------------------------------
JWT 三段          Header.Payload.Signature
Payload           明文 Base64,可解开看,★绝不放敏感信息
Signature         服务器密钥签的防伪章,保证"改不了"非"看不见"
access token      真正访问接口用,exp 设短(如 15 分钟)
refresh token     换新 access 用,存服务端、可作废、命长
httpOnly          Cookie 设它,JS 读不到,防 XSS 窃取

口诀:Session 是服务器记得你,JWT 是你把身份带在身上
      纯方案是极端,生产最稳是 JWT access + 服务端 refresh

避坑清单

  1. HTTP 是无状态的,服务器处理完一个请求就忘掉,登录态就是要让它认出这些请求是同一个人
  2. 两种方案的本质分歧是登录态存在哪,Session 存服务端、JWT 存令牌自己身上,优劣全由此衍生
  3. JWT 的 Payload 只是 Base64 编码不是加密,任何人都能解开看,绝不要往里面放密码等敏感信息
  4. JWT 保证的是改不了不是看不见,改了内容签名就对不上,签名密钥一旦泄露任何人都能伪造身份
  5. JWT 最大的硬伤是签发了就收不回,服务器只验签名和过期不查库,旧 token 没过期就一直有效
  6. 用户改了密码 JWT 旧 token 照样有效,因为验 token 时根本不会去查密码改没改,这是事故的根
  7. 要能踢人下线就别和无状态死磕,用短命 access token 加存在服务端可作废的 refresh token
  8. token 黑名单能立刻作废但等于给 JWT 加回了服务端存储,承认纯无状态是有代价的
  9. 强需求随时踢人改权限即时生效的后台金融系统,Session-Cookie 比硬上纯 JWT 省心得多
  10. 生产环境最稳的不是纯方案,是 JWT 做 access token 加服务端 refresh token 的折中组合

总结

这一趟把 Session 和 JWT 彻底理清的过程,纠正了我一个关于"先进"的、藏得很深的错觉。在我做技术选型的时候,我脑子里其实有一条没说出口的鄙视链:Session-Cookie 是"老"的、"传统"的方案,它要在服务器上存东西、要查库、在分布式下还得搞个共享存储,处处透着一股笨重和过时的气息;而 JWT 是"新"的、"现代"的方案,无状态、不用存、天然适合微服务——文档里、博客里,人人都在夸它。所以我选 JWT,与其说是"选型",不如说是一种下意识的"站队":我站在更新的那一边,并理所当然地以为,新的那个,就是更好的那个、就是没有短板的那个。我把 JWT 的所有优点都看在眼里,却从没认真问过一句:它为了这些优点,到底放弃了什么?直到那个踢不掉的盗号者,用一个我无能为力的现实,狠狠地教了我:JWT 的"无状态",根本不是一个白来的优点——它是一个【交易】。它用"服务器不用记住你"换来了轻盈和分布式友好,但同一笔交易的另一面是:服务器既然不记得你,它当然也就【无法忘掉你、无法作废你】。"不用存储"和"无法作废",根本是同一件事的正反两面,是被同一个"无状态"死死焊在一起的。我之前只贪婪地盯着那个"优点"的正面,却对焊在它背面的那个"代价",视而不见。复盘到最深,我意识到我犯的错,是把"权衡"误读成了"进步"。我一直以为 Session 到 JWT,是一条从"落后"到"先进"的单向升级路——好像 JWT 是 Session 的一个更好的版本,全面替代它。可真相是,它俩根本不在一条"进步的直线"上,它俩是一道【选择题】的两个选项:你是要"服务器替你记着、于是它能精确掌控你"(Session),还是要"你把身份自己带着、于是服务器轻松但管不住你"(JWT)?这两个选项,各自服务于不同的需求,各自付出不同的代价,谈不上谁先进、谁落后。我那条自以为是的"鄙视链",纯属虚构。这个教训,我后来在技术世界里到处都看见它的影子:NoSQL 不是 SQL 的"升级版",它是用"放弃强一致和关系"换"扩展性";微服务不是单体的"升级版",它是用"运维复杂度"换"团队和部署的独立";异步不是同步的"升级版",它是用"代码心智负担"换"吞吐"。它们全都不是"新的打败旧的",而是"一种代价,换另一种代价"。可"新"这个字,实在太有迷惑性了——它太容易让人误以为,新的那个,是把旧的所有好处都继承下来、再额外白送你一些好处。这世上没有这种好事。这次最大的收获,是我给自己立了一条新规矩:每当我要采用一个被夸为"更先进"的方案时,我不再只问"它好在哪",我会逼着自己,把那个问题反过来再问一遍——"它为了好在那里,【放弃了】什么?那个被它放弃的东西,恰好是我这个系统【离不开】的吗?"JWT 那个收不回的令牌教给我的,从来不是一个 refresh token 的拼装技巧,而是一句朴素到我用了很多年才真正听懂的话:技术选型里,几乎没有"更先进"这种东西,只有"我更需要哪一种好处、更付得起哪一种代价"。每一个让你心动的"优点"背面,都焊着一个你必须看清的"代价";你真正要选的,从来不是那个优点最闪亮的,而是那个代价你最承受得起的。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

RAG 检索增强完全指南:让大模型回答你的私有知识

2026-5-21 12:10:12

技术教程

大模型 Function Calling 完全指南:让 AI 真正能干活

2026-5-21 12:21:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索