公众号网页授权 OAuth2.0:从拼链接到拿 openid 的完整流程
你大概遇到过这种场景:用户在页面上点了「立即领取」,页面转了一圈白屏,日志里躺着一条 40029;更隐蔽的情况是第一次授权正常,用户退回上一页再点一次就变成 40163。这两个错误都不在业务逻辑里,而在网页授权被压缩成了一句「跳到微信、拿回 openid」。它是一条有状态、有时效、有一次性约束的链路,任何一步顺序错了,表现出来都是「拿不到 openid」。
先把踩过的坑摆出来
最常见的写法是把授权入口直接做成一个链接,用户点进去,微信回跳到你配置的 redirect_uri,页面加载时读 query 里的 code 再丢给后端。这套流程在「只点一次」时能跑通,于是很多人认为它是对的。但只要用户下拉刷新、前进后退、或者在回跳后做了一次前端路由跳转,同一个 URL 被重新执行,code 被第二次提交,就返回 40163。
第二种同样高频:测试和线上共用一个公众号,网页授权域名只配了线上那一个,本地联调把内网域名填进去,微信连 code 都不给,直接甩回 10003,而你的服务端日志里空空如也,因为请求根本没到你这里。
两种 scope 决定了用户看到什么
微信在授权入口只问你一件事:要静默还是要用户点头。这个选择由 scope 决定。
snsapi_base:静默授权
用户看不到任何确认页,页面直接完成跳转,你只能拿到 openid。适合「只需要知道这个人是谁」的场景,比如活动页判断是否参与过、H5 的未登录态识别。它不打扰用户,能用 base 的地方不要用 userinfo。
snsapi_userinfo:弹窗授权
首次访问会弹出授权确认页,用户点同意后才回跳,你除了 openid 还能拉昵称、头像、性别、地区。注意「首次」两个字:同意过之后不会重复弹窗,所以你没法靠「有没有弹窗」判断新老用户。
- 只区分身份:snsapi_base。
- 要展示昵称头像:snsapi_userinfo。
- 用户拒绝过:别反复拉起弹窗,先给说明再给重试入口。
- 两种 scope 可按页面粒度混用。
授权链接的完整拼装
入口地址固定,参数一个都不能少,注意最后的片段标记是协议的一部分。
https://open.weixin.qq.com/connect/oauth2/authorize
?appid=your_appid
&redirect_uri=URLENCODE后的回调地址
&response_type=code
&scope=snsapi_base
&state=自己生成的防伪串
#wechat_redirect
response_type 只能是 code。state 由你生成、你校验,微信只负责原样带回。结尾的 #wechat_redirect 必须带上,否则部分客户端会跳出微信内置浏览器,或者出现页面不刷新。
拼装时把 redirect_uri 整体做一次 urlencode,包括协议头、路径和 query。只编码 query 部分是常见错误,微信收到的 redirect_uri 会在第一个 & 处被截断,你的页面只能拿到半个参数。
redirect_uri 的两个坑:域名配置与编码
第一个坑是域名。回调地址的域名必须和后台「网页授权域名」里配置的完全一致,微信校验的是域名,不是完整路径。不少人以为把整条 URL 填进配置项就行,实际它只比对域名部分。不匹配时返回 10003,而错误发生在授权阶段,你的服务端连请求都收不到。
第二个坑是编码。判断方法很直接:把最终链接粘到浏览器打开,看回跳后落到你页面上的 query 与预期是否逐字一致,尤其是回调地址自身带多个参数的情况。另外回调域名不要带下划线、不要用 IP,这些限制在调试期常被绕过,上线后集中爆发。
code 只能用一次,而且只有五分钟
code 是一次性凭证,成功消费一次后立即失效。下面几种写法都会撞上 40163:
- 前端拿到 code 后发了一次请求,失败重试又用同一个 code。
- 页面里两处初始化逻辑各提交了一次 code。
- 后端把 code 当缓存键,多个并发请求同时消费。
正确做法是 code 只在后端消费一次,换到 openid 后立刻落到自己的会话体系,前端后续只带我方凭证。如果第一次消费超时、判断不清是否成功,就重新走一次授权拿新 code,别拿旧 code 重试。也不能把 code 塞进队列慢慢处理:异步任务排队超过五分钟,百分之百失败。
用 code 换 access_token 与 openid
拿到 code 后第一步在后端完成,参数放在 query 上。
https://api.weixin.qq.com/sns/oauth2/access_token
?appid=your_appid
&secret=your_appsecret
&code=回跳带回的code
&grant_type=authorization_code
返回体里有 access_token、expires_in、refresh_token、openid、scope,绑定开放平台且用户授权过还会带 unionid。两个注意点:appid 与 secret 必须来自同一个公众号,混用返回 40125;scope 为 snsapi_base 时返回体里没有昵称头像字段,别指望后面从别处补上。
网页授权 access_token 和全局 access_token 是两套东西
这是最容易混的一点。网上大量 token 缓存代码缓的是全局 access_token,而网页授权拿到的是另一种凭证。两者名字相同,来源、用途、生命周期都不同,混用只会得到 40001 或 48001。
| 对比项 | 网页授权 access_token | 全局 access_token |
|---|---|---|
| 获取方式 | 用 code 换,跟在用户授权之后 | 用 appid 与 secret 直接换取 |
| 代表谁 | 某一个具体用户 | 整个公众号 |
| 能调什么 | 拉用户资料、刷新授权 | 模板消息、素材、菜单等 |
| 缓存策略 | 按用户维度存,配 refresh_token 续期 | 全局一份,提前刷新 |
全局 access_token 走下面这个地址,grant_type 用 client_credential。它不依赖用户授权,但读不了用户资料;反过来网页授权 token 也发不了模板消息。
https://api.weixin.qq.com/cgi-bin/token
?grant_type=client_credential
&appid=your_appid
&secret=your_appsecret
拉取用户信息、state 的用法与错误码
只有 snsapi_userinfo 才能拉到资料,接口是固定的:
https://api.weixin.qq.com/sns/userinfo
?access_token=网页授权access_token
&openid=用户的openid
&lang=zh_CN
返回昵称、头像、性别、城市、省份、国家。提醒两点:头像地址是微信侧的地址,别当长期资源直接落库;昵称里可能出现表情符号,字段长度和字符集要留余量。
state 不是可选项,它唯一的作用是防 CSRF:发起授权前生成随机串写进用户会话,回跳时比对。不校验的后果是,攻击者能构造一条指向你回调地址的链接,诱导已登录用户完成一次「绑定」,把对方的 openid 绑到受害者账号上。state 用一次即作废,别塞业务数据——它是明文回传的。
错误码对照
| 错误码 | 原因 | 处理 |
|---|---|---|
| 40029 | code 无效、被截断或已过期 | 重新走授权拿新 code,不要重试旧 code |
| 40163 | code 已被使用 | 后端保证单次消费,前端加防重复提交 |
| 10003 | redirect_uri 域名与配置不符 | 核对后台授权域名,注意只比对域名 |
| 40125 | appsecret 无效 | 确认 appid 与 secret 属于同一公众号 |
| 40001 | 凭证无效或已被刷新顶掉 | 重新获取后重试,收敛统一刷新入口 |
| 48001 | 接口未授权,多为用错 token 类型 | 区分网页授权与全局 token 的调用场景 |
最后一条建议:把「发起授权」和「消费 code」各收成一个函数,不要散在页面各处。授权链路一旦被复制粘贴,40163 就会以你意想不到的路径出现,排查成本远高于当初的抽象成本。






