news 2026/9/30 6:19:16

JWT登录全流程详解:签发、携带、校验、续签与安全实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JWT登录全流程详解:签发、携带、校验、续签与安全实践

用户登录做完了,接下来怎么证明“你登录过”?目前的主流方案就是JWT(JSON Web Token)。这篇文章作为登录流程系列的第二篇,专门把JWT生成token登录这条链路从头到尾捋一遍:token是怎么签发的、前端怎么携带、后端怎么校验、过期了怎么续签,最后再聊聊网上讨论最多的JWT漏洞和防串改机制。不管你是刚接触后端的新人,还是已经在项目里接过登录模块的开发者,照着这篇走一遍,都能把登录态的完整闭环搭起来。

这套东西我前后在好几个项目里落地过,从单体应用做到微服务,踩过的坑不少,特别是token续签和刷新令牌轮换这部分,如果设计阶段没想清楚,后期返工成本很高。所以这篇不只是讲怎么调库生成token,更重要的是把每个环节为什么这么设计讲透,让你拿到自己的项目里能直接落地。

1. 先搞明白:为什么登录态要用JWT

1.1 Session登录的三大痛点

早期做用户登录,最经典的一套方案是Session。用户提交账号密码,服务端验证通过后,在内存里存一份会话记录,生成一个sessionId通过Cookie返回给浏览器。之后浏览器每次请求自动带上Cookie,服务端拿到sessionId去内存里查一下,有记录就算登录。

这套方案在单体应用、用户量不大时确实好用,但等到应用规模上来,三个问题会越来越明显:

一是存储成本。Session是服务端状态,用户量一大,内存不够用,得引入Redis做集中式Session存储,同时要处理过期策略、淘汰策略、序列化方案,维护成本直线上升。

二是多机部署下的会话共享问题。你部署了两台后端服务器,登录请求打到了A机器,session存在A的内存里。下一个请求被负载均衡分到了B机器,B机器没有这个用户的session,直接返回401。解决方案要么做粘滞会话(同一用户的请求固定分发到同一台机器),要么做集中式Session存储,但前者在机器重启或扩缩容时会出问题,后者又回到了存储成本问题。

三是非浏览器端适配困难。移动App、小程序这类客户端根本没有Cookie概念,你得自己约定一套Header传递sessionId的规则,绕了一圈还是回到“前端传一个标识符,后端查状态”的模式,而且跨域场景下Cookie的SameSite、CORS策略还会带来一堆额外配置。

这三个痛点,催生了无状态登录方案的需求:服务端不保存登录态,把用户身份信息直接打包发给客户端,客户端请求时把包带回来,服务端验一下签名就放行。JWT就是这种思路的事实标准。

1.2 JWT解决了什么,又带来了什么新问题

JWT的核心价值是无状态认证。签发的token里直接携带用户ID、角色、过期时间等信息,服务端不需要查库、不需要查缓存,只用签名算法验证token的真伪和时效,就能完成身份认证。

好处很直观:

  • 具备天然的水平扩展能力,任意一台服务器都能验证同一个token,不需要做session共享。
  • 不依赖Cookie,移动端、小程序、SPA项目都能轻松适配。
  • Token里能携带结构化信息,解析出来就能用,减少部分请求的数据库查询次数。

但它也带来一组新问题:token被窃取了怎么办?token怎么主动作废?怎么无感续签?签名密钥怎么保护才能不泄露?这些问题不是JWT的缺陷,而是无状态认证模式的固有代价。所以搞懂JWT,重点不是学会调一个库生成token,而是把这条链路的每个环节都设计清楚。接下来我从token的内部结构开始,把签发、携带、校验、续签这条链路完整走一遍。

2. JWT的三段式结构:token是怎么生成的

2.1 Header、Payload、Signature分别装了什么

一个标准的JWT长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三段字符串用点号分隔。第一段是Header,第二段是Payload,第三段是Signature。

Header声明了这个token的类型和签名算法:

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

Payload是业务声明区,也就是自定义数据区。标准字段包括sub(主题,一般放用户ID)、iat(签发时间)、exp(过期时间)、iss(签发者)、aud(受众)等。你可以额外放username、role这些业务字段。比如:

