做过微信公众号开发的人基本都遇到过这个场景:公司同时运营着服务号、订阅号甚至小程序,用户在A号里是张三,到了B号里系统又把他当成一个陌生人。这是因为微信为每个公众号分配的openid都是独立的,同一个微信用户在不同公众号下的openid完全不同。要打通这套用户体系,就必须借助unionid机制。本文围绕网页授权流程,完整讲解如何拿到用户的unionid。

一、unionid到底是什么,为什么必须绑定开放平台
先厘清几个容易混淆的概念。openid是用户在某个公众号下的唯一标识,注意是"某个公众号下",换一个公众号它就变了。而unionid是用户在同一家企业主体下的全局唯一标识,前提是这些公众号都绑定到了同一个微信开放平台账号。
unionid的获取有一条硬性前提:公众号必须绑定到微信开放平台账号。绑定入口在微信开放平台的管理中心,添加公众号后用微信扫码确认即可。如果公众号没有绑定开放平台,无论你怎么调用接口,返回的JSON里都不会有unionid字段,这是最常见的一个坑。另外,绑定操作要求公众号和开放平台账号的主体一致,或者开放平台账号已认证且公众号管理员扫码授权。
绑定成功后,unionid的来源主要有三个渠道:网页授权的userinfo接口、公众号获取用户基本信息的接口(需用户已关注公众号)、以及小程序登录流程。本文重点讲第二种,也就是网页授权方式,因为它不要求用户先关注公众号。
二、网页授权四步流程详解
网页授权的核心流程分为四步:引导用户进入授权页面同意授权、通过code换取网页授权access_token、刷新token(可选)、拉取用户信息。整个流程中,code是关键的中转凭证,有效期只有5分钟且只能使用一次。
第一步是构造授权链接,把用户引导到微信的授权页面。链接格式如下:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
这里有几个参数需要特别注意。redirect_uri是授权成功后跳转回你的回调地址,必须经过urlencode处理,且该域名需要先在公众号后台的网页授权域名中配置并通过验证。scope有两个可选值:snsapi_base只能拿到openid,静默授权用户无感知;snsapi_userinfo会弹出授权确认框,但只有这个scope才能拉取用户昵称、头像以及unionid。
很多人为了用户体验选择静默授权,结果发现拿不到unionid,原因就在这里。不过实际测试中,只要公众号绑定了开放平台,部分情况下通过code换token的返回结果里也会直接带上unionid,这点后面代码里会体现。
三、用code换取access_token并获取unionid的代码实现
第二步是用code换取网页授权access_token。注意这个token和基础支持的access_token不是同一个东西,网页授权token的接口不需要提前换取全局凭证,直接用code、appid、secret三个参数调用即可。
下面以PHP为例演示完整流程:
<?php
// 第一步:通过code换取网页授权access_token
$appid = '你的appid';
$secret = '你的secret';
$code = $_GET['code']; // 微信回调时附带在redirect_uri上的参数
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?"
. "appid={$appid}&secret={$secret}&code={$code}&grant_type=authorization_code";
$res = json_decode(file_get_contents($url), true);
// 绑定了开放平台时,这里通常已经能直接拿到unionid
$openid = $res['openid'];
$unionid = isset($res['unionid']) ? $res['unionid'] : '';
$accessToken = $res['access_token'];
// 如果上一步没返回unionid,则通过userinfo接口拉取
if (empty($unionid)) {
$infoUrl = "https://api.weixin.qq.com/sns/userinfo?"
. "access_token={$accessToken}&openid={$openid}&lang=zh_CN";
$userInfo = json_decode(file_get_contents($infoUrl), true);
$unionid = $userInfo['unionid'];
// $userInfo中还包含nickname、headimgurl等字段
}
echo '用户unionid为:' . $unionid;如果用Java,逻辑完全一样,只是HTTP调用方式不同。核心都是两个HTTP请求:先换token,再拉用户信息。建议对返回结果做好errcode判断,比如40029表示code无效,40163表示code已被使用,这些都是调试阶段的高频错误码。
拿到unionid后,把它作为用户表的唯一索引存储。用户从任意一个公众号进入系统时,先查unionid是否已存在,存在则直接登录,不存在则创建新用户。这样多个公众号、小程序共享一套会员体系的目标就实现了。
四、常见问题排查与实践建议
第一个高频问题是回调后拿不到code。检查redirect_uri的域名是否与公众号后台配置的授权域名完全一致,包括是否带了www前缀。其次,redirect_uri必须urlencode,特别是带有查询参数时,参数中的特殊字符不编码会被微信截断。
第二个问题是返回结果没有unionid字段。按优先级排查:确认公众号已绑定开放平台;确认scope是snsapi_userinfo而不是snsapi_base;确认绑定关系生效(有时需要等待几分钟)。如果用接口获取用户基本信息拿不到unionid,检查获取方式是unionID机制还是直接返回,不同公众号类型行为略有差异。
安全方面有几点建议。第一,access_token和openid不要暴露在前端页面或URL中传递;第二,state参数建议带上随机串并在回调时校验,防止CSRF攻击;第三,secret绝对不能写在前端代码里,换取token的操作必须在服务端完成。另外网页授权access_token有效期约7200秒,如果需要重复拉取用户信息,可以用refresh_token续期,不必让用户反复授权。
最后提一个多公众号架构下的实践技巧:可以在数据库中建立一张映射表,记录unionid与各公众号openid的对应关系。用户每在一个新公众号完成授权,就往映射表插入一条记录。这样后续调用各公众号的模板消息、客服消息接口时,能根据unionid快速反查出对应公众号的openid,接口调用不会乱套。这套设计在集团多品牌运营的场景里非常实用。