JWT认证从入门到安全落地:结构、续签、漏洞与SPA实践
2026/9/17 7:34:39 网站建设 项目流程

1. 从一个凌晨三点的告警说起:JWT到底解决了谁的痛

那是几年前的一个电商大促前夜,我们的订单服务在扩容到第六个实例之后,用户开始零星反馈"刚登录就被踢下线"。排查到天亮才发现,认证服务还在用单机内存里的Session存登录态,新扩容的实例压根不认识老实例发的会话ID。那次事故之后,我们把认证方案整体换成了JWT,也踩了后面我会一个个讲到的坑。

JWT,全称JSON Web Token,翻译过来就是"JSON格式的Web令牌"。它本质上是一串用点号分隔的字符串,长得像xxxxx.yyyyy.zzzzz这样。服务端用它来证明"这个请求确实是某个已登录用户发来的",而关键在于——验证这个过程不需要查数据库、不需要共享内存,只要双方约定好一个密钥,任何一台机器都能独立完成校验。这一点决定了它在多实例部署、跨服务调用、前后端分离这些场景里的天然优势。

这篇文章适合三类人看:正在做前后端分离项目、纠结登录态怎么存的后端同学;被"token过期了怎么办"折磨过的全栈同学;以及想搞清楚JWT到底安不安全、漏洞出在哪的架构同学。我会从结构拆解、密钥与算法选型、签发校验的完整代码、续签方案对比、防篡改加固、SPA落地这六个角度,把我这些年踩过的坑和验证过的方案都摊开讲。全程给可运行的代码,参数取值也给理由,不做"你先这么配就行"这种糊弄。

先说一个反直觉的结论:JWT本身不是登录方案,它只是一种"带签名的数据载体"。很多人把它当成Session的替代品直接套用,结果做出一个既不能主动踢人、又不能防止重放的"伪登录系统"。理解它的能力边界,比会调它的API重要得多。

2. 拆开那三段字符串:结构、签名与三个易混概念

2.1 Header、Payload、Signature各自装了什么

JWT的三个部分,用点号隔开。第一部分是Header,第二部分是Payload,第三部分是Signature。

Header是个JSON,描述这个token用什么算法签的,典型长这样:

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

Payload也是JSON,装的是你要传递的业务数据,官方管里面的字段叫"声明"(claim)。声明分三种:预定义声明(比如iss签发者、exp过期时间、sub主体、iat签发时间)、公共声明(自己定义但语义通用的,比如role)、私有声明(业务专用的,比如userIdtenantId)。

Signature是把前两段Base64URL编码后的字符串拼起来,加上密钥,用指定算法算出来的。以HS256为例,公式大致是:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

这里有个新手最容易犯的误解:Header和Payload只是Base64URL编码,不是加密。Base64URL做的是"把二进制转成URL安全的字符",任何人都能解码还原出原文。所以Payload里塞手机号、身份证、密码这类敏感信息,等于当街喊出来。

2.2 编码、签名、加密,三件事别混为一谈

我见过不少同学把"JWT是加密的"当成常识,面试时也这么说,这就危险了。

  • 编码(Encoding):Base64URL 属于编码,目的是让数据能安全地放进URL和HTTP头,谁都能解,没有任何保密性。
  • 签名(Signing):用密钥和算法生成Signature,目的是防篡改——谁改了Payload,签名就对不上。但它不隐藏内容。
  • 加密(Encryption):JWE(JSON Web Encryption)才涉及加密,能把Payload真正隐藏起来。日常业务里用JWS(就是普通JWT)的场景占绝大多数,用JWE的很少。

所以一个准确的表述是:标准JWT保证的是完整性和来源可信,不保证机密性。要机密性,要么别往Payload里放敏感数据,要么上JWE,要么自己再对敏感字段做一层对称加密。

2.3 签名到底怎么防篡改:一次手工验算

光说原理容易虚,我们手工算一遍,你就能彻底记住。

假设Header是{"alg":"HS256","typ":"JWT"},Payload是{"userId":1,"exp":1735660800}

第一步,各自Base64URL编码:

header -> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 payload -> eyJ1c2VySWQiOjEsImV4cCI6MTczNTY2MDgwMH0

