在微信公众号 H5 项目里,登录态管理经常被简化成一张表、两个字段,例如 openid 和 token。但当需求突然要求获取用户手机号、运动步数等敏感数据时,后端很容易拿网页授权返回的 access_token 去当成 session_key 使用,结果在 AES 解密阶段直接报错。要理清这条链路,必须先确认一个核心问题:公众号网页授权接口本身不会返回 session_key,真正的 session_key 来自微信登录类接口,例如小程序登录的 code2Session。理解了这一点,后面的有效期管理和数据解密才不会走偏。

一、网页授权和 session_key 的接口差异
公众号网页授权走的是 OAuth2.0 流程。用户从微信客户端打开 H5 页面,前端引导跳转到微信授权页,用户点击同意后,微信会携带一个临时 code 回到业务页面。此时后端需要用这个 code 换取网页授权凭证,调用接口为 https://api.weixin.qq.com/sns/oauth2/access_token。该接口的返回字段主要有 access_token、expires_in、refresh_token、openid 和 scope 等。
其中 access_token 的有效期为 7200 秒,refresh_token 长期有效但也会因用户取消授权等情况失效。这两个凭证的用途是通过 sns/userinfo 拉取用户昵称、头像、性别和地区等信息。它的设计目标很明确,就是解决 H5 页面获取用户基本资料的授权问题,并不承担敏感数据解密能力,因此接口返回里根本没有 session_key。
而 session_key 通常出现在微信小程序登录流程中。小程序前端调用 wx.login() 获取 code,服务端再调用 jscode2session 或新版 code2Session 接口,返回 openid、session_key 和可选 unionid。它是一把会话密钥,用来解密 encryptedData 中的手机号、微信运动数据。换句话说,如果把网页授权凭证和会话密钥混在同一套缓存里,后续一旦需要解密,就会拿错密钥。
二、session_key 的有效期与失效场景
微信官方对 session_key 并没有像网页授权 access_token 那样给出固定 7200 秒的有效期说明。实际开发中可以把它理解为一个动态密钥:一次登录生成一个值,遇到重新登录、修改密码、账号风控、客户端缓存被清除等动作,都可能提前失效。官方文档也没有承诺新旧密钥可以长期并存,因此后端不能只用一个固定的过期时间来判断是否需要重新登录。
常见做法是把 session_key 放在 Redis 中,按 openid 或业务用户 ID 建键。可以设置一个经验 TTL,例如 2 小时到 6 小时,同时配合解密失败处理。也就是说 TTL 只是缓存兜底,不是密钥真实有效期。真正的有效性要由解密结果来验证:如果解密成功,说明当前 key 仍然可用;如果解密时报 padding 错误、JSON 解析错误或微信接口返回 code 无效,就应当返回给前端一个明确的登录过期码,让用户重新发起授权或登录。
下面是一个极简的缓存与读取示例,用于说明 keys 的保存和查询逻辑:
import time
import redis
cache = redis.Redis()
def save_session(openid, session_key, ttl=7200):
cache.set(f"wx:mp:session:{openid}", session_key, ex=ttl)
cache.set(f"wx:mp:session:{openid}:ts", int(time.time()), ex=ttl)
def get_session(openid):
key = cache.get(f"wx:mp:session:{openid}")
if not key:
return None, "SESSION_EXPIRED"
return key.decode('utf-8'), None
这段代码里并没有把网页授权 access_token 和 session_key 放在同一个键名空间。对于同时运营公众号和小程序的业务,建议将 H5 凭证保存为 wx:h5:token:{openid},小程序会话密钥保存为 wx:mp:session:{openid}。即使同一个用户在两端的 openid 相同,也要严格隔离,避免误读。
三、AES-128-CBC 解密用户数据的完整过程
微信敏感数据如手机号、运动步数,在客户端获取后会得到 encryptedData 和 iv 两个字段。服务端解密的密钥就是登录时拿到的 session_key。算法固定为 AES-128-CBC,填充方式为 PKCS#7。第一步先对 session_key、iv 和 encryptedData 分别做 Base64 解码,第二步用解码后的 key 和 iv 初始化 AES 解密器,第三步去掉填充并解析 JSON。
Python 示例如下,主要依赖 pycryptodome:
import base64
import json
from Crypto.Cipher import AES
def unpad(data):
pad_length = data[-1]
return data[:-pad_length]
def decrypt_user_data(encrypted_data, iv, session_key):
# 先做 Base64 解码
encrypted_data = base64.b64decode(encrypted_data)
iv = base64.b64decode(iv)
session_key = base64.b64decode(session_key)
# AES-CBC 解密
cipher = AES.new(session_key, AES.MODE_CBC, iv)
padded_plaintext = cipher.decrypt(encrypted_data)
# 去掉 PKCS#7 填充
plaintext = unpad(padded_plaintext)
# 微信返回的是 UTF-8 JSON
return json.loads(plaintext.decode('utf-8'))
解密后的 JSON 通常包含 phoneNumber、purePhoneNumber、countryCode 以及 watermark。其中 watermark.appid 是数据所属应用的标识,正式环境一定要校验它和当前应用的 appid 是否一致。缺少这一步,一旦请求参数被串用,就可能把 A 用户的数据用 B 的 key 解出来,或者解出其他应用的数据。
解密失败时有几个高频原因。第一是 session_key 已经过期,客户端没有重新登录;第二是 iv 和 encryptedData 不匹配,常见于前端异步请求中参数被覆盖;第三是使用了 H5 网页授权的 access_token 替代 session_key,直接导致密钥长度错误。后端返回给前端时应区分参数错误和登录过期两种情况,否则客户端无法判断是否需要重新弹授权。
四、公众号 H5 里拿不到 session_key 时怎么处理手机号
如果业务确实建立在公众号网页里,又必须获取用户手机号,不能指望网页授权接口直接返回可解密的 encryptedData。目前相对稳妥的方式是借助同主体小程序:在 H5 页面引导用户点击按钮跳转小程序,小程序内发起手机号快速验证组件,获取 code 后由服务端调用 phonenumber.getPhoneNumber 接口,拿到的手机号直接是明文,不需要再用 session_key 做 AES 解密。这里依赖的是小程序的接口调用凭证 access_token,和网页授权 access_token 也不是同一个东西。
另一种场景是公司已经通过小程序登录保存了用户的 session_key,H5 端只需要复用这个会话。此时可以通过 unionid 关联同一个微信用户,但前提是公众号和小程序都绑定在同一个微信开放平台账号下。服务端在解密前必须确认这个 encryptedData 来自小程序端,并且使用的是该用户最新一次登录生成的 session_key。如果嫌麻烦,可以在后端统一封装一个解密服务,输入 unionid 和密文,内部自己查找对应小程序的 session_key。
最后回到会话管理本身,最容易被忽视的不是加解密算法,而是凭证角色混乱。网页授权的 access_token 只做资料读取,小程序登录的 session_key 只做解密和会话校验。二者可以共存于同一个用户体系,但在存储、缓存键名、过期策略、重置时机上都要分开设计。把这两个概念分清楚,后续排查登录态问题会少走很多弯路。
微信网页授权session_key用户数据解密修改时间:2026-09-29 04:30:26