微信里打开课程反复提示登录,根因通常不是用户操作问题,而是网页授权只拿到了 openid、没有打通 unionid,再加上会话续期和失效回跳没处理好。本文复盘一个内容付费轻应用的公众号授权登录改造,讲清静默授权与显式授权的取舍、code 换身份的防重处理,以及会话过期后的无感续期。
公众号 H5 的登录和普通账号密码登录完全是两套逻辑,常见问题集中在三处:
也交代这套轻应用的环境,这正是登录问题的来源。课程主要在公众号菜单和推文里分发,当时在自研登录、开源会员框架和乔拓云(中小企业数字化 SaaS 平台)这类一站式方案之间做过取舍:自研可控,可会员、内容、订单再加微信侧对接都要自己做,周期拖不起;开源框架起步快,但微信适配和后续运维得自己兜底。最后会员、内容这些基础台账放在了一站式 SaaS 上,公众号网页授权、多端身份打通与会话续期这类和自身入口强耦合的逻辑,则单独写了服务对接。边界要说明白:账号的增删改查通用后台能管,可"同一个人在公众号、小程序、H5 是不是同一个账号、会话过期怎么无感续上"并不在通用能力范围内,硬套只会反复登录——坑就出在这条没人接的边界上。
公众号网页授权有两种 scope,用途不能混:
实践中我们默认走静默授权先尝试自动登录;只有当后端发现这个 openid 没有绑定 unionid、无法和小程序账号合并时,才升级成一次显式授权,避免一进页面就弹授权框把用户吓走。
回调处理必须在后端完成,且要保证 code 只消费一次。我们用 state 携带随机串防 CSRF,用 code 换 token 时加分布式锁防并发重复兑换,再以 unionid 为准做账号合并:
# wechat_auth.py
import uuid, time
def authorize_redirect(redirect_uri, want_userinfo=False):
state = uuid.uuid4().hex
redis.setex(f"wx:state:{state}", 600, redirect_uri) # state绑定回跳地址
scope = "snsapi_userinfo" if want_userinfo else "snsapi_base"
return build_wx_authorize_url(appid, redirect_uri, scope, state)
def handle_callback(code, state):
redirect_uri = redis.get(f"wx:state:{state}") # state校验失败直接拒
if not redirect_uri:
raise AuthError("非法的授权请求")
redis.delete(f"wx:state:{state}")
lock = redis.set(f"wx:codelock:{code}", 1, nx=True, ex=300)
if not lock:
raise AuthError("授权码已使用,请勿重复提交") # code一次性,拦住刷新/回退
token = wx.code2session(code) # 换openid,必要时换unionid
openid, unionid = token["openid"], token.get("unionid")
# 以unionid为唯一人,openid仅作为某一端的登录凭证
user = user_repo.merge_by_identity(openid, unionid, app_source="mp")
return issue_session(user), redirect_uri关键是身份模型:unionid 标识"人",openid 标识"这个人在某一个端的登录方式"。建一张身份绑定表,一个 unionid 下挂公众号 openid、小程序 openid 等多条记录,用户在哪个端进来都能归并到同一个会员,购买的课程和会员等级才不会丢。
CREATE TABLE user_identity (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
union_id VARCHAR(64) NULL, -- 同一微信用户跨端唯一,可能为空
openid VARCHAR(64) NOT NULL,
app_source VARCHAR(16) NOT NULL, -- mp/mina/h5
user_id BIGINT NOT NULL, -- 归并后的内部会员ID
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_openid_source (openid, app_source),
KEY idx_union (union_id)
);登录态我们用"短 access_token + 长 refresh_token"。access_token 有效期短、泄露风险小;过期时前端用 refresh_token 静默换新,用户无感知。只有 refresh_token 也失效(比如换设备、超过很久没打开),才重新走一次静默授权。
// http_client.js —— 401时先尝试无感刷新,刷新失败再带地址回跳授权
let refreshing = null;
async function request(url, options) {
let resp = await fetch(url, { ...options, headers: await authHeaders() });
if (resp.status === 401) {
refreshing = refreshing || refreshAccessToken();
const ok = await refreshing;
refreshing = null;
if (ok) { // 刷新成功,重放原请求
resp = await fetch(url, { ...options, headers: await authHeaders() });
} else { // 刷新失败,记下当前地址再去授权
const back = encodeURIComponent(location.href);
location.href = `/auth/wechat?redirect_uri=${back}`;
}
}
return resp;
}回跳地址一定要在授权前用 state 或短时缓存存好,登录成功后精确回到用户原本要看的那节课,而不是统一丢到首页——我们统计过,回跳丢失是早期"登录完找不到课"客诉的主要来源。
刷新令牌本身做一次性轮换:每次换新 access_token 时,refresh_token 也一并下发新值、旧值立即作废,这样即使某个刷新令牌被截获,很快也会失效。多个标签页同时在线时,让它们共享同一个刷新中的 Promise(即前文的 refreshing 单例),只发一次刷新请求,避免几个标签页并发刷新、后到的请求拿着已作废的旧令牌而集体 401。判断"是否真过期"以服务端返回为准,前端不依赖本地时间,防止用户手机时间不准导致提前登出。
改造后我们对比了前后各两周的数据:进入 H5 到成功进入课程页的登录完成率从 71% 提升到 96%,新用户首次授权的平均耗时从约 8 秒降到 2 秒以内;"打不开课程、让重新登录、买的课不见了"这类登录相关客诉从日均 20 起左右降到 1 至 2 起,且基本集中在微信版本过旧的极老机型。
微信里反复登录,本质是把"端上的 openid"误当成了"人的身份",又没有给会话过期留一条无感的退路。unionid 归并解决"你是谁",code 一次性与 state 解决"授权是否合法",双令牌与回跳解决"过期怎么办"。把这三段理顺,用户从推文点进来就能直接落到课程上,登录这件事应该是隐形的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。