☰
小程序登录态架构设计:code2session、双令牌刷新与多端互踢实战
2026/10/6 12:43:07 网站建设 项目流程

做小程序后端,登录态看似简单——不就是调个code2session拿 openid 吗?真正上线后才会发现一堆问题:令牌过期要不要让用户重新登录?刷新令牌被人截获怎么办?同一账号在手机和平板同时登录要不要互踢?存储用 Redis 还是 JWT 无状态?本文把我们项目里踩过坑的一整套登录态方案完整梳理出来,包含可运行的核心代码和 4 个实战踩坑点。

一、整体流程:从 wx.login 到拿到业务令牌

小程序的登录是微信托管的,标准流程如下:

  1. 小程序端调用wx.login()拿到临时登录凭证code(有效期仅 5 分钟,且只能用一次)
  2. 后端拿code+appid+appsecret请求微信code2session接口,换取openid(用户在当前小程序的唯一标识)和session_key
  3. 后端根据 openid 查库或建用户,签发自己的业务令牌(access token + refresh token)
  4. 之后所有业务请求在 header 中携带 access token,后端校验通过即放行

关键点:openid 只在后端用来建立用户身份,绝不能下发给前端当作长期令牌;session_key涉及用户敏感数据解密,要安全存储且不能暴露给前端。

// 后端换码的核心逻辑(伪代码,实际用 WebClient/RestTemplate)publicLoginVOlogin(Stringcode){// 1. code 换 openidCode2SessionRespresp=wechatClient.code2session(appid,secret,code);if(resp.getErrcode()!=null&&resp.getErrcode()!=0){thrownewBizException("微信登录失败:"+resp.getErrmsg());}Stringopenid=resp.getOpenid();// 2. 查库或建档Useruser=userMapper.findByOpenid(openid);if(user==null){user=User.builder().openid(openid).createdAt(LocalDateTime.now()).build();userMapper.insert(user);}// 3. 签发双令牌returntokenService.issue(user.getId());}

二、为什么要用双令牌而不是单个长效 token

最朴素的方案是登录后发一个有效期很长的 token(比如 30 天),简单但危险:token 一旦被截获(日志泄露、抓包、恶意转发),在整个有效期内都可以冒充用户,且无法主动失效。

双令牌方案把"安全性"和"体验"分开:

  • access token:有效期短,建议 2 小时,用于所有业务请求,泄露后风险窗口小
  • refresh token:有效期长,建议 15-30 天,只在 access token 过期时调用刷新接口换新,且必须可被服务端主动作废

用户全程无感知——access token 过期后用 refresh token 静默换新,不需要再走一次wx.login,也不会被踢到登录页。

三、令牌存储:有状态 Redis 还是无状态 JWT

这是争议最多的点。我们的选型是refresh token 存 Redis(有状态),access token 用 JWT(无状态校验),理由:

  • access token 用 JWT,自包含 uid、过期时间,网关层本地验签即可,不用每个请求都打一次 Redis,性能好
  • refresh token 存 Redis,可以做到主动作废、单点登录互踢、登录设备管理——这是纯 JWT 无状态方案的死穴(JWT 签发后在过期前无法撤回)
