简介:面向uni-app开发者的微信H5授权登录工具包,专门解决H5端获取用户openId的常见痛点,可用于用户登录、注册及账号绑定场景。代码已提前封装好,下载即可引入项目,适合刚接触微信网页授权的开发者,也适合需要快速上线H5登录功能的中小型项目。资源压缩包共2个文件,包含1个js工具文件与1个vue页面示例,整体大小仅4KB,携带方便、侵入性低。js文件内封装了授权请求、openId解析及错误处理逻辑,vue文件则演示了从发起授权到获取openId并回传后端的完整调用流程,有助于开发者理清H5与微信服务端的交互关系。目前已有8158人学习/下载,经较多开发者验证,可减少踩坑成本。通过该资源,开发者可直接复用核心代码,补充自己的业务逻辑,快速打通微信授权登录链路,无需从零研究官方文档和回调细节。
1. 微信 H5 授权获取 openId 是在解决什么问题
有时候一个简单的页面,嵌在微信公众号菜单或微信内打开的 H5,需要只凭微信号就能识别用户身份。微信出于安全考虑,不会在网页端直接提供 openId 给前端,必须走网页授权流程。uniapp 写 H5 时最大的误区是在小程序里用的uni.login拿 code 直接换 openId,在 H5 端根本不触发。实际做法是改用微信公众平台的 OAuth2.0 网页授权,用跳转授权链接、回调拿 code、后端换 openId 完成登录注册。下面这条链路会完整演示从授权链接拼接到 token 签发的做法,并给出用户表与 token 设计。适合卡在配置回调域名、不知道 code 怎么传给后端、或者想区分静默授权和手动授权的开发者。
2. 授权方式选型与前置配置:snsapi_base、snsapi_userinfo 和回调域名
2.1 两种 scope 的适用边界
微信网页授权按 scope 分成两种,差别不是“能不能拿 openId”,而是“拿 openId 的过程中要不要弹授权确认页”:
| scope | 是否弹确认 | 能拿到什么 | 适合场景 |
|---|---|---|---|
snsapi_base | 否 | openId、unionid(在已绑定开放平台时返回) | 纯登录注册、静默识别 |
snsapi_userinfo | 是 | openId、unionid、昵称、头像、性别等 | 需要展示用户资料 |
如果只是做登录注册,直接用snsapi_base。如果想在登录后顺便把微信昵称头像一起存下来,用snsapi_userinfo。但要注意,即使用snsapi_base,在后续sns/oauth2/access_token的响应里也有可能返回unionid,前提是当前公众号已经绑定过微信开放平台账号。
选择snsapi_userinfo时还要考虑用户耐心。首次授权必须手动点确认,第二次进入时是否再弹确认取决于微信对该用户的历史授权记录,不可控。对登录注册这种高频操作,静默授权明显体验更好,用户资料完全可以等用户进入个人中心后引导补全。
2.2 在公众号后台配置网页授权回调域名
H5 要拿到 code,必须先配置回调域名。打开公众号后台,进入“设置与开发 -> 公众号设置 -> 功能设置 -> 网页授权域名”,填写实际部署 H5 的域名:
- 域名必须是公网可访问的域名,不能是 IP,协定默认 80/443 端口。
- 如果你把 H5 部署在
https://example.com/h5/,域名只填example.com,微信允许你任意指定回调路径。 - 保存前微信会让你下载一个校验文件,放到 H5 站点根目录。uniapp 构建出来的 SPA 没有现成的静态根目录,我一般把校验文件放到独立的静态目录里,确认 HTTPS 能访问到再点保存。
这里很多人被卡住,不是授权代码写错了,而是回调域名配置时校验文件一直下载不到。原因通常是 uniapp 构建后部署在 CDN 或子路径下,Nginx 没有把校验文件单独路由出来。你可以先建一个verification/目录,把校验文件放进去,再在 Nginx 里单独声明location = /MP_verify_xxx.txt。
2.3 uniapp 中先判断微信浏览器环境
授权链接只能在微信内置浏览器里调起,所以在 uniapp 的登录页里要先判断环境,依据是navigator.userAgent是否包含MicroMessenger:
// utils/env.ts export function isWeChatBrowser(): boolean { return /MicroMessenger/i.test(navigator.userAgent); }这段代码在 H5 端直接可用,在 App 端的内嵌 web-view 里通常不包含MicroMessenger,所以它只适用于微信浏览器环境判断。如果非微信浏览器打开,应该走账号密码或手机验证码登录,不要让用户卡在授权页。在登录页里可以这样用:
if (!isWeChatBrowser()) { // 走普通账号密码登录 } else { const redirectUri = encodeURIComponent(location.href); location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?...`; }这里location.href是 uniapp H5 构建后的当前页面地址。注意别把location写成window.location,有些 uniapp 跨端编译器在特定环境下会对 window 做限制。逻辑上,非微信浏览器的降级入口必须保留,否则用户从浏览器打开公众号链接时,连登录方式都没有。
2.4 用一个独立的授权中转页管理 code
不要每个页面都去拼授权链接。我一般会在 uniapp 里单独开一个pages/wechat-auth/index页面,专门接收微信回调并处理 code。从任何页面发起登录时,都先跳转到这个页面。
这个页面本身不展示 UI,它的职责只有三件事:从 URL 里取code,把code传给后端换 openId,拿到用户后跳回登录前页面。这样做还有个好处,授权回调域名只要保证这一个页面能访问到就行,其他页面不参与跳转,排查问题范围更小。后面如果要加绑定手机号、unionid 合并逻辑,也只需要改这一个地方。
3. 从 code 到 openId:完整步骤与后端换码接口
3.1 拼接网页授权链接
微信网页授权链接是固定格式:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect在 uniapp 的 H5 端拼接时,redirect_uri必须经过encodeURIComponent,否则微信会解析失败:
// utils/wechatAuth.ts export function buildAuthUrl(redirectUri: string, state: string): string { const url = 'https://open.weixin.qq.com/connect/oauth2/authorize'; const params = new URLSearchParams({ appid: 'wx1234567890abcdef', // 实际项目建议放在构建环境变量里 redirect_uri: redirectUri, response_type: 'code', scope: 'snsapi_base', // 纯登录用静默授权 state: state, }); return `${url}?${params.toString()}#wechat_redirect`; }参数说明:appid是公众号的 AppID;redirect_uri是微信回调地址,必须与后台配置的网页授权域名一致;response_type固定为code;scope决定授权模式;state是自己生成的随机串,用于防 CSRF,微信回调时会原样返回。最后的#wechat_redirect是微信要求的固定标记,不能丢。
state一般用带时间戳的随机字符串,比如Math.random().toString(36).slice(2),在前端跳转前写入uni.setStorageSync。不能把state写成固定值,否则攻击者可以伪造授权链接让用户点击,再把伪造的 code 带到你后端,造成账号混淆。
3.2 在回调页拿 code 并传给后端
用户同意授权或自动跳转后,微信会把页面重定向到redirect_uri,并在 query 上拼code和state。在 uniapp 页面里用onLoad(options)拿这两个参数:
// pages/wechat-auth/index.vue <script setup> import { onLoad } from '@dcloudio/uni-app'; onLoad(async (options) => { if (!options.code || !options.state) { uni.redirectTo({ url: '/pages/login/index' }); return; } const savedState = uni.getStorageSync('wechat_auth_state'); if (options.state !== savedState) { uni.showToast({ title: '授权状态校验失败', icon: 'none' }); return; } const res = await uni.request({ url: 'https://api.example.com/api/login/wechat', method: 'POST', data: { code: options.code }, }); uni.setStorageSync('token', res.data.token); uni.redirectTo({ url: '/pages/index/index' }); }); </script>这里有几个关键点。onLoad拿到的options是 uni-app 解析好的 query 对象;code只在这一次跳转里有效,有效期大约 5 分钟,用一次后就失效。所以code不应该被存储,更不应该出现在日志里。state校验不能省略,否则攻击者可以诱导用户访问一个带 code 的回调地址。
注意uni.request的返回值结构是res.data,不是res.body,跨端写法要和 H5 保持一致。后端返回里如果包含token,就说明登录注册已经完成;如果返回 40029 等错误码,要引导用户重新发起授权,不能停在当前页。
3.3 后端用 code 换取 openId
前端把 code 拿到后,不能在前端直接请求微信接口,因为这一步需要用到 appsecret,一旦暴露在 H5 源码里就等于公开。后端收到 code 后,向微信服务器发起下面这个 GET 请求:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code用 Node.js(Express)实现的换码接口大致如下:
// server/routes/wechat.js const express = require('express'); const axios = require('axios'); const router = express.Router(); router.post('/api/login/wechat', async (req, res) => { const { code } = req.body; if (!code) { return res.status(400).json({ error: 'code is required' }); } try { const tokenResp = await axios.get('https://api.weixin.qq.com/sns/oauth2/access_token', { params: { appid: process.env.WECHAT_APPID, secret: process.env.WECHAT_SECRET, code, grant_type: 'authorization_code' } }); const data = tokenResp.data; if (data.errcode) { // 常见错误: 40029 code 无效, 40163 code 已使用 return res.status(400).json({ error: data.errmsg, errcode: data.errcode }); } const openid = data.openid; // 这里 data 里还有 access_token, expires_in, refresh_token, scope, unionid(可选) const user = await loginOrRegister(openid, data.unionid || null); res.json({ token: signUserToken(user), userId: user.id }); } catch (e) { res.status(500).json({ error: 'wechat api failed' }); } });响应里的openid才是这个用户在你这套系统里的唯一标识。expires_in是网页授权 access_token 的有效期,通常 7200 秒;但登录场景只关心 openid,不需要拿这个 access_token 去拉用户信息,所以不用保存。如果你后续确实要用snsapi_userinfo获取头像昵称,才需要把这个 access_token 暂存起来。注意这个 access_token 和普通调用微信 API 的全局 access_token 不是一个池子,不能混用。
3.4 code 只能用一次
一个很容易被忽略的坑:同一个 code 只能换取一次 openId,第二次请求微信会返回errcode: 40163。所以流程上要保证前端只把 code 发给后端一次。如果网络抖动导致前端重复提交,后端要对这个 code 做幂等处理。简单做法是用 code 作为 key 存到 Redis,过期时间设 5 分钟,第一次处理后写入标记,后续请求直接提示已处理。这样能避免用户被失败响应误导,反复走授权流程。
4. 用 openId 构建登录与注册:用户表、判断逻辑和 token 签发
4.1 用户表设计:不要把 openId 当主键
很多最初接触这套流程的开发者会把 openId 直接作为用户表主键。短期能用,但只要你后面接入小程序、App 或同一个微信开放平台下的多个应用,就会暴露问题:openId 是跟着公众号/小程序走的,同一个用户在同一个开放平台下的不同应用 openId 不一样,只有 unionid 相同。我更推荐用自增 id 做物理主键,给 openId 加唯一索引:
CREATE TABLE `wechat_user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '公众号网页授权 openid', `unionid` VARCHAR(64) DEFAULT NULL COMMENT '开放平台 unionid,同一用户在不同应用下相同', `nickname` VARCHAR(64) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL, `mobile` VARCHAR(20) DEFAULT NULL COMMENT '后续绑定的手机号', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`), KEY `idx_unionid` (`unionid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信 h5 登录用户表';唯一索引放在 openId 上,可以保证同一公众号下同一个用户不会注册出多个账号。unionid字段可以允许为空,等后面做多应用账号合并时再回填。openId 本身算是一种半敏感数据,在日志里看到时要做脱敏,不能整段打到错误日志里。
4.2 登录注册一体化接口
后端拿到 openId 之后,先按 openId 查用户,查不到就插入新记录,查得到就直接走登录。这里不能只用“先查后插”两步,因为并发注册时两个请求都查不到用户,再同时 insert,唯一索引就会冲突。我一般用“先查后插,插不进就再查一次”的逻辑:
async function loginOrRegister(openid, unionid) { let user = await User.findOne({ where: { openid } }); if (!user) { try { user = await User.create({ openid, unionid }); } catch (e) { if (e.name === 'SequelizeUniqueConstraintError') { user = await User.findOne({ where: { openid } }); } else { throw e; } } } else if (unionid && user.unionid !== unionid) { // 同一用户此前可能在小程序里注册过,这里补全 unionid user.unionid = unionid; await user.save(); } return user; }这段逻辑里,User.create失败大概率是唯一索引冲突,说明并发请求已经由另外的进程插入了用户记录。这个时候再查一次就能拿到正确用户。如果你用的是 MySQL 原生驱动,可以捕获ER_DUP_ENTRY错误码;用 ORM 时捕获对应唯一约束异常。
要注意,在snsapi_base静默授权下,第一次注册出来的用户没有昵称和头像,只有 openId。不要在这个环节试图用snsapi_userinfo去补充,因为它需要用户主动授权弹窗,静默授权下拿不到资料。正确做法是先完成登录,等用户后续完善个人资料或绑定手机号时再更新这些字段。
4.3 token 签发与 uniapp 端保存登录态
openId 不能直接作为登录凭证返回给前端。它相当于微信生态里的身份证号,被截获后可以被拿来冒充用户。正确做法是:登录注册成功后,后端生成一个带时效的业务 token,把 userId 和 openId 放在 token 语义里,回复给前端。前端统一把 token 存到uni.setStorageSync,后续请求都带上:
// utils/request.js export function authRequest(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: `https://api.example.com${url}`, method, data, header: { 'Authorization': `Bearer ${uni.getStorageSync('token')}` }, success: (res) => { if (res.statusCode === 401) { uni.removeStorageSync('token'); uni.redirectTo({ url: '/pages/login/index' }); reject(new Error('login expired')); return; } resolve(res.data); }, fail: reject }); }); }这里的关键是业务 API 的鉴权完全依赖后端签发的 token,而不是微信给的 code。每次授权换回来的 openId 只用于创建登录会话,后续在业务请求里不再传递 openId 和 code。如果你的用户体系已经存在,还可以在签发 token 时带上当前的 session 状态,方便后端做踢人、封禁等操作。
4.4 unionid 与绑定手机号
如果你的业务同时有公众号 H5 和微信小程序,同一个用户在 H5 授权拿到的是 H5 的 openId,在小程序里拿到的是小程序的 openId,两者不同。要识别成同一个人,必须先在微信公众平台账号中心里绑定“微信开放平台”账号,把公众号和小程序都加入同一个开放平台账号下。绑定后,两个渠道的授权响应里都会带unionid。下次在 H5 授权登录时,如果发现已经存在相同 unionid 的用户,就自动把 openId 合并到那个用户记录下面。
如果只是纯粹的 H5 业务,暂时可以不处理 unionid。但我在建表时会把这一列先留着,以后做多渠道账号打通时不用改表结构。绑定手机号的逻辑也一样,openId 授权只能证明“这个微信号访问了你的页面”,不能直接证明“这个手机号属于这个人”,需要另发短信验证码做绑定。
5. 上线前要检查的 5 个 openId 授权细节
5.1 回调域名必须和 redirect_uri 完全同域名
配置了网页授权域名example.com,授权链接里的redirect_uri就必须是https://example.com/...。用www.example.com或者test.example.com都会直接报redirect_uri参数错误。本地开发时不要尝试用 IP 调试,微信不支持 IP 回调,也没办法 import localhost。需要真机测试时,可以临时用一个测试域名,验证完再切回正式域名。
5.2 微信开发者工具里开启“不校验合法域名”
在微信开发者工具中调试 H5 时,默认会校验域名。打开右上角“详情”,进入“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”后,工具里才能正常访问微信授权链接。但真机微信浏览器里没有这个开关,最终仍然需要正式域名和有效 HTTPS 证书。调试时如果遇到refused或invalid url,先检查这个开关。
5.3 优先使用 hash 模式减少回调路径冲突
uniapp 的 H5 路由如果选了 history 模式,页面路径在 URL 上会变成/pages/wechat-auth/index。微信回调到达服务器时,如果 Nginx 没有配置对应的 history rewrite 规则,就会直接 404,导致 code 拿不到。用 hash 模式时,回调链接是https://example.com/#/pages/wechat-auth/index?code=xxx,文件服务器不用额外路由配置就能打开页面。从授权稳定性角度看,我推荐 H5 端优先用 hash 模式。
5.4 同一个 code 后端必须做到一次性消费
前端防了重复提交,后端也要给 code 加缓存。可以用 Redis 写入一个wechat:code:{code}的 key,有效期 300 秒,第一次请求后把缓存删除。代码层面对同一个 code 并发请求做固定处理,防止两个请求同时去微信服务器换 openId,一个成功另一个返回 40163,导致接口报错。这个细节在高并发登录下特别重要。
5.5 用 Network 面板验证整条链路
如果用户还是拿不到 openId,打开微信开发者工具里的 Network 面板,把请求 URL 过滤出authorize和oauth2/access_token。正常流程是先看到authorize302 跳到 redirect,然后看到后端请求sns/oauth2/access_token且返回openid。如果authorize没有回调,问题在域名配置或 URL 拼写;如果回调有了但后端报错,直接看响应里的errcode,40029 代表 code 无效,40163 代表 code 被用过了。把这两个状态和微信官方文档一起对一遍,基本能定位 90% 的授权问题。把微信开发者工具的 JS 调试模式打开,在 Network 面板里确认sns/oauth2/access_token的errcode是否 0,比看任何日志都快。
本文还有配套的精品资源,点击获取