{ "sub": "10086", "username": "zhangsan", "role": "admin", "iat": 1737100800, "exp": 1737108000 }

Signature是对前两段做的签名,防止内容被篡改,具体机制下一小节展开。

这里必须先强调一个新手最容易忽略的点:Header和Payload只是做了base64url编码,不是加密。任何人都能把这串token拷贝下来解码看到里面的内容。我早期有个项目把用户手机号直接放进payload,结果测试组拿着token一解码,手机号清清楚楚躺在里面。从那以后我就定了一条规矩:payload里只能放不怕被看到的身份信息,像用户ID、昵称、角色等,任何隐私字段都不能进token。

2.2 签名机制到底怎么防篡改

“JWT如何防止数据被串改”是搜索热度非常高的一个问题,说明很多人没把签名机制吃透。我拆开讲。

服务端签发token时,会用密钥对Header和Payload拼起来的字符串做一次签名计算:

signature = HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )

计算出的signature作为第三段拼上去。由于密钥只存在于服务端,客户端拿不到,所以当客户端带着token回来时,服务端重新对Header和Payload做一次相同的签名,再和token自带的签名比对。如果一致,说明内容没有被改过;如果不一致,只有一种可能——客户端修改了Header或Payload中的任何一个字符,导致重算出来的签名对不上。

这就是JWT防篡改的原理:不是加密,而是通过签名保证完整性。

这里有两个细节要特别留意。

一是签名比较要用恒定时间比较,不要用字符串直接equals比较。因为普通比较在遇到第一个不同字符时就返回false,攻击者可以通过统计响应时间差异来逐字猜测签名内容,这就是时序侧信道攻击。好在主流JWT库内部都实现了安全比较函数,比如Java的MessageDigest.isEqual、Node.js的crypto.timingSafeEqual,用库的时候确认它内部走的是安全比较就够了。

二是算法混淆攻击。攻击者把Header里的alg改成none,然后去掉签名段,看服务端是否直接放行;或者把RS256改成HS256,再用公钥作为HMAC的密钥来签名,诱导服务端用错误的方式验签。这类漏洞在安全圈子里出现频率很高,防御办法就是对解码时的算法做白名单限制,绝不能直接信任Header里写的alg。后面安全章节会专门展开。

2.3 HS256还是RS256,别等上线再后悔

签名算法怎么选,直接决定你后面密钥管理和系统扩展的复杂度。

HS256是对称签名,签发和校验用的是同一个密钥。优点是实现简单、计算性能好,适合单体应用、快速原型、没有第三方系统接入的场景。缺点是要严格保护密钥,所有需要校验token的服务都要共享这一个密钥,一旦某个下游服务密钥泄露,整个系统的token都能被伪造。

RS256是非对称签名,私钥签发、公钥校验。私钥只存留在认证服务本身,其他业务服务只拿公钥就能校验token,公钥泄露也不影响安全性。微服务架构、多个业务系统统一接入一个认证中心时,RS256是标准选择。代价是性能略差,密钥管理也更复杂。

选型建议很直接:单体应用、团队规模不大,直接HS256,密钥放环境变量或配置中心,别写死在代码里。微服务架构、有多个系统需要验证token,上RS256,私钥严格放在认证服务,公钥通过配置中心分发。

3. 登录接口落地:从账号密码到签发token

3.1 前置准备:用户表、密码存储与密钥配置

动手写登录接口之前,先把用户表的基础字段确认好。我的习惯是至少包含这几个字段:

  • id:用户唯一标识,永不变更。
  • username:登录账号。
  • password_hash:密码哈希值,绝对不能存明文。
  • status:账号状态,0禁用、1正常。
  • role:角色,用于后续权限控制。

密码存储推荐BCrypt,不要用MD5、SHA-256这类纯哈希。原因是纯哈希算法对相同密码会产生相同哈希值,攻击者拿彩虹表一查就能还原出原文。BCrypt自带随机盐,而且计算速度刻意设计得较慢,能显著拖慢暴力破解的速度。Java生态里Spring Security自带的BCryptPasswordEncoder、Python生态的bcrypt库都直接可用。

密钥配置这块,强烈建议走环境变量或配置中心,避免把密钥提交到Git仓库。之前有个项目就是密钥写死在配置类里,后来代码仓库权限回收不及时,密钥跟着源码一起泄露,第二天就有人伪造出了管理员token。密钥单独管理这件事,真不是小题大做。