第二步,拼成header.payload,用HMAC-SHA256加密钥算出签名,再Base64URL编码:

Signature -> 3Xk9... (一段44字符左右的串)

最终token就是三段拼起来。现在攻击者把Payload改成{"userId":999,...},想把自己伪造成999号用户。他一改,第二段变了,但他没有密钥,算不出对应的新签名。服务端拿着新前两段和旧签名一对,HMAC结果不匹配,直接拒绝。

注意:整个防篡改能力完全押在密钥保密上。密钥一旦泄露,攻击者可以伪造任意用户的token。这就是为什么后面要专门讲密钥管理。

从这个手工过程能看出一个关键点:服务端校验时必须用原始字符串重新算签名,不能先用库把token解析成对象、再自己拼回去算。因为解析再序列化,字段顺序、空格、转义都可能变,签名必然对不上。这是排查"签名校验莫名失败"时的经典陷阱,后面第7节会再展开。

3. 密钥、算法、过期时间:配置里最容易埋雷的三处

3.1 HS256还是RS256:对称与非对称的取舍

选算法是JWT配置的第一个岔路口。

HS256是对称算法,签发和校验用同一个密钥。特点是快、实现简单,适合单体应用或者签发方和校验方是同一套系统的场景。

RS256是非对称算法,私钥签发、公钥校验。特点是校验方拿不到私钥也能验签,适合多服务共用认证的场景——认证中心持私钥签发,其他业务服务只拿公钥校验,即使某个业务服务被攻破,也伪造不出token。

我把两者的对比列清楚,方便你拍板:

维度HS256RS256
密钥类型对称,单密钥非对称,公私钥对
签发速度慢一些(约慢数倍)
校验速度慢一些
密钥分发风险高,校验方也拿到签发密钥低,校验方只拿公钥
密钥长度建议≥32字节RSA建议2048位以上
适用场景单体、内部服务多服务、第三方对接、开放平台

我的经验是:只要校验方不止一个,或者校验方不完全可信,就上RS256。别为了那点性能在安全上偷懒。如果确实用了HS256,密钥长度必须够——HS256的密钥建议至少256位(32字节),别用"secret123"这种。判断标准很简单:密钥的熵要远超暴力破解的成本。

3.2 Payload里什么该放、什么绝对别放

Payload的体积也要控制。因为token要在每个请求的Header里带上,太大就会白白吃掉带宽——HTTP头通常有8KB限制,JWT放在里面,超了直接报错。

该放的:用户ID、角色、租户ID、会话标识(jti)、过期时间。这些字段体积小、校验时常用。

不该放的:

  • 密码、密码哈希:一旦泄露直接完蛋。
  • 手机号、身份证、银行卡:Base64能解,等于明文。
  • 特别大的权限列表:塞几百个权限码进去,token体积失控,而且权限一改旧token就过期了,反而更麻烦。
  • 频繁变动的状态:JWT是无状态的,改了Payload里的数据,已签发的旧token不会自动更新。放进来的动态数据只会让你更头疼。

有个折中做法:Payload里只放userIdrole这类"变化不频繁"的字段,权限细粒度控制放到服务端缓存里查。这样token小,权限调整也能生效。代价是校验时多一次缓存查询,但对绝大多数业务来说这点开销完全可以接受。

3.3 密钥管理:把secret从代码里赶出去

我见过最离谱的一次,是把JWT密钥硬编码在Git仓库的config.js里,仓库还是公开的。那套系统的token等于任何人都能伪造。

正确做法:

  • 密钥放在环境变量或配置中心,不进代码仓库。用.env也要确保.env.gitignore里。
  • 不同环境用不同密钥。开发、测试、生产的密钥必须隔离,绝不能让开发密钥能签发生产token。
  • 支持密钥轮换。密钥不是设一次就永远不变的,要有轮换机制。JWT有个kid(Key ID)字段,专门用来标识"这个token是用哪个密钥签的"。服务端维护一个kid -> 密钥的映射,轮换时新token用新kid,旧token仍能用旧密钥校验,平滑过渡。
  • 定期审计。谁动过密钥,什么时候动的,要有记录。

