首页 / 资讯中心 / 文章详情

小程序登录后端:code、session_key 与 token 生命周期

小程序登录后端:code、session_key 与 token 生命周期 ★ FEATURED ARTICLE
接手一个短视频平台小程序项目时登录这块被排在了第一周。当时我以为这事顶多半天小程序端点一下授权按钮后端调个接口换 openid塞进 Redis返回个 token收工。结果真正做下去发现真正的难点根本不在调用接口这四个字上而在这条链路上每一环的有效期、一次性、以及失败后的兜底。同一个 code 被重复提交、session_key 被并发登录冲掉导致解密手机号失败、体验版和正式版 appid 混用导致换出来的 openid 对不上人——这些问题不会在文档的第一页出现但会在上线第一周准时出现。这篇东西写给谁手里已经有一台能跑 Spring Boot 的服务、要给小程序做登录的 Java 后端或者前端已经把tt.login调通了、卡在拿到 code 之后怎么办的同学。我会把整条链路拆开讲清楚每一段的职责边界给出可直接落地的 Java 代码重点放在那些文档里不写、但实际会让人返工两天的地方。抖音小程序只是入口这套思路换到别的小程序平台也基本通用差别主要在接口地址和字段名。1. 登录链路到底由几段拼成从临时 code 到自家 token很多人对这条链路有误解以为登录是一个动作。实际上它是三个独立的小动作串起来的每个动作解决一个不同的问题混在一起想就会乱。1.1 小程序端拿到的永远只是一个一次性临时凭证小程序端能做的事非常有限。它调tt.login()拿到的是一个code这个 code 什么都不代表——不含用户身份、不含昵称、不含手机号它唯一的作用是向服务端证明这次请求来自一个真实的小程序运行环境。这个 code 有三个硬约束必须刻在脑子里有效期极短通常几分钟、只能用一次、和 appid 绑定。用过一次之后再拿它去换服务端接口会直接返回错误用 A 应用的 code 配 B 应用的 appid 去换同样报错。我见过最典型的错误用法是前端把 code 存在 storage 里用户下次打开小程序直接拿旧 code 去请求登录接口结果一直报错还找不到原因。所以前端正确的姿势是每次需要登录时重新调tt.login()拿新 code拿到立刻发给后端不做任何缓存。这一点跟网页端把 token 存 localStorage 反复用的习惯完全相反很容易惯性写错。另外要注意tt.login()本身有失败回调。用户网络抖动、小程序基础库异常都可能触发前端必须有重试逻辑而不能假设它一定成功。1.2 服务端 code 换 session这一步才是真正的认人后端拿到 code 之后带着appid和appSecret去调平台的服务端接口换回来的是一组字段openid、session_key条件满足时还有unionid。这三个字段的含义差别很大搞混了会做出一堆错误设计字段含义是否稳定能否给前端openid用户在该应用内的唯一标识同一应用内永久稳定可以但没必要session_key会话密钥用于解密敏感数据会随登录刷新变化绝对不可以unionid用户在同一开发者主体下多个应用的统一标识需绑定开放平台后才有可以谨慎openid是你做用户表主键关联的锚点它在这个应用内永远不变所以第一次登录时拿它去查库查不到就注册新用户查到了就更新登录时间这是标准做法。session_key不是身份它是密钥只是恰好和身份一起下发。把session_key返回给小程序端是最常见的严重错误——它一旦泄露攻击者可以伪造解密敏感数据等于把保险箱钥匙和保险箱一起寄出去。unionid的坑在于它不一定有。只有在开发者账号下绑定了开放平台、并且小程序也归属该主体时才会返回。代码里如果直接对unionid做非空断言某些情况下会直接空指针。正确的写法是有就用没有就存 null做多应用打通时再考虑。1.3 为什么不能把 openid 当登录态直接下发新手最省事的做法是换到 openid直接把 openid 返回给前端之后每次请求带上 openid 就当已登录。这个方案能跑通但等于没有登录。原因是 openid 是标识而不是凭证。标识可以被猜到、被抄走、被从前端日志里翻出来而凭证应该是不可伪造、有有效期、可主动失效的。openid 一旦泄露别人拿去就能冒充这个用户而且你无法让它过期也没法在用户换设备时强制下线。所以第三步必须存在服务端自己签发一个登录态凭证。这个凭证可以是随机串不透明 token也可以是 JWT两者各有取舍后面第 4 章会细说。要点是这个凭证由你自己控制生命周期能续期、能吊销、能和设备绑定。把这三段串起来的完整时序大概是这样小程序端tt.login()→ 拿到 code → POST 到你自己的/api/auth/login→ 后端调平台接口换openid和session_key→ 查库或注册用户 → 生成自定义 token →session_key存服务端Rediskey 用 openid 或用户 id→ 只把 token 返回前端。前端把 token 存起来之后所有业务请求带在 header 里。2. Java 侧工程落地配置、HTTP 调用与错误分流理论清楚之后落到 Spring Boot 工程里有几处细节决定这套东西是能跑还是跑得稳。2.1 凭证配置别把 appSecret 写进代码和日志appId和appSecret属于最高等级的敏感配置。最低要求是走配置文件加环境变量本地用application-dev.yml线上走环境变量或配置中心仓库里的文件只放占位符。miniapp: app-id: ${MINIAPP_APP_ID:} app-secret: ${MINIAPP_APP_SECRET:} code2session-url: https://developer.toutiao.com/api/apps/v2/jscode2session phone-url: https://developer.toutiao.com/api/apps/v1/get_phone_number connect-timeout-ms: 2000 read-timeout-ms: 3000再配一个强类型绑定类避免满项目Value满天飞Data Component ConfigurationProperties(prefix miniapp) public class MiniAppProperties { private String appId; private String appSecret; private String code2sessionUrl; private String phoneUrl; private int connectTimeoutMs 2000; private int readTimeoutMs 3000; }这里有一条容易被忽略的纪律调用平台接口的请求体和响应体在打日志时必须先把 secret、session_key、code 这几个字段脱敏。我见过排障时顺手把整个响应体log.info出来session_key 就这么进了日志平台后来做安全审计被点出来全部日志还得清理一遍。宁可多写一行替换逻辑也别图省事。2.2 用 RestTemplate 还是 OkHttp先看你的调用量级登录接口的调用特征是低频每用户每次进小程序最多一次、对延迟敏感用户在等、失败必须能区分原因。这个特征下选型的关键不是性能而是超时控制和错误可读性。RestTemplate 胜在和 Spring 生态融合好、配置简单OkHttp 胜在连接池和拦截器更灵活。登录这种量级RestTemplate 完全够用把超时显式设上就行。真正要避免的是用默认构造的 RestTemplate——它默认没有连接超时平台接口一旦抖动你的线程池会被慢慢占满最后整个服务卡死而登录接口是所有人进小程序的第一步它挂了等于全站挂了。Configuration public class HttpConfig { Bean public RestTemplate miniAppRestTemplate(MiniAppProperties props) { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(props.getConnectTimeoutMs()); factory.setReadTimeout(props.getReadTimeoutMs()); return new RestTemplate(factory); } }超时数值我给的建议是连接 2 秒、读取 3 秒。读取时间不要设得太短平台接口偶尔会有 1 秒以上的响应设 500 毫秒会造成大量假失败也不要设太长超过 5 秒用户已经退出小程序了你还在等结果这时候成功也没意义。2.3 code 换 session 的实现与错误码分流新版接口是 POST请求体里放appid、secret、code。响应结构里有一个err_no和err_tips成功时err_no为 0业务数据在data里。Slf4j Service RequiredArgsConstructor public class MiniAppAuthClient { private final RestTemplate miniAppRestTemplate; private final MiniAppProperties props; public Code2SessionResult code2Session(String code) { MapString, Object body new HashMap(); body.put(appid, props.getAppId()); body.put(secret, props.getAppSecret()); body.put(code, code); ResponseEntityString resp; try { resp miniAppRestTemplate.postForEntity( props.getCode2sessionUrl(), body, String.class); } catch (ResourceAccessException e) { // 超时或连接失败属于可重试类错误 log.warn(code2session 网络异常, code{}, mask(code), e); throw new LoginException(LoginError.PLATFORM_TIMEOUT); } JSONObject json JSON.parseObject(resp.getBody()); int errNo json.getIntValue(err_no); if (errNo ! 0) { log.warn(code2session 失败, errNo{}, tips{}, errNo, json.getString(err_tips)); throw new LoginException(mapErrNo(errNo)); } JSONObject data json.getJSONObject(data); Code2SessionResult result new Code2SessionResult(); result.setOpenId(data.getString(openid)); result.setSessionKey(data.getString(session_key)); result.setUnionId(data.getString(unionid)); return result; } private LoginError mapErrNo(int errNo) { // 40029 等属于 code 无效其余按平台侧错误处理 return errNo 40029 ? LoginError.INVALID_CODE : LoginError.PLATFORM_ERROR; } }这段代码里有两个设计决定值得展开说。第一个是把网络异常和业务失败分开。网络异常可以给前端一个请重试的提示业务失败里的 code 无效则应该让前端重新调tt.login()。这两种情况用户看到的话术不同前端处理逻辑也不同全揉成 500 错误前端只能瞎猜。第二个是错误码宁可粗也要准。平台侧错误码会变我不建议把每个码都硬编码成一个中文提示那玩意儿维护成本极高。正确做法是分三类code 无效类、参数配置类、平台异常类。前者让前端重登中者告警给运维后者打日志加兜底。剩下的细节靠日志和 traceId 去查。2.4 收到 session_key 之后立刻做的事拿到session_key后有两个动作要立刻完成顺序不能乱。一是查库或注册用户。用openid做唯一索引先select为空则insert注意别用先查再插这种非原子操作并发首次登录会撞出重复数据。稳妥写法是给openid加唯一索引插入时捕获DuplicateKeyException捕获到了就重新查一次。这个坑在新用户首次进入、小程序冷启动时特别容易触发因为前端有时会并发发两次登录请求。二是把 session_key 落到 Rediskey 设计成miniapp:sk:{openid}value 是会话密钥过期时间设得比 session_key 实际有效期略短一点比如 25 分钟。为什么要缩短因为如果按刚好设可能出现你这边还没过期、平台侧已经失效解密必然失败的窗口期缩短几十秒能大幅降低这种概率。stringRedisTemplate.opsForValue() .set(miniapp:sk: openId, sessionKey, Duration.ofMinutes(25));3. 手机号与用户资料两种授权路径怎么选openid 登录只能解决这个人是谁能免登录、能存数据。但很多业务需要手机号下单、客服回访需要昵称头像社区展示。这部分是第二层授权逻辑和第一层完全不同。3.1 手机号快速验证前端拿 code后端换号码现在主流方式是小程序端用按钮触发手机号授权前端拿到一个code把这个 code 传给后端后端调接口换真实号码。这个流程的好处是手机号全程不经过前端前端只能拿到一个临时凭证安全性比旧方案高一个档次。public String getPhoneNumber(String phoneCode) { MapString, Object body new HashMap(); body.put(code, phoneCode); String url props.getPhoneUrl() ?appid props.getAppId(); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(X-Token, accessToken()); // 如果需要服务端 access_token ResponseEntityString resp miniAppRestTemplate.postForEntity( url, new HttpEntity(body, headers), String.class); JSONObject json JSON.parseObject(resp.getBody()); if (json.getIntValue(err_no) ! 0) { throw new LoginException(LoginError.PHONE_FAILED); } return json.getJSONObject(data).getString(phone_number); }这段代码里那个accessToken()是很多人第一次会漏掉的东西。手机号接口需要服务端凭证这个凭证要通过 appid 和 secret 换取有效期两小时必须缓存复用。如果每次请求都去换一遍一是浪费二是并发时会触发平台的频率限制直接把你限流。缓存的写法要带提前刷新Redis 里存 token过期时间设为官方有效期的 90% 左右比如有效两小时就存 108 分钟剩下 12 分钟作为缓冲。同时在取的时候加个互斥锁避免缓存刚过期时几十个线程同时去刷新。这些细节不写业务量一上来就会碰到。3.2 旧的加密数据解密路径AES 的四个参数一个都不能错有些老项目或特殊场景还在用encryptedDataiv的方式拿手机号。这条路径需要后端用session_key做 AES-128-CBC 解密参数四件套是密文、密钥session_key、初始向量iv、填充方式PKCS#7。public String decrypt(String encryptedData, String iv, String sessionKey) { try { byte[] keyBytes Base64.getDecoder().decode(sessionKey); byte[] ivBytes Base64.getDecoder().decode(iv); byte[] dataBytes Base64.getDecoder().decode(encryptedData); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, AES), new IvParameterSpec(ivBytes)); return new String(cipher.doFinal(dataBytes), StandardCharsets.UTF_8); } catch (Exception e) { throw new LoginException(LoginError.DECRYPT_FAILED); } }Java 里写PKCS5Padding是对的虽然标准名是 PKCS#7但 JDK 对 AES 的 PKCS5 实现实际就是 PKCS#7两者在这里等价不用纠结。真正会让人崩溃的是解密失败的各种原因我按踩坑频率排了个序现象大概率原因处理方向解密后全是乱码session_key 与密文不是同一次会话产生的重新走完整登录BadPaddingExceptioniv 传错或没做 Base64 解码检查前端传参IllegalArgumentException: Invalid keysession_key 被 urlencode 过先 urldecode 再解码解密成功但号码为空用户拒绝授权或字段名写错看原始 JSON第二种和第三种是新手高频问题。session_key里可能含有、/这类字符如果中间经过一次 URL 传输又被错误处理长度会变AES 一算就报密钥长度不合法。我现在的习惯是拿到 session_key 后先打日志确认长度Base64 解码后应该是 16 字节长度不对就不再往下试直接判定为会话失效让前端重新登录省下大量排查时间。还有一个隐蔽的坑解密用的 session_key 必须是产生这份密文那一次登录的。如果用户在别的地方又登录了一次平台下发了新的 session_key旧的自然失效。而你的 Redis 里如果存的已经是新的拿新钥匙去开旧锁必然报错。这就是第 4 章要讲的并发问题。3.3 昵称头像别指望一次授权全拿到昵称和头像的获取方式这些年变过好几轮现在主流做法是让用户主动填写头像用按钮open-typechooseAvatar昵称用input typenickname。前端把用户选择的头像临时路径和昵称文本传给你你上传头像文件、存昵称。这里的工程注意点是头像临时路径会失效。前端拿到的是本地临时文件路径几个小时后就不能访问了。所以你不能只存路径字符串必须在接收时立刻把文件传到自己的对象存储存的是你自己的永久 URL。我见过项目直接存临时路径上线第二天用户头像全变成空白回头补数据脚本补了一晚上。另外昵称要做长度和内容校验。用户可能输入极长字符串或特殊字符直接入库会在某些展示位上出问题也会给后续的搜索、导出埋雷。建议入库前统一做 trim、长度截断到 30 字符以内、过滤掉控制字符。4. 登录态维护token 设计与会话密钥的生命周期登录做完只是开始真正的维护成本在这个登录态怎么活、怎么死、怎么续。4.1 随机 token 还是 JWT先想清楚要不要踢人下线这两个方案的差别不在技术上而在业务上。JWT 的好处是无状态服务端不查库就能校验扩展方便。坏处是没法主动作废——除非你维护一个黑名单那就又变成有状态了。如果业务里有封号立即生效换设备强制下线管理员踢人这类需求JWT 会让你做得很别扭。随机 token不透明串的好处是每次都查 Redis可控性拉满想失效就删 key想续期就改 TTL。坏处是多一次 Redis 查询但这个开销在登录场景下可以忽略。我的选择是面向普通用户的业务系统优选随机 token。原因很实际——用户体系的运营动作太多了封禁、合并、风控、异地登录提醒每一样都需要即时生效无状态凭证在这些场景下只会让你写一堆补丁。JWT 更适合服务间调用或者短生命周期的临时凭证。具体落地public LoginResult login(String code) { Code2SessionResult sess authClient.code2Session(code); User user userRepository.findByOpenId(sess.getOpenId()) .orElseGet(() - registerNewUser(sess.getOpenId())); String token UUID.randomUUID().toString().replace(-, ); // token - userId30 天 stringRedisTemplate.opsForValue().set( auth:token: token, String.valueOf(user.getId()), Duration.ofDays(30)); // session_key 单独存与 token 解耦 stringRedisTemplate.opsForValue().set( miniapp:sk: sess.getOpenId(), sess.getSessionKey(), Duration.ofMinutes(25)); return new LoginResult(token, user.getId()); }注意我把 token 和 session_key 分开存key 前缀也不同。原因是它们的生命周期完全不一样token 可以活 30 天session_key 只能活 25 分钟。如果硬塞到一个结构里session_key 一过期就把 token 带走了用户第二天进来还得重新登录体验很差。4.2 session_key 过期引发的连锁反应session_key 是登录链路里最脆弱的一环。它的特点是只有用户重新走一次登录才会刷新而且有效期不由你控制。如果你的业务里有解密敏感数据的操作手机号、加密的用户信息就必须处理它过期的情况。判断过期的信号很直接解密抛异常。这时候不能简单返回解密失败给用户那样用户一脸懵。正确的处理是返回一个明确的业务错误码比如NEED_RELOGIN前端捕获这个码之后重新调tt.login()拿 code再请求一次登录接口拿到新的 session_key 后重试刚才的操作。整个流程对用户应该是无感的——他只是在点手机号授权按钮时多等了半秒。这里有个细节重试一定要有次数上限。我见过写成死循环的代码session_key 因为某些配置问题一直换不对前端就一直重登把接口打出一堆流量日志刷爆。设个两次上限第二次仍失败就报错并上报告警。4.3 静默登录与 token 续期让用户感受不到登录这件事好的小程序登录体验是用户从来不觉得我在登录。第一次进来静默换 openid 建档之后每次进来靠 token 直接进业务页token 快过期了自动续。实现上分三层第一层入口静默登录。小程序启动时检查本地 token没有就静默走一遍登录流程全程不弹任何 UI。这一步只拿 token不请求任何授权用户完全无感。第二层业务请求拦截续期。后端在 token 校验的拦截器里如果发现这个 token 剩余有效期低于某个阈值比如 7 天就自动延长 TTL并把新 token 通过响应头下发。这就是所谓的滑动过期。Long userId getUserIdFromToken(token); if (userId null) { throw new UnauthorizedException(); } Long ttl stringRedisTemplate.getExpire(auth:token: token, TimeUnit.SECONDS); if (ttl ! null ttl 0 ttl 7 * 24 * 3600) { stringRedisTemplate.expire(auth:token: token, Duration.ofDays(30)); }第三层敏感操作的二次确认。涉及手机号、支付、修改绑定信息这类操作即使 token 有效也不能只靠 token。要么重新走一次手机号授权要么要求用户在近期内有过交互。这一层是安全边界不能省。滑动过期有个副作用要提前说Redis 里会沉淀大量长期不活跃用户的 token。如果用户量做到几十万这些 key 会吃掉不少内存。解决方式是定期扫一遍 TTL 剩余很长的 key 做抽样清理或者干脆把续期上限封顶——比如 token 签发后最多续到 90 天超过就强制重新登录一次。这个上限值按业务敏感度定越敏感越短。5. 我踩过的坑与完整排查链路前面讲的都是应该怎么做这一章讲做完了为什么还是不对。下面三个问题我都在真实项目里遇到过排查过程记录下来比结论更有用。5.1 一直报 code 无效从请求到配置逐层剥离现象前端调tt.login()拿到了 code后端调接口 100% 返回 code 无效前端重试也没用。我的排查顺序是这样的一层一层往下砍第一步确认 code 是新的。让前端在拿到 code 的瞬间执行console.log同时后端记录收到的 code 前 8 位和后 8 位。两边对一下如果后端收到的是上一个版本缓存的值问题就在前端。这一步排掉了大约三成的案例。第二步确认 code 只被用过一次。在后端接口里数一下某个 code 被请求了几次。如果发现前端在页面onLoad和某个组件mounted里各调了一次登录两次请求带着同一个 code那第二次必然失败。解决方案不是让后端兼容而是前端合并登录请求——用一个 Promise 做单例正在登录中就直接复用同一个 Promise。第三步确认 appid 匹配。这一条最难查因为表面上一切正常。小程序的开发版、体验版、正式版对应不同的 appid有时是同一个主体下的不同应用如果后端配置的 appid 跟当前跑的小程序版本不一致code 一定换不出来。验证方法是让前端在小程序里打印tt.getAccountInfoSync().miniProgram.appId和后端配置里的值逐字符对比。我就在这上面浪费过大半天最后发现是测试环境的配置被顺手改成了另一个应用的凭证。第四步确认 code 没被 URL 编码搞坏。code 里可能包含特殊字符如果前端拼 URL 时没编码或者后端接收时多做了一次解码传过去的值就和原始值不一样了。这里最简单的验证是把后端发出的完整请求 URL 打出来看一眼。这个排查顺序的价值在于每一步都能独立证伪不会陷入到处改一点试试的状态。线上问题最怕的就是没有排查路径改一处重启一次最后自己都不知道是哪一处改好的。5.2 体验版能登录正式版一直失败这个现象的迷惑性极强因为代码一模一样。原因基本都在配置层面。最常见的三种一是正式版没在小程序后台配置服务器域名白名单请求直接被拦前端只看到网络错误二是正式版的 appSecret 和测试环境用的不是同一套或者复制时多了一个空格三是正式版的包用的是另一套环境变量后端读到的 appid 是空的。第一种的验证很直接在正式版里看请求是否真的发出去了。如果前端显示请求失败但后端日志一条都没有那就是域名白名单的问题。这个配置在开发阶段经常被临时关掉上线前忘了补属于典型的环境差异坑。第二种多一个空格的坑我要单独说因为它太隐蔽了。appSecret从后台复制时偶尔会带上首尾空格或换行符配置文件里肉眼看不出来调接口就是签名不对。我现在的习惯是在应用启动时打一行日志输出 appId 和 secret 的长度不输出值长度对不上立刻就能发现。5.3 并发登录把 session_key 冲掉了这个坑最典型也最值得讲因为它涉及的是设计问题而不是配置问题。现象用户点手机号授权偶尔失败重试有时候好有时候不好日志里解密报错但把错误日志拉出来看session_key 明明是刚换的。原因是这样的小程序有两个入口比如首页和某个活动页用户快速切换时两处都触发了登录。第一次登录拿到 session_key A写入 Redis第二次登录拿到 session_key B覆盖了 A。而用户上一次点击授权时加密用的是 A。后端拿 B 去解 A 加密的数据必然失败。更麻烦的是这类问题复现率很低测试同学点十次可能只碰到一次开发自测根本碰不到只有真实用户快速操作时才会撞上。我的解决方案是给 session_key 加上版本号允许多份并存Redis 里不存单个值存一个 hashminiapp:sk:{openid}field 是会话版本可以用时间戳或递增序号value 是 session_key整个 hash 设 30 分钟过期。登录时写入新版本前端拿到 token 的同时也拿到当前版本号后续解密请求带上版本号后端按版本取对应的 key。如果不想改动这么大退一步的方案是解密失败时不立刻报错先用 Redis 里当前的 session_key 试一次失败再走完整重登流程。这个方案简单代价是多一次接口调用但能在绝大部分情况下自愈。真正不能接受的方案是解密失败就告诉用户操作失败。用户完全不知道自己该干什么只会觉得这个功能时好时坏。5.4 一个容易被忽略的细节openid 与 user_id 的映射要双向可查最后一个坑偏架构。项目初期很多人直接用 openid 当用户主键图省事。到了后期要做多端打通比如再上一个 App 或公众号发现 openid 有多个主键不能变了只能加映射表迁移成本很高。我的做法从第一个项目起就固定内部用户表用自增 id 或雪花 id 做主键openid 作为唯一索引存在关联表里。这样以后接入第二个渠道只是多加一行关联记录主键不变所有业务表的外键都不用动。多花半小时建一张表能省掉后面几天的数据迁移。6. 上线前值得过一遍的自检项代码写完、联调通过不代表可以上线。登录这条链路是整个小程序的入口它出问题等于所有功能都进不去所以上线前我会固定过一遍下面这些项。配置侧产品环境的 appId、appSecret 是否确实从对应应用的后台取服务器域名白名单是否配齐包括业务接口域名和文件上传域名HTTPS 证书是否在有效期内且链完整有些工具会自动补中间证书真机可能不认。接口侧所有对外接口是否都有超时设置平台接口调用是否都有重试且重试次数有上限token 校验拦截器是否排除了登录接口本身不然永远进不去错误响应里是否泄露了内部异常栈。数据侧openid 是否建了唯一索引用户表是否有创建时间和最后登录时间的默认值session_key 是否只存在服务端且有过期时间日志里是否还有明文的 secret 或 session_key。体验侧首次进入是否无感token 过期后重新进入是否自动续上手机号授权被拒绝时是否有友好提示且能再次触发弱网下点击授权按钮是否有 loading 且不会重复提交。监控侧登录成功率和失败率是否分开统计code 无效、平台超时、解密失败这三个错误码是否有独立告警登录接口的 P99 耗时是否在可接受范围。最后分享一个我在实际项目里坚持的小习惯给每个登录请求生成一个 traceId透传到下游接口日志里全部带上。登录问题九成发生在前端说失败了但后端说成功了这种扯皮场景里有 traceId 就能把一次完整请求的所有环节串起来看比来回问你几点点的高效太多。这套东西搭起来不复杂一个 Filter 加 MDC十几行代码但排障时的收益是实打实的。另外提一个后续可以扩展的方向如果业务量继续涨登录接口会成为全站最热的接口之一。这时候可以在 openid 查询用户那一步加一层本地缓存Caffeine 之类把数据库压力削掉一大截同时注意用户信息变更时主动失效缓存。不过这属于优化范畴先把链路做对、做稳再谈快顺序别搞反。
阅读完成 · 觉得有帮助?
咨询建站