ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uni-app 微信小程序授权登录:code、openid、token 全链路

uni-app 微信小程序授权登录:code、openid、token 全链路 1. 先搞清楚 uni-app 授权登录到底在做什么接手过的 uni-app 项目里授权登录几乎是最容易卡住新人的一个环节。不是因为代码难写而是因为这条链路上牵扯了太多看不见的东西前端拿到的不是用户身份而是一张一次性的临时凭证真正的身份信息在后端换取换回来之后又不能直接扔给前端。任何一个环节理解偏了都会写出看起来能跑、上线就出问题的代码。这套东西解决的问题很具体让用户不输入账号密码只用微信身份完成注册或登录同时让后端有能力长期识别这个请求是谁发的。适合正在做 uni-app 微信小程序、需要接入登录态的开发者也适合已经写过登录但对 code、openid、session_key、token 这四个东西的关系还是一团浆糊的朋友。全文的代码都会带注释关键参数我会把为什么这么填讲清楚。我自己的判断是授权登录的难点从来不在uni.login这一行而在于整套凭证流转的设计。前端只是整条链路的起点它负责拿到 code剩下的身份解析、状态维护、安全边界全部压在后端。很多人把这套逻辑写成了纯前端方案把 appid 和 secret 塞进小程序里直接请求微信接口这种写法在开发者工具里跑得通一上线就会被抓出来因为它把最关键的密钥暴露在了最不该暴露的地方。下面会把每个环节的取舍讲透。2. 授权登录的整体设计与思路拆解2.1 前端拿凭证、后端换身份的经典分工微信小程序的登录协议本质上是一个授权码换会话的模型。整个流程可以拆成四段前端调用uni.login()向微信服务器申请一个 code前端把这个 code 发送给自己项目的后端后端拿着 code 加上小程序的 appid 和 secret去微信的接口换回 openid、unionid 和 session_key后端根据 openid 建立或查询用户记录签发自己系统的登录凭证返回给前端。这套分工的设计意图很明确。code 是一次性且短时效的即使被截获攻击者拿着它也无法直接得到身份因为换 code 还需要 secret而 secret 只存在服务端。微信把 secret 定位成只能放在服务器上的东西一旦下发到客户端等于把整个小程序的用户体系敞开。我见过太多人图省事在前端直接uni.request微信的接口把 secret 写在manifest.json或者单独一个 js 里这种做法除了能在本地自测时看到数据没有任何好处。注意小程序前端代码在真机上是可以被提取分析的任何写在前端的 secret 都等同于公开。这条红线不要碰。2.2 为什么登录态不能直接用 openid 当凭证初学者最常有的想法是既然后端已经拿到了 openid那前端直接存 openid之后请求都带着它不就行了问题在于 openid 是身份标识而不是会话凭证。它对同一个用户是固定不变的一旦泄露攻击者可以永久冒充这个用户而用户自己毫无感知也没法主动使这个凭证失效。合理的做法是后端签发一个有时效的 token把 openid 藏在服务端的会话表或缓存里前端只持有这串看不懂的字符串。这样一来登录态可以主动失效用户退出、检测到异常登录、可以续期、可以做到一次泄露影响有限。这就是为什么授权登录的完整实现里后端一定会多一张用户表或者一份会话缓存的原因。2.3 uni-app 跨端场景下的选型考量uni-app 的价值在于一套代码多端发布但登录这件事各端差异很大很难真正做到一套代码通吃。微信小程序端走的是uni.login({ provider: weixin })拿到的 code 是通过微信授权体系生成的App 端如果接的是微信登录走的是开放平台的移动应用登录code 的换取接口和参数都不一样H5 端则通常接公众号网页授权。我的经验是把登录逻辑抽象成一层平台适配层外层暴露统一的login()方法内部用#ifdef MP-WEIXIN、#ifdef APP-PLUS这样的条件编译区分实现。核心的用户信息存储、token 续期、请求拦截器这些与平台无关的部分保持共用只有如何拿到 code这一段分平台写。这样后期加一个新端改动量会小很多。如果一开始就把所有逻辑写死在小程序分支里后面接 App 端会是一场重构。3. 核心概念与关键参数解析3.1 code、openid、unionid、session_key 到底各是什么这四个名词几乎决定了整条链路的正确性我一个个说清楚。code是微信发的一次性临时票据有效期只有五分钟而且用一次就作废。它的作用类似取货凭证本身不含任何用户信息换到 openid 之后就完成使命了。同一时间重复使用同一个 code微信会直接返回错误码这也是联调时最常撞上的坑之一。openid是用户在当前这个小程序内的唯一标识。同一个用户在你的小程序 A 和别人的小程序 B 里openid 是不同的。它用来做本小程序内的用户识别长度一般是 28 位左右的字符串。unionid只有在你的账号绑定了微信开放平台、并且该用户也关注或使用过同一开放平台下的其他应用时才会返回。它的作用是跨应用识别同一个用户比如你既有小程序又有公众号又有 App希望这三边能对上同一个人就得靠 unionid。所以做用户体系规划的时候如果未来有跨端打通的需求务必在拿 code 换身份的时候就尝试获取 unionid不要等到后期再补。session_key是微信给当前会话生成的一把对称密钥用于解密用户的敏感数据比如手机号、加密的用户信息。它绝对不能下发给前端只能在后端保存。而且它本身也会过期用户重新登录时会刷新。名称是否可下发前端时效主要用途code可以但要马上用5 分钟一次有效换取身份openid谨慎建议只存后端长期稳定本小程序用户标识unionid谨慎长期稳定跨应用用户标识session_key绝对不可以随会话刷新解密敏感数据业务 token可以自定义通常 2 小时起维持登录态3.2 授权登录与获取用户信息是两件事这里有个很常见的误解以为调用了uni.login()就等于拿到了用户的头像和昵称。实际上登录只是完成了你是谁的识别并没有拿到任何资料的展示权限。头像、昵称、手机号这些属于用户信息需要额外的授权动作。过去uni.getUserInfo可以直接拿到用户昵称头像现在微信对这块做了收紧头像和昵称推荐用头像昵称填写能力button组件的open-typechooseAvatar配合input typenickname让用户自己确认后再提交。手机号则通过open-typegetPhoneNumber的按钮拿到一个动态令牌也叫 phone code再由后端去换取真实的手机号。把它们分开理解有个好处登录可以在用户一进页面时就静默完成不影响体验而信息授权则放在用户需要填写资料、下单、领券这些具体场景时再触发用户拒绝也不至于连登录都失败。3.3 关键接口和参数清单前后端提到的接口其实不多但每个都要知道参数怎么填。前端的核心方法uni.login({ provider: weixin })发起登录成功回调里能拿到 code。uni.checkSession()检查登录态是否还有效。uni.getUserProfile()部分版本仍可用但能力已受限不建议依赖。按钮的open-typegetPhoneNumber拿手机号动态令牌。后端需要调用的微信服务端接口是https://api.weixin.qq.com/sns/jscode2session必填参数是appid、secret、js_code、grant_typeauthorization_code。返回体里会有openid、session_key、unionid出错时带errcode和errmsg。提示jscode2session这个接口是有频率限制和风控的绝对不要在客户端直接调用服务端也要做好错误重试和日志不然用户量一上来会莫名其妙被限流。4. 实操过程从按钮到登录态的完整实现4.1 前置准备AppID 与项目配置动手写代码之前先把基础配置做对。在微信公众平台注册小程序后拿到 AppID然后打开项目的manifest.json在mp-weixin节点填入 appid。这一步如果漏了HBuilderX 运行到微信开发者工具时会提示未配置 appId登录接口会直接失败。用 HBuilderX 发行微信小程序的流程大致是配置好 manifest 里的 appid菜单里选择发行 → 小程序-微信工具会打包并唤起微信开发者工具然后在开发者工具里上传代码。这里有个细节要留意manifest.json里的 appid 和微信开发者工具项目设置里的 appid 必须一致否则换 code 的时候会报invalid appid。// manifest.json 片段注意 mp-weixin 节点 { mp-weixin: { appid: wx你的小程序appid, setting: { urlCheck: false, // 开发期可关闭域名校验上线前必须打开并配置合法域名 es6: true, minified: true }, usingComponents: true } }后端那边要准备 appid 和 secret建议放在环境变量或配置中心不要硬编码进代码提交到仓库。这个习惯听起来小事但团队协作里泄露密钥往往就是这么发生的。4.2 登录按钮与 code 获取的完整代码前端登录的入口一般是一个按钮点击后触发登录把拿到的 code 发给后端。下面这段代码我加了详细注释重点在于错误处理和登录态判断。// login.js 登录封装 export function wxLogin() { return new Promise((resolve, reject) { // 先检查本地是否已有未过期的登录态 uni.checkSession({ success: () { const token uni.getStorageSync(token); if (token) { // 登录态还在直接用避免频繁调用 login 触发风控 resolve({ cached: true, token }); return; } doLogin(resolve, reject); }, fail: () { // 登录态失效必须重新走登录流程 doLogin(resolve, reject); } }); }); } function doLogin(resolve, reject) { uni.login({ provider: weixin, success: (res) { // res.code 就是这一次性的临时凭证 if (!res.code) { reject(new Error(未获取到 code)); return; } // 把 code 交给自己的后端由后端去换 openid uni.request({ url: https://你的域名/api/login, method: POST, data: { code: res.code }, success: (resp) { if (resp.data resp.data.token) { // 存下后端签发的登录凭证 uni.setStorageSync(token, resp.data.token); resolve({ cached: false, token: resp.data.token }); } else { reject(new Error(resp.data.msg || 登录失败)); } }, fail: () reject(new Error(网络异常)) }); }, fail: (err) { // 用户拒绝授权或系统异常 reject(err); } }); }这段代码里有两个容易被忽略的点。一是uni.checkSession的存在意义它能告诉你在不重新登录的情况下微信侧的会话是否还有效。如果每次进页面都无脑uni.login在用户量大的时候会频繁产生新会话对自己的后端和微信接口都是压力也可能触发风控。二是uni.login的 fail 分支一定要接住虽然大部分情况下它不会失败但用户在某些特殊环境下可能拒绝授权这时如果没处理页面会卡在加载中。4.3 后端换取 openid 并签发业务 token后端这一段是整个方案的核心逻辑是拿前端传来的 code调用jscode2session得到 openid再查库或建用户最后生成 token 返回。// Node.js 示例express 风格 const axios require(axios); app.post(/api/login, async (req, res) { const { code } req.body; if (!code) return res.json({ code: 400, msg: 缺少 code }); try { // 向微信换取用户身份 const url https://api.weixin.qq.com/sns/jscode2session; const { data } await axios.get(url, { params: { appid: process.env.WX_APPID, secret: process.env.WX_SECRET, js_code: code, grant_type: authorization_code } }); // 微信出错时会返回 errcode if (data.errcode) { return res.json({ code: 401, msg: data.errmsg }); } const { openid, unionid, session_key } data; // session_key 只存服务端缓存用于后续解密手机号等敏感数据 await redis.set(session_key:${openid}, session_key, EX, 7200); // 按 openid 查用户没有则创建 let user await db.findUserByOpenid(openid); if (!user) { user await db.createUser({ openid, unionid }); } // 签发自己的业务 token2 小时有效 const token jwt.sign( { uid: user.id, openid }, process.env.JWT_SECRET, { expiresIn: 2h } ); res.json({ code: 0, token }); } catch (e) { res.json({ code: 500, msg: 服务异常 }); } });这段后端代码里有几个关键决定值得说明。session_key 用 Redis 存并设置过期时间是因为它只在解密敏感数据时需要而且会随会话变化没必要持久化到数据库。token 里塞 uid 和 openid 而不是把整个用户对象编码进去是为了控制体积同时避免敏感字段随 token 在外流转。expiresIn设成 2 小时是权衡太短用户频繁掉线太长泄露风险大配合静默续期机制比较合适。4.4 请求拦截与 token 续期登录拿到 token 只是开始后续所有接口都要带上它。我一般会在 uni-app 里封装一个请求方法统一注入 token并在收到 401 时触发重新登录。// request.js 统一请求封装 import { wxLogin } from ./login; export async function request(options) { const token uni.getStorageSync(token); const res await uni.request({ ...options, url: https://你的域名 options.url, header: { ...options.header, Authorization: Bearer ${token} // 统一带上登录凭证 } }); if (res.statusCode 401) { // 登录态过期静默重新登录后重试一次 const { token: newToken } await wxLogin(); res await uni.request({ ...options, url: https://你的域名 options.url, header: { ...options.header, Authorization: Bearer ${newToken} } }); } return res; }静默续期这件事看着简单实际能省掉大量用户操作到一半突然被踢回登录页的差评。关键点是重试只能做一次不能无限循环否则 token 一直无效时会陷入死循环把页面卡死。5. 常见问题与排查技巧实录5.1 code 重复使用与 invalid code 报错这是我遇到频率最高的问题。表现是后端调jscode2session时返回errcode: 40163或者40029提示 code 已被使用或无效。原因通常是同一个 code 被提交了两次。常见触发点是用户在登录页反复点击按钮或者前端做了重试逻辑却用了同一个 code。解决思路有几个。前端在登录按钮上要加防抖进入登录流程后立刻禁用按钮后端的接口要做好幂等同一秒内相同 code 的重复请求直接拒绝如果确实需要重试必须重新调uni.login拿新的 code不能复用旧的。我踩过的一次坑是项目中做了请求失败自动重试三次的通用逻辑结果登录接口被覆盖导致 code 被重复提交。后来在请求封装里对登录接口单独关掉了自动重试。5.2 真机和开发者工具行为不一致不少朋友反映开发者工具里登录好好的一到真机就失败。常见原因有三个。第一个是合法域名没配开发工具默认关闭了域名校验真机则强制校验请求直接发不出去。解决办法是在公众平台的开发管理 → 开发设置 → 服务器域名里把后端域名加进 request 合法域名。第二个是 HTTPS 证书问题自签名证书在真机上不被信任。第三个是 appid 和 secret 不对应开发者工具的 appid 和后端配置的不一致换 code 时就会报invalid appid。排查顺序建议从后端日志看起。让后端把微信返回的完整 errmsg 打出来对照错误码去查文档比在前端反复改代码快得多。5.3 常见问题速查表现象可能原因处理方式后端返回 40029code 无效确认是否重复使用检查 appid 是否匹配提示 invalid appidappid 配置不一致对齐 manifest 与后端配置的 appid真机请求发不出合法域名未配置在公众平台添加 request 域名checkSession 一直失败会话过期或跨设备重新走 login 流程手机号解密失败session_key 过期或错位重新登录刷新 session_key 后再解密用户拒绝授权后卡住fail 分支未处理补全 fail 回调与提示5.4 几个实战里总结出的注意点第一不要在 App.vue 的onLaunch里同步等待登录完成再渲染首页。登录是个异步过程用户网络差的时候会看到长时间白屏。更好的做法是先渲染页面骨架登录在后台并行进行需要登录态的地方做占位或加载态。第二uni.checkSession的成功只代表微信侧的会话有效不代表你后端的 token 也有效。两者要分别判断前端的缓存逻辑不能只依赖其中一方。第三关于加固后重新签名的问题很多做 App 端的朋友会遇到。核心原则是签名证书保持一致否则微信开放平台那边的应用签名校验会失败导致唤起微信登录时报错。每次更换签名或加固方案后都要在开放平台后台同步更新应用签名信息这个步骤很容易被漏掉。第四善用日志。把每次登录的 code、返回的 openid 前几位、错误码都记下来出问题时能快速定位是前端没拿到 code还是后端换失败还是用户体系那边出了问题。6. 登录态安全与长期维护的实操建议6.1 敏感数据一律后端解密手机号、加密的用户信息这类数据解密必须放在后端用的就是登录时存下的 session_key。前端不要尝试自己解密一是拿不到 session_key也不该拿到二是解密算法和密钥管理放在客户端本身就是安全隐患。后端解密时如果报错八成是 session_key 过期了这时候让前端重新登录刷新一次再重试即可。6.2 登录态过期与用户体验的平衡token 有效期设置直接关系到体验和安全。我的做法是 access token 短时效2 小时另配一个较长时效的 refresh token30 天access 过期时用 refresh 换新的 access只有 refresh 也过期了才让用户重新授权登录。这样既降低了长期凭证暴露的风险又让用户基本感觉不到掉线。不过要注意refresh token 一旦被发现异常使用应该立刻失效并让用户重新登录。简单判断异常的方式是记录 token 的使用设备或网络特征出现明显不一致时收紧策略。6.3 从单端登录扩展到多端的规划如果你的项目未来可能同时有小程序、App、H5建议从一开始就在用户表里保留 unionid 字段即便当前用不上。用户体系设计成一个用户对应多个平台的绑定关系而不是把 openid 直接当成主键。这样等到需要打通的时候只要补上各端的绑定逻辑即可不用动已有的数据结构改造成本会低很多。7. 个人收尾最后分享一个我用了很久的小技巧把登录流程里的每一个失败分支都打上不同的提示文案而不是统一一句登录失败。用户看到的是网络好像不太稳定还是授权被取消了感受完全不同你自己排查问题时也能一眼从用户反馈里定位环节。授权登录这套东西写一次可能只需要半天但要写得让它在真实网络环境、真实用户操作下都稳靠的是对每个失败分支的认真处理。我接手过的最难排查的一次登录问题最后发现是用户手机上时间被改过导致某些校验失败这种边缘情况只有把日志和错误分支做细了才找得到。
返回列表