3.2 登录核心逻辑与代码实现

我用Java Spring Boot来演示主流程,这也是目前后端最主流的组合之一。核心步骤:接收用户名密码,校验账号存在性和密码,签发token返回。

先定义JWT工具类,负责生成和解析token:

@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.issuer}") private String issuer; @Value("${jwt.access-token-expire-ms}") private long accessTokenExpireMs; private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateToken(Long userId, String username, String role) { Date now = new Date(); Date expireAt = new Date(now.getTime() + accessTokenExpireMs); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setIssuer(issuer) .setIssuedAt(now) .setExpiration(expireAt) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .requireIssuer(issuer) .build() .parseClaimsJws(token) .getBody(); } }

然后写登录Service:

@Service public class AuthService { private final UserMapper userMapper; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; public LoginResponse login(LoginRequest request) { User user = userMapper.findByUsername(request.getUsername()); if (user == null) { throw new BusinessException("用户名或密码错误"); } if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { throw new BusinessException("用户名或密码错误"); } if (user.getStatus() != 1) { throw new BusinessException("账号已被禁用"); } String token = jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); return new LoginResponse(token, user.getUsername()); } }

Controller层只做参数接收和响应返回,不做业务逻辑,这样结构清晰。

这里有两个设计细节值得说明。

第一,用户不存在和密码错误应该返回同一句提示:“用户名或密码错误”。目的是防止账号枚举。如果两种错误提示不一样,攻击者就能通过提示信息判断某个账号是否注册过。同理,注册接口对“手机号已注册”这类提示也建议做模糊处理。

第二,过期时间和密钥这两个参数一定要从配置里读取,不要硬编码。我之前接手过一个项目,access token固定写死24小时过期,用户改了密码之后旧token还能用一天,安全上非常被动。正确做法是过期时间放配置中心,根据业务随时调整。

3.3 token下发后,前端怎么存、怎么带

token签发完,登录流程才走了一半。token怎么返回、前端怎么存储、请求怎么携带,直接决定安全性。

返回结构建议放在响应体,不要只返回裸token,最好带上过期时间,方便前端提前做续签判断:

{ "code": 0, "message": "success", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "expiresIn": 7200, "tokenType": "Bearer" } }

这里的expiresIn建议用秒做单位,避免前端还要猜是毫秒还是秒。

前端存储,我现在的结论是:access token优先放内存,不要放localStorage。localStorage里的数据可以被页面上的任意JavaScript读取,一旦页面被注入恶意脚本,token就被直接拖走。放内存(比如Vue的Pinia、React的全局状态)配合刷新页面后的静默续签,能把风险窗口缩小很多。如果团队暂时改不动存储方式,至少要把XSS防护做扎实,对用户输入做过滤和转义。

请求携带的标准做法是加Authorization头:

Authorization: Bearer <token>

前端Axios拦截器统一处理:

axios.interceptors.request.use(config => { const token = authStore.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });

注意Bearer前缀不能少。很多后端框架在解析Authorization头时,会严格匹配“Bearer ”开头的字符串,少了这个前缀直接解析失败。

4. 请求校验链路:签发token只是开始

4.1 拦截器里的校验顺序

用户登录后,业务接口需要一个统一的校验入口。Spring Boot里常见做法是写一个OncePerRequestFilter或HandlerInterceptor,在业务代码执行前完成token验证。

校验的标准顺序是这样:

  1. 从请求头取出Authorization,确认以“Bearer ”开头,不是则直接拒绝。
  2. 提取token,做格式校验,确认是完整的三段结构、base64url可以解码。
  3. 验证签名,确认token确实由服务端签发且未被篡改。
  4. 校验过期时间exp,过期返回401。
  5. 校验iss、aud等声明是否符合预期。
  6. 解析出用户ID,写入请求上下文,业务代码直接取用。

有人可能会问:先验exp还是先验签名有什么区别?区别很大。exp字段在payload里,而payload未经签名验证前是不可信的。如果先读exp再验签名,等于相信了攻击者可能篡改过的数据。正确的顺序永远是先验签名,再信任payload里的任何字段。