给一个用kid的Header示例:

{ "alg": "RS256", "typ": "JWT", "kid": "2024-11-key-01" }

校验时先读kid,从密钥库里取对应的公钥验签。轮换期可以配置成"新密钥已生效但旧密钥仍可验签30天",30天后旧token自然过期,旧密钥就能下线了。

4. 从零跑通签发与校验:Node与Java两套代码

4.1 环境准备与依赖选择

Node侧用jsonwebtoken(社区里用得最多,API简单)或jose(更现代,支持Promise和更多算法)。Java侧用jjwt(io.jsonwebtoken,集成方便)或 Nimbus JOSE + JWT。

安装:

npm install jsonwebtoken
<!-- Java: jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency>

为什么用jjwt而不是java-jwt?两者都能用,jjwt对Spring生态更友好,文档也更全,我在Spring项目里习惯用它。选型上其实差别不大,关键是别自己手写Base64和HMAC,除非你在学习。手写极容易在padding、字符集、Base64URL这些细节上出错。

4.2 签发token:把参数取值讲清楚

Node版本:

const jwt = require('jsonwebtoken'); function signToken(user, secret) { const payload = { userId: user.id, role: user.role, jti: require('crypto').randomUUID() }; return jwt.sign(payload, secret, { algorithm: 'HS256', expiresIn: '15m', issuer: 'auth.center', audience: 'web.api' }); }

这里几个参数值得说:

  • expiresIn: '15m':有效期15分钟。取值逻辑是——access token要短,因为一旦泄露,攻击者能用的时间窗口就是这个有效期。我一般把access token设成15分钟到2小时,越敏感的系统越短。
  • issueraudience:签发者和受众。校验时一起验,能防止"这个token本来是A系统发的,被拿到B系统用"。跨系统场景必须配。
  • jti:唯一标识,为后面主动踢人预留。

Java版本:

import io.jsonwebtoken.Jwts; import java.security.Key; import java.util.Date; public String signToken(Long userId, String role, Key key) { long now = System.currentTimeMillis(); return Jwts.builder() .subject(String.valueOf(userId)) .claim("role", role) .id(java.util.UUID.randomUUID().toString()) .issuer("auth.center") .audience().add("web.api").and() .issuedAt(new Date(now)) .expiration(new Date(now + 15 * 60 * 1000)) .signWith(key) .compact(); }

4.3 校验:中间件里必须处理的异常分支

校验不是"对/错"两个结果,而是有一堆异常分支要分别处理。下面这个Node中间件把常见分支都覆盖了:

function authMiddleware(secret) { return (req, res, next) => { const header = req.headers.authorization || ''; if (!header.startsWith('Bearer ')) { return res.status(401).json({ code: 'NO_TOKEN' }); } const token = header.slice(7); try { const decoded = jwt.verify(token, secret, { algorithms: ['HS256'], issuer: 'auth.center', audience: 'web.api' }); req.user = decoded; next(); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ code: 'TOKEN_EXPIRED' }); } if (err.name === 'JsonWebTokenError') { return res.status(401).json({ code: 'TOKEN_INVALID' }); } return res.status(401).json({ code: 'TOKEN_ERROR' }); } }; }

两个细节我必须强调:

第一,algorithms: ['HS256']这个白名单是必须写的。如果不写,库会信任token Header里的alg字段,这就给了alg=none攻击的可乘之机,第5节展开讲。白名单等于把"用哪个算法"的决定权从攻击者手里夺回来。

第二,TokenExpiredErrorJsonWebTokenError要分开返回。前者代表"token是真的但过期了",前端应该去走刷新流程;后者代表"token压根不合法",前端应该跳登录页。混在一起返回,前端没法区分,用户体验会很差——过期了却要重新登录,明明可以静默刷新。

4.4 过期时间、时钟偏移与nbf

exp(过期时间)和nbf(not before,生效时间)都是时间戳,单位秒。这里有两个坑:

一个是时钟偏移。分布式系统里各机器时间不完全同步,签发机器的exp到了校验机器可能还没到,或者校验机器觉得已经过期。解决方案是在校验时留一点容差,Node里叫clockTolerance

