☰
JWT原理与实战:从Token续签到漏洞防御的完整指南
2026/10/9 3:37:43 网站建设 项目流程

做后端这几年,JWT(JSON Web Token)几乎是绕不开的东西。前后端分离的项目、App 接口、微服务网关鉴权,到处都有它的身影。我也见过不少团队把 JWT 用得“能用就行”,一旦出问题就抓瞎:token 不知道怎么续签,漏洞被人利用,SPA 里验证码和 token 配合得乱七八糟。这篇博文就围绕 jwt 学习这条主线,把原理、漏洞总结、续签方案和 SPA 实战一次讲透,希望能帮你在实际项目里少踩几个坑。

这篇文章适合这几类人看:刚接触 JWT、想系统搞懂它原理的后端开发;在前后端分离项目里被 token 续签和登录校验折磨过的同学;以及想避开 JWT 常见漏洞、提升接口安全性的团队。我会把自己实际踩过的坑和验证过的配置方案都写出来,看完可以直接拿去用。

1. 为什么大家都在学 JWT:先从它解决的问题说起

很多新手学 JWT 上来就抠 Base64、看签名算法,结果看了半天也不知道这东西到底好在哪。我建议反过来,先搞清楚场景,再回来看原理。

1.1 传统 Session 方案卡在哪

在 JWT 流行之前,主流的登录方案是 Session + Cookie。用户登录成功后,服务器在内存(或者 Redis)里存一份会话数据,生成一个 session_id 返回给浏览器,浏览器把它写进 Cookie,之后每次请求自动带上。服务端拿到 session_id 去查对应的会话数据,查到了就放行。

这套方案在单机时代很舒服,但到了分布式环境就开始头疼了:

  • 用户的请求被负载均衡转发到不同服务器,如果这台机器上没有对应的 Session 数据,用户就被强制下线。解决办法通常是引入 Redis 做会话共享,但这意味着每次请求都要有一次额外的网络开销,而且 Redis 一旦抖动,整个登录体系跟着遭殃。
  • 移动端 App 的请求没有浏览器自动带 Cookie 这套机制,你得手动维护 Cookie 状态,非常别扭。
  • 服务端保存了所有用户的会话状态,内存成本随用户量线性增长。

这些痛点不是不能解决,但解决过程并不轻松。JWT 的流行,本质上就是因为它把“会话状态”从服务端挪到了客户端,让服务器变成一个“无状态”的验证者。

1.2 JWT 的设计思路:把通行证直接发给用户

JWT 的核心思想,我打个比方你就明白了:传统 Session 好比你去健身房,前台工作人员在电脑里登记你的名字,你每次去都报名字,他查系统确认。

JWT 则像是健身房发给你一张带防伪印章的会员卡,卡片上写着你的会员等级、有效期。你每次进门,工作人员只检查卡片上的印章是否合法、是否过期,不用再查后台系统。

对应到技术里,这个“带防伪印章的卡片”就是 JWT,它是一个字符串,由三部分组成:Header(头部)、Payload(载荷)、Signature(签名)。三部分用英文句点.分隔,整体长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

客户端拿到这个字符串后,后续请求在请求头里加上Authorization: Bearer <token>,服务端只需要验证签名,就能确认用户身份并读取卡片上的信息。

1.3 一次完整的 JWT 认证流程长什么样

完整的认证链路大概是这个流程:

  1. 用户输入用户名和密码,前端把数据提交到服务端登录接口。
  2. 服务端校验用户名密码,校验通过后用密钥生成一个 JWT,返回给客户端。
  3. 前端拿到 JWT,把它存到 localStorage、sessionStorage 或内存变量里。
  4. 后续请求,前端在 HTTP 头里带上Authorization: Bearer <JWT>。
  5. 服务端收到请求后,从请求头里取出 JWT,验证签名、校验过期时间,确认合法后从 Payload 里拿用户 ID、角色等业务数据,响应业务请求。
  6. 当 JWT 过期失效,服务端返回 401,前端收到后跳转到登录页或者触发刷新逻辑。