写一个简化版过滤器:

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Claims claims = jwtUtil.parseToken(token); request.setAttribute("userId", Long.valueOf(claims.getSubject())); request.setAttribute("role", claims.get("role")); } catch (ExpiredJwtException e) { response.setStatus(401); response.getWriter().write("{\"code\":40101,\"message\":\"token expired\"}"); return; } catch (JwtException | IllegalArgumentException e) { response.setStatus(401); response.getWriter().write("{\"code\":40102,\"message\":\"invalid token\"}"); return; } } filterChain.doFilter(request, response); } }

注意登录、注册、获取验证码这些白名单接口要放行,不校验token。白名单建议集中在配置里管理,方便后续维护和扩展。

4.2 过期token的优雅处理

token过期是登录系统里最常遇到的情况。服务端返回401后,前端拿到401做什么,这里有很多团队处理得很粗暴:直接调登出接口、清掉本地状态、跳登录页。结果用户正在填一个很长的表单,提交时突然被踢到登录页,表单内容全丢了。

体验好的应用会做静默续签。大致逻辑:前端在响应拦截器里统一判断状态码,收到401时,确认这个请求本身不是登录请求,就去调用refresh接口换一个新的access token,成功后再重放刚才失败的请求。整个过程中用户无感知。

axios.interceptors.response.use( response => response, async error => { const { response, config } = error; if (response && response.status === 401 && !config._retry) { config._retry = true; try { const { accessToken } = await authApi.refresh(); authStore.setAccessToken(accessToken); config.headers.Authorization = `Bearer ${accessToken}`; return axios(config); } catch (refreshError) { authStore.logout(); router.push('/login'); return Promise.reject(refreshError); } } return Promise.reject(error); } );

这个逻辑要实现到位,需要配合下一节的Refresh Token机制。这里记住一个原则:过期token走统一拦截器处理,不要在每个业务接口里各自捕获401,否则行为不一致,会出现一个接口跳登录页、另一个接口毫无反应的混乱情况。

4.3 无状态认证的边界:主动失效怎么办

很多人宣传JWT无状态,但无状态这个特性在某些场景下反而是软肋。三个高频需求就能难倒纯JWT方案:

  • 用户修改密码后,让旧token立即失效。
  • 管理员封禁用户后,让已签发的token立刻失效。
  • 用户退出登录时,让当前token作废。

纯JWT做不到这些,因为token只要没过期且签名正确,服务端就认账。如果不引入服务端状态,唯一能做的就是把access token有效期设短,比如15分钟,让泄露和误用的窗口尽量小。

如果业务确实需要主动失效,常见做法是引入Redis黑名单:签发token时记录一个唯一的jti(JWT ID),需要主动失效时把jti写入黑名单,设置过期时间等于token剩余有效期。拦截器校验时,先查黑名单再验签名。这样做的代价是牺牲了“无状态”,但换来的是“随时可控”,对于管理后台、支付系统这类安全敏感场景非常值得。

5. token续签设计:让登录态稳定延续

5.1 双token机制:access token + refresh token

登录态保活,是JWT方案里讨论最多的话题。最简单的做法是把access token有效期设成7天甚至30天,用户几乎感受不到过期。坏处是token一旦泄露,攻击者的有效窗口太长,风险太大。

为了平衡安全与体验,业界主流方案是双token:

  • access token:有效期短,15分钟到2小时,用于业务请求的认证,泄露影响面可控。
  • refresh token:有效期较长,7天到30天,只在刷新接口中使用,不随业务请求发送,泄露面比access token小得多。

refresh token的核心价值是“长生命周期凭证”和“短生命周期凭证”分离。即便access token被偷,小偷只能在15分钟内冒用,过期后就失效了;而用户手里的refresh token还能继续换新的access token,登录态不受影响。只有当refresh token也过期或被主动吊销时,才需要重新登录。

5.2 刷新接口的实现与轮换策略

刷新token的接口逻辑不复杂,但有几个细节必须处理好。设计如下:

POST /api/auth/refresh Header: Authorization: Bearer <refresh_token>

服务端处理步骤:

  1. 校验refresh token的签名、有效期和必要声明。
  2. 校验这个refresh token是否已经使用过(支持轮换时)。
  3. 校验关联用户状态是否正常。
  4. 签发新的access token和refresh token。
  5. 返回给前端,同时标记旧refresh token已作废。

