ARTICLE DETAIL

资讯详情

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

H5微信授权登录实战:OAuth2授权码模式全链路解析

H5微信授权登录实战:OAuth2授权码模式全链路解析 微信授权登录这个话题我做 H5 开发这几年几乎每次接新项目都会有人问一次。别看微信官方文档写得挺全真正把 OAuth2 授权链接跑通、把各种边界情况处理干净还是有不少坑要踩。今天不打算把文档复读一遍而是结合自己做过的项目把 H5 微信授权登录从授权链接构造到用户信息获取的完整链路拆开讲清楚。先说清楚这东西到底在解决什么。一个用户访问你的 H5 页面你希望知道他是谁最省事的方案就是让他用微信身份一键登录。微信网页授权基于 OAuth2.0 的授权码模式核心思路是你的服务器引导用户到微信的授权页用户同意后微信把用户引回你指定的回调地址并附上一个一次性 code你的服务器再用这个 code 向微信服务器换取 access_token 和用户身份信息。这个模式的好处是密码不落地、授权状态可撤销也是微信在 Web 场景下唯一推荐的登录方式。这篇文章适合正在做公众号 H5 活动页、电商页面、内部工具 H5 的开发者尤其是第一次接微信网页授权被各种参数弄得一头雾水的同学。读完你至少能搞清楚三件事授权链接的每个参数为什么要那样写回调地址和域名配置为什么总是对不上拿到 code 之后服务器端要做什么才能算一个完整闭环。1. 方案拆解OAuth2 在微信生态里是怎么流转的1.1 微信网页授权对应 OAuth2 的哪一种模式很多人一提到 OAuth2 就先想到 access_token但实际上 OAuth2 一共有四种授权模式授权码模式、简化模式、密码模式和客户端凭证模式。微信网页授权用的是其中最常见、也是安全性最高的一种——授权码模式Authorization Code。授权码模式的核心特征是“两段式换取”第一步用户通过浏览器访问微信授权页并确认授权后微信回跳到你的回调地址此时地址栏里带的是一个临时凭证 code而不是最终的 access_token第二步你的后端服务器拿着这个 code再带着 appid 和 appsecret去微信的接口换正式的 access_token。为什么要绕这一圈因为 code 是短期凭证而且只在浏览器里传输即使被窃取了没有 appsecret 也无法兑换成 tokenappsecret 始终保存在服务器端从不暴露给浏览器。这是授权码模式最核心的安全设计。微信在 OAuth2 基础上做了一些简化。比如 access_token 分两种一种是网页授权 access_token用来获取用户基本信息另一种是公众号全局 access_token用来调用公众号后台接口。两者完全不是一回事我见过不少新人把网页授权 token 拿去调模板消息接口结果报错 40001就是因为概念搞混了。1.2 用一个生活类比理解授权码流程把整个授权流程类比成去酒店入住会更直观。你是住客你的 H5 是酒店前台微信是发房卡的安保系统。你走到前台说要订房前台H5不会直接把你信息全查了而是给你一张纸条让你去安保窗口微信授权页登记。你拿着纸条到安保窗口安保确认你身份后给你一张临时通行条code同时通知前台“这个人登记过了”。你拿着临时通行条回到前台前台再用自己的工牌appsecret找安保换正式的房卡access_token。有了房卡之后才能去餐厅查询你的会员信息用户资料。这个类比里有几个容易被忽略的点值得强调。第一临时通行条只能在很短时间窗口内使用所以从微信回跳到后端换 token 这一段的网络必须快不能把 code 存数据库再慢慢处理第二安保通知前台这件事实际上是通过浏览器带着 code 重定向到你的回调地址实现的因此回调地址必须是用户浏览器能直接访问的 HTTPS 地址不能是内网 IP第三如果你在前台这一步没有工牌也就是 appsecret 配错那就算拿到临时通行条也开不了门。1.3 为什么 H5 授权比原生 App 授权更麻烦原生 App 做微信登录用的是微信 SDK拉起的是微信客户端自身授权完成后微信直接把 code 通过回调函数交还给 App全程不需要关心浏览器、域名、重定向这些问题。但 H5 授权完全建立在 Web 浏览器之上这带来几个天然痛点一是回调地址必须是公众号后台配置的域名而且很多情况下微信对域名的配置有严格规则路径写错都会出问题二是用户可能在微信内置浏览器里打开也可能用外部浏览器打开两种场景下授权体验完全不同三是微信内置浏览器有缓存机制用户反复进页面时授权状态可能不一致调试起来非常别扭。这几件事单看都不难凑在一起就很容易翻车。我最常看到的新手错误是把后端 API 接口地址当成 redirect_uri 去配置结果回调地址和微信后台配置的域名对不上直接被系统拒绝。所以开始写代码之前先把域名、回调地址、scope 这几个基础配置理清楚后面能省很多事。2. 授权链接构造每一个参数都不能乱改2.1 授权 URL 的标准格式微信网页授权的授权链接通常是这样一个 URLhttps://open.weixin.qq.com/connect/oauth2/authorize?appidAPPIDredirect_uriREDIRECT_URIresponse_typecodescopeSCOPEstateSTATE#wechat_redirect从左到右逐个拆开看appid公众号的唯一标识在公众号后台“基本配置”里可以找到注意不是 appsecret。redirect_uri授权完成后微信要回跳的地址必须做 URL 编码而且编码前的域名要和后台配置的授权回调域名一致。response_type固定填code表示使用授权码模式。scope授权作用域分snsapi_base和snsapi_userinfo两种后面详细讲。state自定义参数回调时会原样带回用来做防跨站请求伪造和业务参数透传。#wechat_redirect这个 hash 不能删它是微信内置浏览器识别授权意图的标记删掉在某些情况下会导致无法正常跳转。我第一次写的时候漏掉了#wechat_redirect在外部浏览器里打开没感觉但在微信内置浏览器里打开就直接白屏排查了很久才发现是这个小尾巴的问题。2.2 scope 怎么选snsapi_base 还是 snsapi_userinfo这是整个授权流程里最影响用户体验的一个选择。snsapi_base是静默授权用户无感知不需要点击确认授权页直接跳回调地址。但它拿到的授权范围非常有限只能换取 openid拿不到昵称、头像这些资料。适合只需要标识用户身份的场景比如签到、抽奖、浏览权限控制。snsapi_userinfo是需要用户手动确认的授权会弹出一个微信官方的授权确认页上面写着“xxx 申请获取你的头像、昵称等信息”。用户点击允许之后才能用拿到的 access_token 调用用户信息接口获取头像、昵称、性别、地区等数据。适合必须展示用户头像昵称的场景比如社区、评论、会员中心。我在实际项目里有一条经验如果只是需要区分用户是谁一律用snsapi_base别为了省事直接上snsapi_userinfo。因为每多一次用户确认就有一定比例的用户流失活动类 H5 对这一步转化率非常敏感。反过来如果产品上明确要求必须展示微信头像和昵称那也别自作聪明用snsapi_base因为拿不到就是拿不到接口会报 40003 之类的错误白白增加调试成本。2.3 redirect_uri 的编码规则与回跳地址的坑redirect_uri 的常见错误主要集中在两个地方没编码、域名不一致。先说编码。微信要求 redirect_uri 参数必须经过 URL 编码。比如你真实的回调地址是https://api.example.com/wechat/callback?sourceh5那么放在授权链接里的 redirect_uri 就应该是https%3A%2F%2Fapi.example.com%2Fwechat%2Fcallback%3Fsource%3Dh5如果你直接把未编码的地址拼进授权链接微信解析参数时会把?和当成自己 URL 的分隔符导致后面所有参数错乱。很多新手用 Postman 或者浏览器直接拼 URL 测试结果返回 400 错误排查半天最后发现是编码问题。再说域名一致性。微信公众号后台配置的是“授权回调域名”注意是域名不是完整路径也不是带协议的地址。比如你在后台配置的是api.example.com那么回调地址https://api.example.com/wechat/callback是合法的但https://www.example.com/wechat/callback就不行因为 www 子域名和后端接口域名不是同一个。这里有一个很多人不知道的细节后台配置的域名不带路径但回调地址本身可以带任意路径只要域名匹配就行。换句话说你在后台只需要配置一次api.example.com所有路径下的回调都可以用不需要为每个模块单独配置域名。2.4 state 参数不只是防 CSRF微信官方文档对 state 的解释是“重定向后会带上 state 参数开发者可以填写 a-zA-Z0-9 的参数值最多 128 字节”。很多人都把它当成安全参数但它的真正价值比你想象的更大。最核心的用途确实是防跨站请求伪造。攻击者可以伪造一个授权链接诱导用户点击如果回调接口不校验 state后端就无法区分这次回调是不是用户真实发起的授权流程。所以正确的做法是后端生成授权链接时生成一个随机 state 并关联当前用户的会话回调时比对 state不一致就拒绝。但 state 还可以做更多事情。我经常用 state 透传业务参数比如statepromo_2024、stateredirect%3A%2F%2Fprofile这样用户授权完成后后端可以根据 state 决定让他跳到哪个页面。这样做的好处是用户从授权页回跳时浏览器地址栏里只会多一个参数不暴露业务参数也避免了把业务敏感信息放在 URL 上。有一个常见误区要提醒state 不是用来做用户身份标识的不能把用户 ID 直接塞进去。因为 state 会随授权链接在浏览器地址栏和微信服务器日志中出现把用户 ID 放进去有信息泄露风险。正确做法是 state 关联一个随机 token用户身份信息从服务端会话里取。3. 完整实操流程从配置到拿到用户信息3.1 公众号后台和域名配置动手写代码之前先把配置做好。这里分两种情况如果你做的是普通公众号里的 H5用的是公众号的 appid如果你做的是开放平台账号下的网站应用用的是开放平台的 appid。两者回调域名的配置入口也不一样别搞混。普通公众号的配置路径是公众号后台 → 设置与开发 → 公众号设置 → 功能设置 → 网页授权域名。这里填域名比如api.example.com不需要填https://前缀。填完之后微信会要求你下载一个校验文件放到该域名根目录下确保域名是你的。配置中有个需要注意的点网页授权域名配置完成之后微信服务器会定时校验你的域名校验文件是否仍然存在。如果你之后把校验文件删了授权会被中断。所以上线之后别急着删文件有些团队把文件放上去就不管了后来域名清理时误删导致第二天授权全部失败业务直接挂掉。3.2 后端生成授权链接并跳转配置完成后后端要做的事是根据当前用户是否已登录决定是否生成授权链接。下面是一个用 Python Flask 写的简化例子import hashlib import time import random from urllib.parse import quote APP_ID wx1234567890abcdef CALLBACK_URL https://api.example.com/wechat/callback def make_state(): raw f{time.time()}-{random.random()} return hashlib.md5(raw.encode()).hexdigest() def build_auth_url(scopesnsapi_base): state make_state() # 把 state 存到 session回调时比对 session[oauth_state] state redirect_uri quote(CALLBACK_URL, safe) return ( fhttps://open.weixin.qq.com/connect/oauth2/authorize f?appid{APP_ID} fredirect_uri{redirect_uri} fresponse_typecode fscope{scope} fstate{state} f#wechat_redirect )前端拿到这个链接之后直接做页面跳转window.location.href authUrl;如果你做的是活动页我建议不要用window.location.href直接替换当前页面而是用一个中间页承载授权跳转这样用户授权完成后可以顺利回到原来的落地页而不是丢失页面上下文。3.3 回调收到 code 后的处理逻辑用户授权完成后微信会 302 回跳到你的回调地址URL 长这样https://api.example.com/wechat/callback?codexxxstatexxx后端拿到 code 和 state 之后第一件事是校验 state确认这个回调确实是你发起的授权流程。state 不一致直接拒绝不用处理后续逻辑。state 校验通过之后后端拿着 code 去换 access_token。微信接口地址是https://api.weixin.qq.com/sns/oauth2/access_token?appidAPPIDsecretSECRETcodeCODEgrant_typeauthorization_code这个接口返回的 JSON 大致长这样{ access_token: ACCESS_TOKEN, expires_in: 7200, refresh_token: REFRESH_TOKEN, openid: OPENID, scope: snsapi_userinfo }这里有几个关键字段需要特别留意access_token是网页授权令牌有效期两小时openid是用户在当前公众号下的唯一标识refresh_token是刷新令牌有效期 30 天可以用来在 access_token 过期后重新获取。换 token 这个步骤有几个容易踩的坑。第一code 只能用一次换完 token 它就失效了所以后端要保证接口幂等不能因为用户刷新页面导致重复用同一个 code 去请求。第二code 有效期很短官方文档写的是 5 分钟实际生产中建议拿到 code 后立刻换取不要入库再异步处理。3.4 用 access_token 获取用户信息如果 scope 用的是snsapi_userinfo换到 access_token 之后还可以继续拉取用户信息。接口地址https://api.weixin.qq.com/sns/userinfo?access_tokenACCESS_TOKENopenidOPENIDlangzh_CN返回的数据大致是{ openid: OPENID, nickname: 微信用户, sex: 1, province: 广东, city: 深圳, country: 中国, headimgurl: https://thirdwx.qlogo.cn/mmopen/..., unionid: UNIONID }这里要注意headimgurl是用户头像的 URL但用户更换头像后这个 URL 会变。所以如果你在数据库里存了头像地址一定要在用户每次回访时重新拉取或主动刷新否则显示的是旧头像。另一个细节微信返回的头像 URL 是按照用户最后使用的头像生成的如果用户很久没登录微信头像 URL 可能指向一张默认灰色头像图。这种场景下前端可以做一次图片加载失败的兜底显示默认头像别让页面出现裂图。3.5 openid 和 unionid 怎么选很多项目团队一上来就说“用 openid 做用户唯一标识”这句话对一部分项目成立但跨场景时就会出问题。openid 的规则是同一个用户在不同公众号、小程序、App 下openid 是不同的。也就是说如果你的业务既有公众号 H5又有小程序用户在这两个场景下拿到的 openid 不一样就无法直接用 openid 关联同一个用户。这时候需要用 unionid它是微信开放平台下同一个用户的唯一标识。前提是你的公众号、小程序、App 都绑定在同一开放平台账号下。所以在设计用户表时我建议把 openid 和 unionid 都存下来用 unionid 做跨端用户唯一标识openid 做当前端标识。如果 unionid 为空说明用户没有关联开放平台或者还没绑定这种情况再退回到用 openid 做唯一标识。4. H5 场景的适配细节微信内置浏览器没那么简单4.1 判断当前是不是微信浏览器OAuth2 授权链接在微信内置浏览器里打开时会走微信自己的重定向逻辑体验相对顺畅。但如果你在外部浏览器比如手机自带浏览器、PC Chrome里打开同一个链接微信会提示“请在微信客户端打开链接”用户就卡住了。因此很多 H5 会在入口处做环境判断。判断方法是检查 User-Agent 里是否包含MicroMessengerconst ua navigator.userAgent.toLowerCase(); const isWechat ua.indexOf(micromessenger) ! -1; if (!isWechat) { // 提示用户使用微信扫码打开或者直接跳转下载/引导页 showTip(); }有人会问如果用户已经在外部浏览器里点开了授权链接能不能引导他跳转到微信严格来说做不到从浏览器直接拉起微信来完成授权微信网页授权这种基于浏览器的流程只能老老实实在微信内跑。所以 H5 页面在设计入口时就要区分微信内直接授权微信外引导复制链接到微信打开或者引导扫码。4.2 用户拒绝授权怎么办snsapi_userinfo授权模式下用户有权利点击“拒绝”。拒绝之后微信仍然会回跳到你的回调地址但 code 是无效的后端拿去换 token 会报错。这个场景一定要处理。策略是如果授权失败判断用户是否只是需要基本功能如果是降级到snsapi_base静默授权至少拿到 openid让用户能用基础功能如果产品确实需要用户信息那就在回调页弹一个友好的提示说明不授权无法继续并提供“重新授权”按钮。注意这里别做无限循环。用户拒绝一次你立刻再次跳授权页体验很差也会被微信限制。我一般是记录一个拒绝标记在同一会话内最多出现一次重新授权引导再拒绝就直接降级或退出把选择权交给用户。4.3 缓存、刷新和回退对授权状态的影响H5 项目做多了就会碰到一个很烦的场景用户授权登录成功后点浏览器回退按钮又回到了授权前的页面再往前跳页面又显示未登录。这个问题的根源是授权状态没有和页面导航机制对齐。OAuth2 授权码模式本身是无状态的它不会在浏览器里留下任何登录标记。你在回调接口里换取用户信息后必须自己给用户发一个登录态比如种 cookie、生成 token 存 localStorage或者服务端维护 session。我见过很多团队在回调接口里直接重定向到首页但登录态还没有发下来导致首页认为未登录又发起一次授权形成死循环。解决方法是回调接口里先完成登录态写入再重定向前端在路由守卫里检查登录态时要预留一个“正在处理授权回跳”的状态避免重复发起授权。另外要注意微信内置浏览器的缓存机制。微信的 WebView 对页面缓存比较激进有时你改了页面 JS用户手机上还是旧版本。这时候除了常规的加版本号缓存策略还可以用微信提供的 JSSDK 里的updateAppMessageShareData等接口做部分能力但页面缓存本身还是靠 HTTP 头控制。给你的 H5 页面加合理的Cache-Control头能少掉很多线上问题。4.4 授权完成后的页面跳转策略授权完成后到底跳到哪里这是产品层面容易被忽视的问题。很多团队不约而同地把用户跳回首页但体验其实不好。我的经验做法是授权链接里的 state 参数带上登录前页面的标识或路径回调完成登录后后端根据 state 决定重定向目标。比如用户从商品详情页触发登录授权完成就回商品详情页而不是把他丢回首页。这里你们可以参考一个类似的场景就是“H5 唤起小程序”那种跳转逻辑。微信生态里的页面跳转普遍都有“回跳”需求做授权登录时把这一步提前设计好能省掉很多产品来回扯皮的时间。5. 高频问题排查这些错误我基本都遇过5.1 微信提示 “redirect_uri 参数错误”这是出现频率最高的报错之一。排查顺序我建议按照下面的表来排查点判断方式解决思路是否 URL 编码授权链接中 redirect_uri 是否包含%编码字符对回调地址做quote()编码域名是否一致后台配置的域名和回调地址的域名是否相同统一到同一个主域名或子域名域名校验文件后台域名根目录的校验文件是否被删除重新上传校验文件appid 是否正确授权链接中的 appid 是否有误从公众号后台重新复制有一个项目上踩过的坑因为项目有多个子域名后端配置了api.example.com作为回调域名但前端跳转时用的是www.example.com第三方测试工具没提示问题到了微信里就报 redirect_uri 参数错误排查了半天才确认是域名不一致。5.2 页面无限重定向 / 授权死循环症状很明显打开页面后URL 在授权页和回调页之间反复跳转用户根本进不了正常页面。原因几乎都是回调逻辑里没有正确写入登录态导致每次进入首页路由守卫时都认为未登录又重新发起授权。排查方法打开浏览器开发者工具看 Network 里回调接口的响应和重定向地址。如果回调接口返回了登录态但前端还是判断未登录说明前端的登录态读取逻辑有问题如果回调接口自己就遭遇了异常比如换 token 报错那问题在后端。一个常见的隐藏 bug 是在回调接口里用了 log 记录日志但 log 挂了导致接口抛异常用户拿到的是一个 500 页面微信回跳后就卡在那里。5.3 code 换 token 报 4002940029 错误码对应的含义是invalid code。最常见的原因是同一个 code 被重复使用了两次。比如用户授权回跳后前端和后端各调了一次换 token 接口第一次成功第二次就报 40029 了。解决方法是保证回调接口的幂等性。后端在同一次授权流程中用一个唯一标识比如 state 或 session id把 code 和换取结果绑定如果发现同一个 code 已经换过 token直接返回已换好的 token 结果而不是再次调微信接口。另外code 的有效期很短且是针对当前 appid 的。如果你的回调接口不小心把 code 记到日志里又不小心用日志里的 code 去另一套环境测试也会报 40029。5.4 静默授权 snsapi_base 却拿不到用户信息scopesnsapi_base拿到的 access_token 只能获取 openid 和部分基础能力没有权限调/sns/userinfo接口。如果强行调用微信会返回错误码 48001 或者api unauthorized。解决方法是如果你需要用户信息授权链接里就必须用snsapi_userinfo。几乎没有投机取巧的办法也不能在静默授权之后再悄悄升级权限微信不支持这种流程。有一种折中方案是先用snsapi_base静默登录当用户主动点击“完善资料”之类的按钮时再发起一次snsapi_userinfo授权。这样大部分用户无感登录有资料完善需求的用户才走一次明确授权体验上更顺畅。5.5 access_token 过期和刷新机制网页授权 access_token 有效期官方写的是 2 小时。但实际生产环境里有时 1 个多小时就失效了尤其在高并发或频繁刷新 token 的场景下更容易踩到。微信提供了refresh_token用于刷新 access_token有效期 30 天且刷新后旧的 access_token 立即失效。注意refresh_token 在 30 天内可以多次使用但每次获取新的 refresh_token 之后旧的 refresh_token 也会失效。我建议后端把 access_token 和 refresh_token 统一缓存起来设置合理的过期时间比如 access_token 提前 10 分钟过期就批量刷新。这样前端请求用户信息时后端总是能提供一个大概率可用的 token而不是每次都让前端重新授权。6. 授权信息的安全边界别把用户数据当儿戏6.1 openid 和 unionid 能公开吗openid 本身不是敏感数据它只是用户在当前应用下的一个随机标识不能直接反查到手机号或真实身份。但 unionid 对应的是用户在同一开放平台下所有应用的统一标识它的关联性比 openid 强所以在日志、URL 参数、前端埋点里都不要随意输出。我踩过的教训是早期某个项目把 openid 放到分享链接的 URL 参数里结果用户之间可以互相对照虽然 openid 不能直接泄露隐私但给用户造成了一种“你们把我 ID 暴露了”的不好观感还被运营同事投诉了。6.2 appsecret 必须锁死在服务端appsecret 是公众号除了 appid 之外的另一个关键凭证。它一旦泄露攻击者就可以用你的公众号名义换取 access_token进而拉取用户信息、发模板消息后果非常严重。所以唯一正确的做法是appsecret 只出现在后端环境变量或配置中心里绝不写在前端代码里不放在本地配置文件里提交到 Git 仓库不在日志里打印。6.3 授权登录态要设置合理有效期OAuth2 授权只解决“身份确认”这一步登录态的有效期完全由你自己控制。我建议 H5 项目根据业务风险级别分档活动抽奖类可以做到 24 小时个人中心类建议 7 天涉及支付或敏感操作的必须要求用户重新登录。最后再分享一个小经验。微信网页授权这套流程文档看起来只有几页但真正要做稳最重要的不是代码写得多么花哨而是把参数配置、回调幂等、登录态写入、降级策略这四件事想清楚。我自己每次接新的 H5 项目时都会先把授权流程图在纸上画一遍用户从哪个页面来、授权成功后回哪去、授权失败降级成什么模式、token 过期了怎么刷新。把这四件事确定下来再去写代码基本不会出大问题。
返回列表