可以看到,服务端在整个过程中不需要保存任何会话数据,只需要记住签发 JWT 的密钥即可。这个设计大大简化了水平扩展的难度,这也是为什么微服务和前后端分离项目特别喜欢它。

2. JWT 核心原理拆解:头部、载荷、签名分别管什么

理解 JWT 原理,关键是吃透它的三段结构。不需要背源码,但要搞懂每一段是干什么的、哪些能改、哪些不能改。

2.1 Header 里到底放了什么

Header 是一个 JSON 对象,通常包含两个字段:

{ "alg": "HS256", "typ": "JWT" }

alg表示签名算法,常见的有 HS256(对称加密)、RS256(非对称加密)、ES256 等。typ固定是JWT,表示这是一个 JWT 类型的 Token。

Header 会被 Base64Url 编码成 JWT 的第一段。注意这里用的是 Base64Url,不是普通 Base64:它把+替换成-,把/替换成_,并且去掉末尾的=。这样是为了让 JWT 能安全地放在 URL 里。

很多初学者会忽略alg字段的作用,但它恰恰是后面我要讲的算法混淆攻击的关键入口。

2.2 Payload:业务数据的正确存放方式

Payload 是 JWT 的第二段,同样是 Base64Url 编码后的 JSON。它里面存放的是业务数据,我建议你把它当做一个“明文可见、只读勿改”的结构。

标准声明里常用的有:

  • sub(Subject):主体标识,通常存用户 ID。
  • iat(Issued At):签发时间,Unix 时间戳。
  • exp(Expiration Time):过期时间,Unix 时间戳。
  • nbf(Not Before):生效时间,在这个时间之前不可用。
  • iss(Issuer):签发者。
  • aud(Audience):受众,表示这个 Token 是给谁用的。

除了标准声明,你还可以加自定义字段,比如role、nickname、avatar。但我要强调一个很多人容易犯的错误:Payload 只是 Base64 编码,不是加密!任何拿到 Token 的人都可以用工具解码看到里面的内容。所以千万别说写入密码、身份证号、银行卡号这类敏感数据。

正确做法是:Payload 里只放不敏感的标识性信息,比如用户 ID、角色等。即使这些信息被看到,也不会直接造成严重后果。

2.3 Signature:安全性的最后一道防线

Signature 是 JWT 的第三段,也是整个 Token 安全性的根基。它是对前两段内容做签名,算法大致是:

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

也就是说,把前两段编码后的字符串用点连接起来,再用指定的算法和密钥做签名。签名的目的是防止有人篡改 Header 和 Payload。

举个例子:攻击者把 Payload 里的role从user改成admin,但由于他不知道密钥,改完后无法重新生成正确的签名。服务端拿到 Token 后用密钥重新算一遍签名,发现和原来的 Signature 对不上,就知道这个 Token 被动过手脚了。

HS256 和 RS256 的区别需要特别说清楚:

  • HS256 是对称算法,签发和验证都使用同一个密钥。实现简单,但密钥一旦泄露,任何人都能签发合法 Token,所以密钥必须严格保密,而且不适合多个服务互相验证的场景。
  • RS256 是非对称算法,签发用私钥,验证用公钥。私钥只在认证服务保存,其他服务只需要拿到公钥就能验证 Token,适合微服务架构。

我给团队的建议是:如果是单体应用或前后端分离,HS256 够用;如果是多个后端服务需要相互校验 Token,直接上 RS256,把公钥分发下去。

2.4 无状态的双刃剑:好处与坑

JWT 的无状态设计带来了好处,也埋了一些坑。

好的一面:

  • 服务端不需要存储会话,水平扩容非常方便,新起一个实例不需要关心会话共享问题。
  • 非常适合移动端和前后端分离项目,只要客户端能存字符串,就能使用 JWT。
  • 网关做统一身份校验非常方便,解析 Token 就能拿到用户身份。