第2步的“refresh token轮换”是安全实践中的重要一环:每次刷新都签发一个新的refresh token,旧的那一个立即标记为已使用。这样即使某个refresh token泄露,攻击者先用它完成了刷新,之后合法用户的刷新请求会因为“已使用”被拒绝,系统还能借此检测到可能存在的token泄露事件。

我用Python FastAPI风格的代码演示刷新逻辑,重点看Redis的原子性比较:

@app.post("/api/auth/refresh") def refresh(refresh_token: str = Header(..., alias="Authorization")): raw_token = refresh_token.replace("Bearer ", "") try: payload = jwt.decode( raw_token, settings.REFRESH_SECRET, algorithms=[settings.REFRESH_ALGORITHM], options={"require": ["sub", "exp", "jti"]} ) except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="refresh token expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="invalid refresh token") user_id = payload["sub"] # 用 SETNX 原子完成“判断未使用 + 标记已使用” used = redis_client.set( f"refresh_used:{payload['jti']}", "1", px=settings.REFRESH_TOKEN_EXPIRE_MS, nx=True, ) if not used: raise HTTPException(status_code=401, detail="refresh token reused") new_access = create_access_token(user_id) new_refresh = create_refresh_token(user_id) return { "access_token": new_access, "refresh_token": new_refresh, "expires_in": settings.ACCESS_TOKEN_EXPIRE_SECONDS, }

这里有两个关键点。

一是PyJWT库的decode默认不会校验exp字段,你必须通过options={"require": ["exp"]}显式强制它检查,否则签发了带过期时间的token也不会按预期失效。这是非常容易踩的坑。

二是“判断token是否已使用”和“标记已使用”必须是原子操作,不能先SISMEMBER再SADD,否则并发场景下会产生竞态条件,同一个refresh token可能被用两次。用Redis的SETNX就能一次完成。

5.3 过期时间参数怎么定

token的有效期不是拍脑袋定的,要结合业务场景来设计。我给出一个常见参考区间:

token类型建议有效期适用场景
access token15分钟~2小时常规业务接口认证
refresh token7天~30天移动端/Web保持登录
remember me30天~90天勾选“记住我”的长期会话

具体取值建议:

  • 管理后台、支付系统这类安全敏感场景,access token设15分钟,refresh token设7天。
  • 社区、内容类产品,access token可以放宽到1小时,refresh token设30天。
  • 移动端还要考虑用户的不活跃时长。如果用户30天不打开App,refresh token已过期,用户重新打开App就需要重新登录。这个结果可以接受,但提示文案要友好,让用户明白是出于安全考虑。

access token短、refresh token长的组合,本质是用短token降低泄露影响,用长token保障用户体验。如果你的系统对安全要求不那么高,access token设2小时也合理,但不要设成24小时以上,否则token泄露的窗口期太长,后续审计会很被动。

6. 安全实践与高频报错排查

6.1 JWT安全漏洞清单

JWT的漏洞问题长期是社区讨论的热点。我按严重程度从高到低,把最常见的几类问题和对应的防御手段列出来。

第一,弱密钥。签名密钥太短,比如“secret”“123456”,攻击者可以直接拿着字典去爆破签名,一旦爆破出密钥,就能伪造任意用户的token。HS256要求密钥至少256位(32字节),必须随机生成,并且定期轮换。我现在的习惯是密钥走配置中心,每季度强制轮换一次,触发条件统一从配置中心下发。

第二,alg=none攻击。部分JWT库在特定配置下支持把alg设成none,此时签名验证会被跳过。攻击者把Header里的alg改成none,删掉Signature段,就能构造一个没有签名的“合法”token。防御方式是在服务端解码时显式指定允许的算法白名单,并且拒绝none。比如Java的jjwt库,解析时指定parserBuilder()并设置签名密钥,就不会接受none算法。

第三,敏感信息放进payload。再次强调:payload只是base64url编码,不是加密。手机号、身份证号、密码哈希等字段一旦放进token,等同于明文泄露。安全审计时我见过有人把密码哈希放进payload,解码后直接拿到哈希,下一步就能撞库。规矩就一条:token里只放用户ID、角色、昵称这类不敏感信息。