jwt.verify(token, secret, { algorithms: ['HS256'], clockTolerance: 5 // 容忍5秒偏差 });

另一个是别只依赖exp做业务时效控制。JWT的exp是"绝对时间点",如果你需要"用户30分钟无操作才过期"这种滑动过期,JWT本身做不到,得靠续签机制,下一节讲。

5. Token续签的三种方案:别让用户每15分钟登录一次

5.1 双Token方案:access + refresh

这是目前最主流的方案。核心思路是拆成两种token:

  • access token:有效期短(15分钟~2小时),每次请求都带,用来访问业务接口。
  • refresh token:有效期长(7天~30天),只在刷新时用,用来换新的access token。它不该在每个请求里传来传去

流程是这样的:

  1. 用户登录,服务端同时下发access token和refresh token。
  2. 前端正常请求带access token。
  3. access token过期,接口返回TOKEN_EXPIRED
  4. 前端用refresh token调刷新接口,换回一对新的token。
  5. 刷新接口校验refresh token是否有效,无效就要求重新登录。

为什么这么设计?因为access token频繁在网络上传输,暴露面大,所以有效期要短;refresh token只在刷新时用,暴露面小,有效期可以长。两者配合,既安全又不用频繁登录。

refresh token的存储我强烈建议放HttpOnly Cookie,而不是localStorage,原因是JS读不到它,能有效缓解XSS窃取。第6节详细说。

5.2 滑动续期的并发坑:同一时刻多个请求都去刷新

双Token方案有个几乎必然会踩的坑:并发刷新

场景是这样的:用户在页面上打开了三个标签页,或者一个页面同时发了5个请求。access token在同一时刻过期,这5个请求同时收到TOKEN_EXPIRED,于是5个刷新请求同时打向刷新接口。如果服务端对refresh token做了"一次性使用"(刷新后旧refresh token立即失效,这是安全最佳实践),那么只有第一个刷新请求成功,后面4个拿到的是"refresh token已失效",前端就会误判为登录失效,把用户踢出去。

这个坑我第一次遇到时排查了很久,因为日志看起来就是"refresh token莫名其妙失效了"。

解决方案有两个方向:

前端侧:加刷新锁。用一个Promise作为锁,第一个触发刷新的请求拿到锁去执行,后面的请求都等待这个Promise的结果,拿到新token后再重放原请求。

let refreshPromise = null; async function refreshToken() { if (refreshPromise) return refreshPromise; refreshPromise = doRefresh().finally(() => { refreshPromise = null; }); return refreshPromise; }

服务端侧:宽限期。旧refresh token在刷新后不立即删,而是打上一个"已轮换"标记,保留一个短窗口(比如30秒),在这个窗口内用旧refresh token来刷新,返回同一个新token对,不报错。这样即使并发,大家拿到的结果也一致。

我倾向于两个都做:前端加锁减少无效请求,服务端加宽限期兜底。双保险。

5.3 主动踢人与黑名单:jti配Redis

JWT是无状态的,这是优点也是缺点。缺点就是——你没法主动让一个还没过期的token失效。用户改了密码、管理员封了账号、用户点了"退出登录",那个token在过期前依然能用。

解决方案是引入一个"有状态的补丁":用jti + Redis。

签发时给每个token一个唯一jti,同时往Redis写一条记录,key是jti,value可以是空或者一些元信息,过期时间设成和token的exp一致。校验时除了验签名,还要查Redis里jti是否存在。要踢人,就把对应jti从Redis删掉。

async function verifyWithBlacklist(token, secret, redis) { const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }); const exists = await redis.exists(`jwt:${decoded.jti}`); if (!exists) throw new Error('TOKEN_REVOKED'); return decoded; }

这么做相当于每个请求多一次Redis查询,JWT的"无状态"优势被削弱了。所以要不要用,取决于业务:

  • 对"能主动踢人"没硬要求的(比如内部工具),可以不做,靠短有效期兜底。
  • 对安全敏感的(金融、后台管理),必须做。Redis的这点开销换来的是可控性,值。