坑也很明显:

  • 无法主动失效。服务端签出去一张 Token,在它过期之前,即使你把用户封号了、把用户密码重置了,已经签发的 Token 依然是有效的。处理方式一般是引入黑名单(Redis 维护失效 Token 列表),但这样做又回到了“服务端保存状态”的老路,只是保存的数据量变小了。
  • 续签是个麻烦事。这个问题非常普遍,后面我单独写一节来展开。
  • Token 大小受限。JWT 里有几个自定义字段还好,如果往里面塞太多东西,每次请求都拖着大字符串走,网络开销会明显增加。

理解了原理和权衡,你在选型的时候就不会盲从“谁火用谁”。

3. JWT 漏洞总结:我在实战中遇到过的高危问题

搜索热词里“jwt漏洞总结”是关注度很高的话题,这部分我把实际工作中遇到过的安全隐患整理一下,每一项都给出攻击方式和防御思路。

3.1 算法混淆攻击

这是 JWT 最经典的高危漏洞。我详细说一下攻击过程:

  • 假设服务端用 RS256(非对称算法)验证 Token,公钥是公开的,私钥在服务端。
  • 攻击者拿到公钥后,把 Token 的 Header 中的alg改成 HS256(对称算法),然后用公钥作为 HS256 的密钥来签一个伪造 Token。
  • 服务端如果只是根据 Header 里的alg字段决定用哪种算法验签,就会用公钥作为对称密钥去验证 HMAC 签名,而攻击者恰好就是用公钥算的 HMAC,于是验签通过,伪造 Token 被当成合法 Token。

这个漏洞在早期一些 JWT 库中是真实存在的。防御方法也很简单:

  • 固定算法,不信任 Header 中的 alg 字段。在代码里写死JWT.verify(token, publicKey, { algorithms: ['RS256'] })这种形式,明确指定允许的算法列表。
  • 在签发时做好算法白名单校验,发现非预期算法直接拒绝。

3.2 弱密钥爆破

这个场景常出现在 HS256 算法下。如果服务端使用的密钥太简单(比如"secret"、"123456"、"password"),攻击者可以收集到一个合法 Token,然后用字典进行离线爆破,猜出密钥后就能任意伪造 Token。

防御方案:

  • 密钥的长度至少 32 个字符,而且要随机生成,不要用一句话当密钥。
  • 定期更换密钥,但更换时需要设计 Token 踢线策略,否则老 Token 会失验。
  • 如果多个环境共用一套密钥,绝对不行,测试环境和生产环境必须分离。

3.3 空签名与 none 算法攻击

有些 JWT 库在早期版本中支持alg: none,即不签名。攻击者把 Header 的alg改成none,把 Signature 留空,如果服务端没有校验算法类型,就可能导致伪造 Token 直接通过。

这个攻击现在已经不常见,因为主流库默认都不支持none,但如果你用的是老版本库,或者自己封装了 JWT 处理逻辑时不加算法校验,还是有中招的风险。防御同样是写死算法白名单,并在代码里显式拒绝alg == 'none'。

3.4 敏感信息泄露

这一点再强调一遍:JWT 的 Payload 是明文,不是加密。我看过有人在 JWT 里放用户密码的 MD5,有人放手机号,还有人在 Token 里直接写身份证号,这等于把敏感信息明文暴露给一段容易泄露的字符串。

最佳实践是:Payload 里只放用户 ID、角色标识这类信息。业务需要更多用户信息时,每次请求从数据库或者缓存里拉取,不要贪图方便把数据塞进 Token。

3.5 过期时间设置过长

有些项目为了省事,把exp设置成一年甚至更长。Token 一旦泄露,被攻击者盗用的窗口就被无限拉长,非常危险。

