小程序登录态设计:session_key 为什么不能下发

admin 微信QQ技巧 2026-10-27 4 0

把 wx.login 当成登录,是小程序开发里最贵的一次误判。它只是向微信要了一张五分钟有效、只能用一次的一次性票据,连「这个人是谁」都没告诉你。真正的身份是后端拿着这张票据去换回来的,而换回来的东西里有一个值一旦泄露,你的用户数据就等于裸奔。这一篇把登录态这条链路上的每个值、每一段时间、每一种失效路径拆开讲,最后给一张表告诉你什么该存在哪里。

wx.login 拿到的 code 到底是什么

它是临时登录凭证,不是身份。特性有三条:只能用一次、短时有效、与当前小程序和当前用户绑定。你在前端拿到 code 之后,唯一该做的事就是把它发给自己后端,其他什么都不用做,也不要在前端保存它。

wx.login({
  success(res) {
    if (res.code) {
      // 只做一件事:发给自己的后端
      wx.request({ url: 'https://your.api/login', method: 'POST', data: { code: res.code } })
    }
  }
})

常见误用是前端多次调用 wx.login 却只用最后一个 code,或者拿到 code 之后重试请求时复用同一个。code 被消费掉就失效,重试必须重新取。

jscode2session 返回的三个值分别意味着什么

后端拿 code 去换,接口是固定的:

https://api.weixin.qq.com/sns/jscode2session
  ?appid=your_appid
  &secret=your_secret
  &js_code=前端传来的code
  &grant_type=authorization_code

返回 openid、session_key,以及满足条件时才会出现的 unionid。

openid

用户在当前这个小程序里的唯一标识。同一个用户在你的两个小程序里 openid 不同,这是它最重要的特征。它稳定、可长期作为业务主键,但它只是「这个用户在这个应用里」的编号,不含任何身份信息。

session_key

会话密钥,用来解密敏感数据、校验数据签名。它由微信生成,会过期,会被新的登录刷新,用户长时间不活跃之后就不可用了。它不是一个可以长期依赖的凭证。

unionid

用户在同一个开放平台账号下所有应用里的统一标识。只有当小程序已绑定到开放平台账号时才会返回。想让多端、多应用认得出是同一个人,只能用 unionid;但不要假设它一定存在,代码里必须有 openid 兜底的分支。

为什么 session_key 绝对不能下发给前端

这是设计上的红线。session_key 一旦到了前端,就意味着它能被任何能读到前端环境的人拿到;而它正是解密手机号、解密用户信息、校验数据完整性的钥匙。拿到它的人可以:

  • 伪造和解密敏感数据,冒充正常客户端提交被篡改的内容。
  • 在 session_key 有效期内离线还原你服务端以为只有自己知道的信息。
  • 把你的整套「靠加密数据保证可信」的假设全部作废。

正确的划分是:session_key 只存在于服务端的会话存储里,和你的自定义登录态一一对应;前端只拿到一个不透明字符串。判断标准很简单——前端代码和网络请求里,不该出现 session_key 这四个字。

自定义登录态的三步

标准做法只有三步,顺序不能变。

  1. 前端 wx.login 拿 code,发给后端登录接口。
  2. 后端用 code 换 openid 与 session_key,落库或写入缓存,然后签发自己的 token(随机会话 ID 或带签名的 JWT),把用户标识绑在 token 上。
  3. 前端只保存这个 token,此后每个请求带上它;后端凭 token 反查 openid 与 session_key。

几个落地细节值得强调:token 用不可猜测的随机串而不是自增 ID;服务端存 token 到会话的映射,并给它设置过期时间;token 与设备信息绑定是可选的,绑了更安全,但要处理好换设备时的重新登录体验;登录接口本身要限流,它是被刷的重点目标。

session_key 会过期,checkSession 判断的是另一件事

wx.checkSession 检查的是「微信侧的登录态是否还有效」,返回成功不代表你后端手里的 session_key 一定还能解密,更不代表你的自定义 token 还有效。反过来,checkSession 失败也不代表你的 token 失效了——它只说明需要重新 wx.login 刷新会话。

所以典型的前端逻辑应该是:在需要解密敏感数据的操作之前,先 checkSession,失败就静默 wx.login 重新换一次;而所有业务请求的鉴权失败,统一按你的 token 失效流程处理(通常是跳登录或静默重登后重放一次原请求)。把这两件事分开,你就不会写出「checkSession 失败就把用户踢出登录」这种把用户赶跑的代码。

敏感数据解密与新版接口的差异

老接口给的是 encryptedData + iv,算法是 AES-128-CBC,key 就是 session_key,IV 是请求里带的 iv,解密后按 PKCS#7 去填充,得到明文 JSON,里面还会带一个水印字段,水印里的 appid 必须和你的 appid 一致,否则说明这段数据不是给你的。

plain = aes_128_cbc_decrypt(base64decode(encryptedData),
                            key = base64decode(session_key),
                            iv  = base64decode(iv))
plain = pkcs7_unpad(plain, block=16)
if (plain.watermark.appid != your_appid) reject()

新版接口的差异在于:手机号这类能力改成返回一个 code,由后端用这个 code 去换具体数据,前端完全不需要接触密文和 session_key;用户信息也改成由用户主动选择头像昵称的形态,不再是「一键静默拉全量资料」。方向很明确:能不解密就不解密,把敏感数据的获取收敛到服务端。你手里如果还有依赖 encryptedData 的老代码,先确认它拿的是哪类数据,再判断能不能换成 code 模式。

多端登录与 unionid 的关系

同一用户在小程序 A 和小程序 B 里的 openid 不同,但 unionid 相同。所以「同一个人的统一账号」这张表的主键应该用 unionid,openid 作为「某个渠道的绑定关系」单独存一张映射表。没有 unionid 的时候(未绑定开放平台或用户未满足条件),只能用 openid 作为主键,同时留好将来合并的字段。

多端登录还有一个容易忽略的点:同一个用户在不同端各自登录,会产生多条自定义 token。要么允许并存、各自有效,要么做单点登录、新登录顶掉旧 token 并给旧端一个明确的失效提示。别做成「既并存又在某些接口上互相覆盖状态」,那会变成偶发的脏数据。

登录态失效的前端处理与要素清单

前端的处理原则是「静默优先,弹窗兜底」:收到鉴权失败,先自动重新 wx.login 换 token,再重放一次原请求,成功则用户无感;如果重放仍失败,才提示需要重新授权或进入登录页。要注意防重入——几十个请求同时失败会触发几十次重登,用一个共享的进行中 Promise 串起来,只发起一次。

要素存放位置有效期
code不保存,取到即发后端一次性,分钟级
openid服务端数据库长期不变
unionid服务端数据库长期不变,可能不存在
session_key仅服务端会话缓存,禁止下发随会话失效、被刷新覆盖
自定义 token前端本地存储 + 服务端映射由你设定,建议短于 session_key

最后一句:登录态的设计目标不是「永远不掉线」,而是「掉线时用户感觉不到」。为此你需要一张明确的失效映射表,和一条只发一次的重登链路。