还有个更细粒度的做法:不按单个token踢,而是按用户维度维护一个"token签发时间下限"。用户改密码时,记录一个passwordChangedAt时间戳,校验时如果token的iat早于它,就判定失效。这样一次操作能踢掉该用户所有旧token,不用逐个删jti。适合"改密码后全端下线"这种需求。

6. 让篡改无处可逃:签名之外的加固清单

6.1 alg=none攻击:把算法决定权夺回来

这是JWT最经典的漏洞之一,必须讲清楚。

早期不少JWT库在校验时,会读取token Header里的alg字段来决定用什么算法验签。攻击者就构造一个"alg":"none"的token,签名段留空。如果服务端的库支持none算法,并且没做白名单校验,那这个token就会被当成"合法的、没有签名的token"放行。攻击者可以随意伪造任何用户身份。

防御其实就一句话:校验时显式指定允许的算法白名单

jwt.verify(token, secret, { algorithms: ['HS256'] });

Java侧同理,jjwt较新的版本已经默认拒绝none,但老版本要注意。另外还有一类变种攻击:把RS256的token改成HS256,然后用公钥当HS256的密钥去签名。因为服务端如果用的是RS256的公钥校验,而校验逻辑又信任了token里的alg=HS256,它就会拿公钥去当HMAC密钥验证——而公钥是公开的,攻击者能拿到,于是签名就对上了。

同一个防御原则:算法白名单写死,绝不信任token自带的alg

6.2 敏感字段二次加密:给Payload加一层保险

标准JWT的Payload能被任何人解开,这是前面反复强调的点。如果你确实需要传一些敏感标识,又不想上JWE,可以手动对敏感字段做一层对称加密再放进Payload。

const crypto = require('crypto'); function encryptField(plain, key) { const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return Buffer.concat([iv, tag, enc]).toString('base64url'); }

放出的是密文,校验方用同一个密钥解密。这样即使token被截获,敏感字段也不暴露。代价是Payload变大(密文比明文长),以及多一次加解密开销。用不用,看你的敏感程度。

6.3 常见JWT漏洞与修复对照表

把常见的坑整理成表,方便你对照自查:

漏洞成因危害修复
alg=none信任token自带alg,未做白名单伪造任意身份校验时指定算法白名单
算法混淆(RS256转HS256)用公钥当HMAC密钥验签伪造token白名单+按kid取正确密钥类型
弱密钥密钥太短或可猜测暴力破解后伪造密钥熵足够,HS256≥32字节
密钥硬编码secret写死在代码/仓库泄露即沦陷环境变量/配置中心+轮换
无过期时间未设exptoken永久有效必设exp,access短有效期
Payload放敏感数据误解为加密信息泄露敏感字段加密或放服务端
无Token撤销机制纯无状态无法踢人jti+Redis,或iat下限

7. SPA里落地JWT:存储位置的抉择与验证码配合

7.1 localStorage、sessionStorage还是HttpOnly Cookie

前端分离项目最纠结的就是token存哪。三个选项各有优劣:

存储位置XSS风险CSRF风险跨标签页共享备注
localStorage高,JS能读最方便但最危险
sessionStorage高,JS能读不能关闭标签页即清
HttpOnly Cookie低,JS读不到需防CSRF需配SameSite

很多人图省事把access token和refresh token都塞localStorage,这其实是把安全押在"我的代码没有XSS漏洞"上——而现实中很难保证。

我的实践方案是分离存储

  • access token放内存变量(比如Vuex/Pinia的state里),页面刷新后丢失,靠refresh token重新换取。JS能读到,但它有效期短,即使被XSS偷走危害有限。
  • refresh token放HttpOnly Cookie,JS读不到,专门用来刷新,并且配SameSite=LaxStrict防CSRF。

这样即使XSS发生,攻击者能偷到access token(15分钟就过期),但偷不到refresh token,没法长期维持访问。

注意:refresh token用Cookie,一定要配Secure(仅HTTPS)+HttpOnly+SameSite。这三个属性一个都不能少,缺一样防护就打折。

7.2 图形验证码和JWT怎么配合