建议是:

  • Access Token 的过期时间:内部控制台类系统我建议 30 分钟到 2 小时,对外 API 也不要超过 24 小时。
  • 配合 Refresh Token 实现无感续签(下一节细讲),而不是简单粗暴地拉长过期时间。
  • 另外,很多库默认不校验exp,你要确认你使用的库在验签时做了过期时间校验,否则即使设置了exp也形同虚设。

3.6 其他容易忽视的风险

还有一个容易被忽略的点是:不要把 Token 放在 URL 里传。例如:

https://example.com/api/data?token=eyJhbGciOiJIUzI1NiIs...

Token 会出现在网关日志、Nginx 日志、浏览器历史记录里,泄露风险极高。正确的做法永远是放在Authorization请求头里。

另外还有一点:服务端在与用户交互的过程中,如果检测到密码被重置、用户被删除、权限被收回等情况,必须有能力把已签发的 Token 拉黑或踢下线。上文的“黑名单”方案虽然让 JWT 不再完全无状态,但在安全性和用户体验之间,这是务实的取舍。

我把常见的 JWT 漏洞和防御方式整理成一张表,方便你排查时对照参考:

漏洞类型攻击特征核心防御
算法混淆篡改 alg 为 HS256,利用公钥签 Token代码固定算法白名单,拒绝非预期算法
弱密钥爆破采集 Token,离线猜密钥使用 32 位以上随机密钥,定期更换
none 算法删除签名,绕过验签显式拒绝 alg=none
敏感信息泄露解码 Payload 读取业务数据仅存用户 ID 等非敏感标识
过期时间过长长期盗用泄露的 Token短时 Access Token + Refresh Token
Token 放进 URL从日志、历史记录中收集 Token统一放在 Authorization 请求头

4. 实战:JWT 实现 Token 续签的几种方案

“jwt实现token续签”成了热搜词,说明这是大家真正头疼的问题。我总结三种在实际项目里验证过的方案,按推荐优先级倒序说。

4.1 方案一:前端定时刷新

最简单粗暴的续签方式:Access Token 设置较短有效期(比如 30 分钟),前端每隔一段时间(比如 25 分钟)主动调后端刷新接口,获取新的 Token。

实现思路:

  • 前端发两种请求:普通业务请求和刷新请求。普通请求带旧的 Access Token,刷新请求带一个额外的刷新凭证(或者是用户密码)。
  • 定时器在 Token 过期前 5 分钟触发刷新操作,把新 Token 替换掉旧 Token。

这个方案的好处是简单,不需要引入额外组件。但有个硬伤:如果前端页面在后台被挂起,或者用户长时间断网后再回来,定时器可能错过了刷新时机,只能等请求 401 后跳登录页,体验不好。

4.2 方案二:双 Token + Redis(最实用,推荐)

这是目前主流且最稳的方案。思路是引入两个 Token:

  • Access Token:短期有效(30 分钟),用于业务请求鉴权。
  • Refresh Token:长期有效(7 天或 30 天),专门用来换取新的 Access Token。Refresh Token 可以也做成 JWT,也可以是一串随机字符串存储在 Redis 里。

流程如下:

  1. 登录成功后,服务端不仅返回 Access Token,还返回一个 Refresh Token。
  2. 前端业务请求只带 Access Token。
  3. 当服务端发现 Access Token 过期(返回 401),前端不直接跳登录页,而是携带 Refresh Token 去访问/refresh接口。
  4. 服务端校验 Refresh Token 合法性后,生成新的 Access Token 返回,并且可以选择刷新 Refresh Token(滑窗续期)。
  5. 如果 Refresh Token 也过期,或者在 Redis 中不存在(说明已被撤销),则要求用户重新登录。

这里我贴一段 Node.js 服务端续签接口的简化示例(Python 部分见 5.3,这里展示 Node 生态常见的写法):

