ARTICLE DETAIL

资讯详情

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

微信H5授权获取openId实战:从网页授权到token签发完整流程

微信H5授权获取openId实战:从网页授权到token签发完整流程 简介面向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?appidAPPIDredirect_uriREDIRECT_URIresponse_typecodescopeSCOPEstateSTATE#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是公众号的 AppIDredirect_uri是微信回调地址必须与后台配置的网页授权域名一致response_type固定为codescope决定授权模式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?appidAPPIDsecretSECRETcodeCODEgrant_typeauthorization_code用 Node.jsExpress实现的换码接口大致如下// 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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?codexxx文件服务器不用额外路由配置就能打开页面。从授权稳定性角度看我推荐 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 拼写如果回调有了但后端报错直接看响应里的errcode40029 代表 code 无效40163 代表 code 被用过了。把这两个状态和微信官方文档一起对一遍基本能定位 90% 的授权问题。把微信开发者工具的 JS 调试模式打开在 Network 面板里确认sns/oauth2/access_token的errcode是否 0比看任何日志都快。本文还有配套的精品资源点击获取
返回列表