第四,日志泄露token明文。排查问题时图省事,在日志里打印Authorization头或token本体,token就跟着日志进了ELK。一旦日志系统权限失控,等于把未过期的身份凭证拱手送人。日志记录只打用户ID和请求ID,token明文一律不进日志。

第五,不校验iss和aud。如果多个系统共用一个验证密钥,而某个系统只验签名不验iss和aud,那么A系统签发的token可以在B系统使用。这在微服务架构里很常见。每个业务系统验证token时,都应该严格要求iss(签发者)和aud(受众)匹配,确保token只能在其设计归属的系统内使用。

6.2 常见登录报错速查表

开发过程中遇到的登录报错五花八门,我把高频问题整理成速查表,方便你直接对照排查。

报错特征可能原因排查思路
401 Unauthorizedtoken过期、未携带或格式错误看响应体里的code区分过期和无效;确认请求是否带了Authorization头
签名验证失败密钥不一致检查签发和校验是否用同一套密钥;确认环境变量是否被覆盖
payload中文乱码base64url与标准base64混用统一使用URL-safe base64解码
SignatureExceptiontoken被篡改或密钥不匹配从客户端抓原始token,比对签发记录,看payload是否被改动
token在A服务能用,在B服务401多服务密钥不一致或未校验iss/aud检查B服务配置,确认公钥或密钥是否正确加载,确认iss和aud限制
refresh token返回400 invalidrefresh token过期、格式错误或已轮换检查是否已在Redis中被标记为已使用;确认过期时间
前端刷新页面就掉登录access token只放在内存,刷新丢失实现刷新接口配合静默续签;或把access token短暂放入sessionStorage
解码后读取exp字段为空签发时没有设置exp检查签发逻辑是否设置了过期时间;确认jwt库的参数配置正确

特别提醒一个典型的坑:JWT的payload是base64url编码,使用的是URL-safe字符集,和标准base64有差异。前端或服务端如果用了标准base64的库去解码,遇到“+”和“/”字符就会报解析异常。处理token字符串,一律用URL-safe base64解码。

还有一类错误,比如各种“token exchange failed”“access token could not be refreshed”提示,核心都是token刷新链路出了问题。排查时先确认refresh token是否过期、是否被轮换过、请求头格式是否正确,再检查刷新接口本身返回了什么具体错误码。这类问题九成出在token的传递方式或者刷新接口的入参校验上。

6.3 token监控与日志规范

登录模块上线后,token的运行状态需要纳入监控。一套完整的token监控至少包含四个指标:

  • 签发量:单位时间内新签发的token数,登录瓶颈排查时有用。
  • 校验失败率:401返回占比,异常攀升意味着可能有攻击或配置错误。
  • 刷新成功率:refresh接口的成功率,骤降说明续签逻辑有问题。
  • 过期分布:token失效时段的分布,方便调整有效期参数。

日志侧,记录每次登录成功、登录失败、token校验失败时,用脱敏后的用户ID或请求ID做关联,token明文和密码痕迹一律不落日志。很多公司在做安全复盘时发现,数据泄露的源头不是数据库而是日志系统,这个教训不值得再踩一遍。

这套JWT登录的链路走到这里就完整了。从token的内部结构、签名防篡改原理,到登录接口签发、拦截器校验、双token续签,再到安全漏洞和报错排查,每一个环节都是我在实际项目里反复验证过的。如果你正在做登录功能,我建议先别急着写代码,把“签发、携带、校验、过期、续签”这条链路画一张流程图,把每个环节的异常分支想清楚,再动手实现。等用户量上来之后再回头改登录态的架构,那个代价就不是加班几天能解决的了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 6:19:06

CSS中Base64背景图的正确使用场景与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:19:04

QNX内存分析实战:pmap与pidin排查内存泄漏

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:19:02

前端图片模糊全解析:从CSS缩放、DPR到工程化规范

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:18:42

蓝队复盘模板:从扯皮到闭环的结构化防守资产

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:17:25

RJ45墙插线序错误导致千兆降速的物理层真相

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 6:17:05

海光入局嵌入式CPU:C86架构如何破解国产化迁移生态难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华