// refreshToken 为一串随机字符串,登录时生成并写入 Redis app.post('/refresh', async (req, res) => { const { refreshToken } = req.body; if (!refreshToken) { return res.status(401).json({ code: 401, msg: '缺少 refresh token' }); } // 查 Redis 中的 refresh token,判断是否存在且未被撤销 const userId = await redis.get(`refresh:${refreshToken}`); if (!userId) { return res.status(401).json({ code: 401, msg: 'refresh token 无效' }); } // 生成新的 access token(还可以根据具体实现决定是否轮换 refresh token) const newAccessToken = jwt.sign( { uid: userId, role: 'user' }, process.env.JWT_SECRET, { expiresIn: '30m', algorithm: 'HS256' } ); res.json({ code: 0, data: { accessToken: newAccessToken } }); });

用 Redis 存 Refresh Token 的好处是:用户重置密码、封号时,可以直接把这个用户的 Refresh Token 从 Redis 删除,实现真正意义上的“踢人下线”。这弥补了 JWT 无法主动失效的缺陷。

4.3 方案三:无感滑动过期

滑动过期指的是:用户在持续活跃期间,Token 自动续期,永不中断;只有当用户连续不活跃超过一定时长,才强制重新登录。

实现方式有两种:

  • 每次请求回来时,服务端在响应头里带上一个New-Token,前端检测到就替换本地 Token。
  • 服务端把 Token 过期时间设置在 12 小时,但凡用户在 12 小时内有任何请求,服务端就额外签发新 Token 放在响应头,前端静默替换。

滑动过期适合后台管理系统、办公类应用,用户体验很顺。实现时需要注意的是:区分“请求成功”和“请求失败但 Token 过期”的情况,避免在并发请求时出现旧 Token 覆盖新 Token 的竞态。

4.4 续签时的安全细节

无论用哪种续签方案,有两个细节要特别小心:

  • Refresh Token 必须支持撤销。我已强调过,如果不把它存到 Redis,你就无法在发现异常时主动让它失效。这样做的代价是后端起一个 Redis,但收益是登录状态可以随时受控。
  • 不要把 Refresh Token 和 Access Token 存在同一个地方。被攻击者拿到一整份的话,防什么都没意义。实际项目中 Refresh Token 通常放 HttpOnly Cookie(减小 XSS 拿到它的概率),Access Token 放内存变量或 sessionStorage。

5. SPA 项目实战:JWT 与验证码的整合实现

“spa项目开发之jwt验证码实现”这个热词说明很多团队在前后端分离里遇到验证码与 JWT 衔接的问题。这一节我完整跑一遍流程,包括后端逻辑和前端应对。

5.1 为什么 SPA 里一定要做验证码

SPA(单页应用)的登录页通常是一个静态页面,接口完全暴露。如果没有验证码,攻击者可以写个脚本直接对着登录接口暴力撞库,或者用撞出来的账号密码做批量操作。

验证码的目的不是拦截所有自动化攻击,而是提高自动化攻击的门槛——让攻击者必须先破解验证码,才能进行下一轮尝试。配合登录失败次数限制,能大幅降低暴力破解的成功率。

5.2 验证码与 JWT 的配合流程

完整流程可以拆成三步:

  1. 用户进入登录页,前端向后端请求验证码。
  2. 后端生成一张图片验证码,同时生成一个唯一的验证码 ID,把答案存在 Redis 中,设置 5 分钟过期。返回{ codeId, imageBase64 }给前端。
  3. 用户输入内容提交:前端把用户名、密码、codeId、用户输入的验证码一起发给后端。
  4. 后端先根据 codeId 取出 Redis 中的正确答案,比对用户提交的验证码。比对失败,拒绝请求;比对成功,删除这个 codeId(一次性使用),然后继续校验用户名密码。
  5. 用户名密码校验通过后,签发 JWT,返回 Access Token 和 Refresh Token。