publicLoginVOissue(Longuid){StringaccessToken=JwtBuilder.builder().claim("uid",uid).expire(Duration.ofHours(2)).signWith(hmacKey).build();// refresh token 用随机串,不携带业务信息StringrefreshToken=SecureRandomUtil.uuid();// key 设计:refresh:{token} -> uid;同时记录登录设备redis.setex("refresh:"+refreshToken,Duration.ofDays(15),String.valueOf(uid));redis.setex("login:uid:"+uid,Duration.ofDays(15),refreshToken);returnnewLoginVO(accessToken,refreshToken,7200);}

四、刷新接口:必须做的旋转和复用检测

刷新不是"拿着 refresh token 换新 access token"这么简单,直接给出我们踩坑后的实现要点:

  1. 令牌旋转(rotation):每次刷新都签发新的 refresh token,旧的立即删除。这样 refresh token 不是长期固定的,截获者拿到一个旧的也用不了
  2. 复用检测:如果一个已经被旋转掉的 refresh token 又被拿来用,说明令牌很可能泄露——此时直接作废该用户的全部登录态,强制重新登录
  3. 刷新接口要限流:同一设备短时间大量刷新属于异常,配合限流防暴力尝试
publicLoginVOrefresh(StringoldRefreshToken){Stringkey="refresh:"+oldRefreshToken;StringuidStr=redis.get(key);if(uidStr==null){// 可能是过期,也可能是在使用"已旋转的旧令牌"LongleakedUid=redis.get("refresh_reuse:"+oldRefreshToken);if(leakedUid!=null){// 命中复用检测:令牌疑似泄露,踢掉该用户全部会话redis.del("login:uid:"+leakedUid);thrownewTokenReusedException("登录态异常,请重新登录");}thrownewUnauthorizedException("refresh token 无效或已过期");}Longuid=Long.valueOf(uidStr);// 旧令牌不是直接删,而是短时记录用于复用检测(如保留 10 分钟)redis.setex("refresh_reuse:"+oldRefreshToken,Duration.ofMinutes(10),uidStr);redis.del(key);returnissue(uid);}

五、多端互踢与单点登录

有些业务要求"同一账号只允许在一台设备登录",新设备登录后旧设备被踢下线。有状态存储让这件事变得简单:签发时维护login:uid:{uid} -> 当前有效refreshToken,每次刷新时校验请求带来的 refresh token 是否等于 Redis 里记录的值,不一致说明已经有更新的登录,拒绝并提示"账号在其他设备登录"。

如果业务允许多端在线但要可管理,则改为login:uid:{uid}下存设备 hash(deviceId -> token),用户可以在"我的设备"里查看和远程下线,后台只需删除对应 token。

access token 是 JWT、无法即时失效的问题怎么解决?两种工程做法:

  • JWT 里带一个loginVersion,用户修改密码、被踢时把 Redis 中用户版本号 +1,网关校验 JWT 版本是否一致(每次请求一次 Redis 查询,可接受)
  • 维护一张短 TTL 的作废清单,被踢用户的 access token jti 放入黑名单(因为 access token 只有 2 小时,黑名单体量很小)

六、前端配合:无感刷新的正确姿势

小程序端建议把令牌放在wx.setStorageSync,并封装统一的请求函数处理 401:

constrequest=async(options)=>{constresp=awaitwxRequest({...options,header:{Authorization:'Bearer '+getAccess()},});if(resp.statusCode===401&&!options._retried){// access 过期,用 refresh 静默换新后重放原请求constnewTokens=awaitrefreshAccess();setTokens(newTokens);returnrequest({...options,_retried:true});}returnresp;};

必须加并发锁:多个请求同时 401 时,只允许第一个去刷新,其余请求等待同一个刷新结果,否则会连发多个刷新请求,触发旋转机制导致互相作废。

七、四个实战踩坑点

坑 1:code2session 没做失败重试和降级

微信接口偶发超时,如果后端直接把错误抛给用户,用户会莫名其妙登录失败。对网络超时类错误做有限次重试;同时要注意区分错误码——40029(code 无效)不能重试,通常是前端重复提交了同一个 code,要在小程序端防重复点击。

坑 2:时钟不同步导致 JWT 大面积失效

服务集群如果有机器系统时间不准,签发的 JWT 在别的机器校验时会出现"还没生效"或"提前过期"。上线前务必确认所有节点开启 NTP 校时;JWT 校验时可以给一个小的 leeway(如 30 秒)容忍微小偏差,但不要给太大。

坑 3:refresh token 明文进了日志和 URL

排查问题时把整个请求参数打日志,refresh token 就进了日志系统,等于白做安全设计。要对令牌做脱敏(只留前后几位),且令牌只放在请求 body 或 header,不要拼在 URL 上——URL 会被网关、代理、浏览器历史记录留存。

坑 4:换绑手机号后登录态没处理

很多小程序支持更换绑定手机号,换绑本质是身份凭证变更,旧设备上的登录态应根据业务策略强制重新验证或全部作废。漏掉这一步,旧手机持有者仍能操作账号,属于账号安全事故。

八、小结

一套可靠的小程序登录态方案可以概括为:wx.login+ 后端code2session建立身份,access token 用短效 JWT 保证性能,refresh token 用有状态 Redis 存储支持旋转、作废和多端管理,刷新接口做令牌旋转与复用检测,前端用带并发锁的无感刷新重放请求。安全性、性能和用户体验三者可以同时兼顾,关键是想清楚每一种令牌"被泄露后怎么办"。

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

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

立即咨询