登录接口是最容易被撞库和暴力破解的入口,加图形验证码是标配。它和JWT的配合流程要理清楚:

  1. 前端请求验证码接口,服务端生成一个图形验证码,返回一张图片(或base64)和一个captchaId
  2. 服务端把captchaId -> 验证码答案存进Redis,设一个短过期(比如2分钟)。
  3. 用户在登录页输入账号、密码、验证码,一起提交,带上captchaId
  4. 服务端先校验验证码:用captchaId从Redis取出答案,比对。无论对错,立刻删除这条记录,防止一个验证码被重复利用。
  5. 验证码通过后,再校验账号密码。都通过,才签发JWT。

这里的关键点是验证码必须先于密码校验,且一次性使用。如果先校验密码,攻击者能通过响应时间或错误信息判断密码对不对,验证码就形同虚设了。另外,登录失败次数要有计数,超限就锁定该账号或该IP一段时间。

// 伪代码示意 String cached = redis.get("captcha:" + captchaId); redis.del("captcha:" + captchaId); // 立即删除,一次性 if (cached == null || !cached.equalsIgnoreCase(inputCaptcha)) { throw new BizException("验证码错误或已过期"); } // 验证码通过,再走密码校验和签发逻辑

7.3 前端拦截器与401处理

前端要统一处理token过期,一般用axios拦截器:

axios.interceptors.response.use( res => res, async err => { const { response, config } = err; if (response && response.status === 401) { const code = response.data && response.data.code; if (code === 'TOKEN_EXPIRED' && !config._retry) { config._retry = true; const newToken = await refreshToken(); // 内部带并发锁 config.headers.Authorization = `Bearer ${newToken}`; return axios(config); // 重放原请求 } if (code === 'TOKEN_INVALID' || code === 'TOKEN_REVOKED') { redirectToLogin(); } } return Promise.reject(err); } );

两个细节:_retry标记防止无限重试;重放请求时要把原始config带上,否则请求体和参数会丢。我见过重放后变成GET请求的bug,就是没带config导致的。

8. 线上排查记录:签名错误与过期错误怎么区分

8.1 一条排查思路:先看时间,再看签名,最后看密钥

线上遇到JWT报错,很多人第一反应是"密钥配错了吧",其实顺序反了。我总结的排查顺序是:

第一步,看错误类型。库一般会区分"签名错误(SignatureException / JsonWebTokenError)"和"过期错误(ExpiredJwtException / TokenExpiredError)"。类型不同,排查方向完全不同。

第二步,确认时间。过期错误先对时间——服务器时间准不准?签发方和校验方时间差多少?exp取值是不是太短?很多"莫名过期"其实是服务器时钟漂移。

第三步,检查签名输入。签名错误的高频原因是对token做了二次处理。比如网关在转发前把token重新拆解又拼回去、或者URL解码时把Base64URL里的-_改了、或者把token存到某个地方时截断了。校验必须用客户端传来的原始字符串,中途任何一次解析再序列化都可能破坏签名。

第四步,才是查密钥。确认验签用的密钥和签发时是不是同一个。特别是配置了kid的场景,要确认取到的是对应kid的密钥,而不是拿错了。

8.2 常见报错对照表

报错信息大概率原因处理方向
SignatureException密钥不一致/中途改过token核对密钥与原始串
ExpiredJwtException已过期/时钟偏差检查exp与服务器时间
MalformedJwtException格式错误/被截断检查三段是否完整
UnsupportedJwtException算法不支持/alg=none配置算法白名单
InvalidClaimExceptioniss/aud校验失败核对签发者与受众配置
签名校验偶发失败多实例密钥不一致统一密钥源/配置中心

最后这张表里,最后一行我单独提一下。多实例部署时如果每个实例加载的密钥来源不一致(比如有的读环境变量,有的读本地文件),就会出现"在A实例校验通过、在B实例失败"的诡异现象,看起来像随机失败。统一密钥源,是分布式场景下JWT最基础也最容易被忽略的一条纪律。

密钥轮换期间,如果你用了kid,还要确保所有实例的密钥库都同步到位了再切流量,否则正在轮换的那一刻会出现一批验签失败。这个经验是我在一次灰度发布时用真实的用户投诉换来的,后来我们把密钥同步做成了配置中心推送,切流量前先确认所有实例拉取成功。

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

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

立即咨询