注意这个顺序:验证码校验必须放在用户名密码校验之前。原因很明确:如果反过来,攻击者每次请求即使验证码错误,服务端也会先跑一遍用户名密码查询,这本身就会消耗数据库资源,而且可以通过响应时间差异来判断账号是否存在。先校验验证码,不通过立刻返回,就不会暴露任何账号相关的信息。

Python FastAPI 后端的交互逻辑伪代码大致这样:

# 生成验证码 @app.post("/captcha") def create_captcha(): code_id = uuid4().hex captcha_text = random_letters(4) redis.setex(f"captcha:{code_id}", 300, captcha_text) image_base64 = make_captcha_image(captcha_text) return {"code_id": code_id, "image_base64": image_base64} # 登录接口 @app.post("/login") def login(username: str, password: str, code_id: str, captcha_input: str): correct = redis.get(f"captcha:{code_id}") if not correct or correct.lower() != captcha_input.lower(): raise HTTPException(401, "验证码错误") redis.delete(f"captcha:{code_id}") # 一次性使用 user = authenticate(username, password) if not user: raise HTTPException(401, "用户名或密码错误") access_token = create_jwt({"uid": user.id}, expires=1800) return {"access_token": access_token, "token_type": "bearer"}

5.3 SPA 前端如何携带 JWT 请求

SPA 里用 axios 或 fetch 请求接口时,需要在请求头上加上 Token。我推荐用一个统一的 axios 实例和拦截器来管理,不要在页面里到处手写 Header,方便统一维护和异常处理。

前端伪代码:

// axios 实例 const service = axios.create({ baseURL: '/api', timeout: 10000 }); // 请求拦截器:每次请求自动带上 access token service.interceptors.request.use(config => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:遇到 401 尝试用 refresh token 续签 service.interceptors.response.use( response => response, async error => { const { response } = error; if (response && response.status === 401) { // 说明当前 access token 失效,尝试刷新 const refreshToken = localStorage.getItem('refresh_token'); if (!refreshToken) { location.href = '/login'; return Promise.reject(error); } const res = await service.post('/refresh', { refreshToken }); localStorage.setItem('access_token', res.data.accessToken); // 重发刚才失败的请求 const retryConfig = { ...error.config, headers: { ...error.config.headers, Authorization: `Bearer ${res.data.accessToken}` } }; return service(retryConfig); } return Promise.reject(error); } );

这个拦截器有两个关键点:

  • 防止并发刷新:如果同时有多个请求返回 401,拦截器可能会发出多次/refresh请求,造成重复刷新和后端报错。优化做法是把刷新逻辑做成一个单例 Promise,所有请求共用同一个刷新动作。
  • 防止死循环:如果/refresh接口本身也返回 401,说明 Refresh Token 失效,此时必须跳登录页,而不是再次尝试刷新。

5.4 验证码与 JWT 过期时间的配合细节

在 SPA 登录场景里,验证码有效期与 JWT 有效期是两套独立的体系,但有一个细节要注意:验证码过期后,用户第一次输入密码报“验证码过期”时,前端应该自动帮用户重新拉取一张新验证码,而不是让用户手动刷新页面。这个细节通常被忽略,但很影响体验。

另外一个细节是“登录失败次数计数”:如果验证码校验通过但密码错误,我会在 Redis 中对这个账号的失败次数加 1;当连续失败 5 次时,锁定账号 15 分钟。锁定期间不再接收该账号的登录请求。这个计数要和验证码的计数分开,否则攻击者每次先输入错误验证码也能耗掉锁定条件。

6. 常见问题与排查技巧实录

这部分我整理了一份 JWT 实战中的问题速查表,大多数是团队踩过坑之后才总结出来的。

6.1 问题速查表

