☰
uni-app 微信小程序授权登录:code、openid、token 全链路
2026/9/29 1:26:29 网站建设 项目流程

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-type="chooseAvatar"配合<input type="nickname">),让用户自己确认后再提交。手机号则通过open-type="getPhoneNumber"的按钮,拿到一个动态令牌(也叫 phone code),再由后端去换取真实的手机号。

把它们分开理解有个好处:登录可以在用户一进页面时就静默完成,不影响体验;而信息授权则放在用户需要填写资料、下单、领券这些具体场景时再触发,用户拒绝也不至于连登录都失败。

3.3 关键接口和参数清单

前后端提到的接口其实不多,但每个都要知道参数怎么填。

前端的核心方法:

  • uni.login({ provider: 'weixin' }):发起登录,成功回调里能拿到 code。
  • uni.checkSession():检查登录态是否还有效。
  • uni.getUserProfile():部分版本仍可用,但能力已受限,不建议依赖。
  • 按钮的open-type="getPhoneNumber":拿手机号动态令牌。

后端需要调用的微信服务端接口是https://api.weixin.qq.com/sns/jscode2session,必填参数是appid、secret、js_code、grant_type=authorization_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 }); } // 签发自己的业务 token,2 小时有效 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 token(30 天),access 过期时用 refresh 换新的 access,只有 refresh 也过期了才让用户重新授权登录。这样既降低了长期凭证暴露的风险,又让用户基本感觉不到掉线。

不过要注意,refresh token 一旦被发现异常使用,应该立刻失效并让用户重新登录。简单判断异常的方式是记录 token 的使用设备或网络特征,出现明显不一致时收紧策略。

6.3 从单端登录扩展到多端的规划

如果你的项目未来可能同时有小程序、App、H5,建议从一开始就在用户表里保留 unionid 字段,即便当前用不上。用户体系设计成"一个用户对应多个平台的绑定关系",而不是把 openid 直接当成主键。这样等到需要打通的时候,只要补上各端的绑定逻辑即可,不用动已有的数据结构,改造成本会低很多。

7. 个人收尾

最后分享一个我用了很久的小技巧:把登录流程里的每一个失败分支都打上不同的提示文案,而不是统一一句"登录失败"。用户看到的是"网络好像不太稳定"还是"授权被取消了",感受完全不同,你自己排查问题时也能一眼从用户反馈里定位环节。授权登录这套东西写一次可能只需要半天,但要写得让它在真实网络环境、真实用户操作下都稳,靠的是对每个失败分支的认真处理。我接手过的最难排查的一次登录问题,最后发现是用户手机上时间被改过,导致某些校验失败,这种边缘情况只有把日志和错误分支做细了才找得到。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询