现象可能原因排查思路与解决方案
接口一直返回 401签名算法不匹配,或密钥不同检查签发和验签的算法是否一致(HS256 vs RS256)、密钥是否完全相同。特别注意测试环境和生产环境密钥分离。
Token 没过期但报 401库未校验 exp,或被时钟偏移(clock skew)影响确认库的配置项ignoreExpiration为 false;将验证容差设为 60 秒以内,不放大。
修改 Payload 后 Token 失效签名被篡改保护,属于正常现象这是预期行为,签名的作用就是防篡改;修改前两段后必须用密钥重新签名。
前端拿到的 JWT 字符串解码乱码用了 Base64 而不是 Base64UrlJWT 中的+、/、=已经被替换和去除,使用标准base64UrlDecode解码,别用base64Decode。
同一终端多次请求出现 Token 偶发失效续签时旧 Token 覆盖新 Token,并发竞态详见 5.3 防止并发刷新,统一刷新入口。
用户退出登录后 Token 仍可用JWT 无状态,无法主动失效服务端维护黑名单(Redis 存失效 Token),或在退出时使 Refresh Token 失效;Access Token 的短过期时间也可以缩小影响窗口。
某台服务器验签通过,另一台失败各实例使用的密钥不一致密钥统一放环境变量或配置中心,不能各写各的。
突然大量登录请求导致 Redis 内存暴涨验证码存储缺少过期清理验证码设置 5 分钟过期,登录失败计数设置 15 分钟过期;用 Redisexpire严格管理。

6.2 几个让我印象深刻的实战教训

第一个教训是关于“密钥管理”的。我曾经接手一个项目,发现代码仓库里直接提交了 JWT 密钥,而且测试环境和生产环境用同一个密钥。这意味着任何一个能看代码的人,理论上都能签发生产环境的伪造 Token。那次整改我把密钥迁到了环境变量和密钥管理服务,并且强制每一个环境使用独立的密钥。

第二个教训是关于“不校验过期时间”的。有个项目因为 JWT 库的默认配置原因,exp一直没生效,导致 Token 永久有效。文档里写了库默认不校验过期时间,但实测容易被忽略。排查时通过解码一个所谓“已过期”的 Token 才发现问题。建议只要是新上加的 JWT,上线前先写一条“过期后访问应返回 401”的测试用例,把这一条纳入自动化测试。

第三个教训是“分类存储 Token”。有前端同事把 Access Token 存到 localStorage 里,XSS 一打,Token 全没了。虽然不能说内存存储就一定安全,但至少风险会小很多。我的建议是:敏感操作环境尽量使用 HttpOnly Cookie 存 Refresh Token,避免大部分 XSS 场景。Access Token 放内存或者 sessionStorage,同时配合 CSP 和其他防护手段。

如果要我给一套最小可用的 JWT 配置参考,大概是这样的方向:

  • 算法:HS256 或 RS256,密钥 32 字节以上;小团队 HS256,多服务场景 RS256。
  • Access Token 有效期:30 分钟到 2 小时。
  • Refresh Token 有效期:7 天到 30 天,存 Redis,支持撤销。
  • Cover 好三个场景:登录签发、请求鉴权、401 自动续签。
  • 每个接口都在网关或统一中间件里校验 Token,不改散装的。

JWT 这东西,说简单很简单,封装好了就是几行代码的事;但要把安全性、续签、验证码全部串起来,还是有几个地方需要想清楚的。我个人在实际操作中的体会是:别把 JWT 当成一个库函数,把它当成一套会话方案来设计。选型之前先想清楚你有没有强制下线需求、有没有续签需求、服务端能不能承载额外的 Redis 成本,之后再动手写代码,踩坑会少很多。

最后再分享一个小技巧:调试 JWT 的时候,可以在 https://jwt.io 上把 Token 粘进去,它会自动解出 Header 和 Payload,方便你快速检查数据是否正常。不过千万要注意,不要把生产环境的真实 Token 贴到陌生网站上,毕竟解码后里面的信息就暴露给你了。更稳妥的办法是在本地用命令行工具解一下,比如echo $PAYLOAD | base64 -d,在本地环境调试足够了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询