unionid是微信生态中唯一标识一个用户的全局ID,只要同一微信号在微信开放平台下绑定了多个应用(公众号、小程序、网站应用),这些应用通过各自接口获取到的unionid就是同一个值。这一特性让unionid成为打通多端用户体系的关键字段。但在实际开发中,很多团队在实现微信公众号网页授权时,只拿到了openid,却没有拿到unionid,或者拿到的unionid在不同端不一致。这通常不是因为微信接口变了,而是因为对授权流程、开放平台绑定关系、接口参数的理解存在偏差。

要从网页授权中稳定拿到unionid,需要同时满足三个条件:公众号已绑定微信开放平台、授权scope为snsapi_userinfo、用户在不同端使用的是同一微信开放平台账号。这三个条件缺一不可。接下来从原理到实践,把整个流程和常见问题一次性讲透。
为什么必须用unionid而不是openid
openid是微信针对某个应用(公众号、小程序、App等)生成的用户身份标识。同一个用户,在公众号A下的openid是a,在公众号B下的openid又是b,在小程序下的openid还可能完全不同。如果系统把openid当作用户主键,那么当同一个用户从公众号跳转到小程序时,后台就会认为是两个不同的人,产生两个账号,后续的积分、订单、会员数据全部割裂。
unionid则绑定在微信开放平台层面。只要多个应用挂载在同一个开放平台账号下,同一个微信号的unionid就始终保持一致。因此,在涉及多端登录、账号统一体系的场景中,必须用unionid作为用户的全局身份标识,openid只能作为某个应用内部的临时身份使用。理解这个区别,是设计统一登录体系的第一步。
网页授权获取unionid的完整流程
微信网页授权分为静默授权和用户信息授权。如果想拿到unionid,就必须使用snsapi_userinfo类型的授权,snsapi_base只能获得openid,无法获得用户详细资料和unionid。完整的流程分为三步:构造授权链接、授权回调换取access_token、拉取用户信息。
首先,需要在公众号后台配置网页授权域名。然后构造如下授权链接:
$appid = '你的AppID';
$redirect_uri = urlencode('https://www.ippipp.com/wechat/callback.php');
$state = 'login';
$scope = 'snsapi_userinfo';
$url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={$appid}&redirect_uri={$redirect_uri}&response_type=code&scope={$scope}&state={$state}#wechat_redirect";
header('Location: ' . $url);
用户点击该链接后会跳到微信授权页面,确认后微信会带着code参数跳转回redirect_uri。在回调地址中,需要用code去换取网页授权的access_token:
$code = $_GET['code'];
$tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={$appid}&secret={$secret}&code={$code}&grant_type=authorization_code";
$response = file_get_contents($tokenUrl);
$data = json_decode($response, true);
$accessToken = $data['access_token'];
$openid = $data['openid'];
$refreshToken = $data['refresh_token'];
拿到access_token后,下一步就是拉取用户信息。注意这里不能用公众号的基础access_token,必须使用sns授权接口返回的access_token,否则会报错。同时,openid也必须来自当前网页授权,不能使用其他端获取的openid。
$userUrl = "https://api.weixin.qq.com/sns/userinfo?access_token={$accessToken}&openid={$openid}&lang=zh_CN";
$user = json_decode(file_get_contents($userUrl), true);
if (isset($user['unionid'])) {
$unionid = $user['unionid'];
} else {
// 未拿到unionid,进入排查逻辑
$unionid = '';
}
当上述流程全部正确时,返回的user数组中会包含unionid字段。如果缺失,则说明前面提到的三个条件中有某个环节没有满足。
拿不到unionid的常见原因排查
不少开发者按照文档写完代码,却发现返回的数据里没有unionid。就我的经验来看,以下四种原因占据了绝大多数场景。
第一,公众号没有绑定微信开放平台。如果开放平台账号下只绑定了这个公众号,或者根本没有绑定任何应用,那么用户信息接口就不会返回unionid。解决方法是登录open.weixin.qq.com,在“管理中心—公众账号”中把公众号绑定到开放平台账号下。注意,一个公众号只能绑定一个开放平台账号,而且绑定后通常需要等几分钟到几小时生效。
第二,授权scope用成了snsapi_base。很多同学为了流程简便,在授权时使用了静默授权,这样拿到的openid虽然能用,但用户信息接口没有权限返回unionid。必须将scope修改为snsapi_userinfo,并且用户必须在授权页面手动点击“同意”。
第三,缓存了旧的用户数据。如果之前已经授权过一次,并且把用户信息存进了数据库,后来才绑定开放平台,那么之前存下的unionid字段自然是空的。此时需要清掉缓存,或者强制用户重新走一次授权流程,才能拿到新的unionid。
第四,回调地址的域名与后台配置不一致。如果redirect_uri的域名没在公众号后台的“网页授权域名”中配置,微信会直接报错,根本走不到用户信息接口。这种情况虽然不会导致unionid为空,但会中断整体流程,也值得检查。
多端统一登录的实践要点
拿到unionid后,还需要在数据库设计中体现以unionid为核心的用户体系。一个常用的做法是建立用户主表和微信绑定表。用户主表存储业务字段,微信绑定表记录unionid与openid、应用类型的对应关系。
下面是一张简化的微信用户绑定表结构:
CREATE TABLE `wechat_user` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `unionid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信开放平台唯一ID', `openid` varchar(64) NOT NULL DEFAULT '' COMMENT '当前应用内openid', `app_type` varchar(20) NOT NULL DEFAULT 'mp' COMMENT '应用类型:mp公众号, mini小程序, app', `nickname` varchar(128) DEFAULT '' COMMENT '昵称', `avatar` varchar(255) DEFAULT '' COMMENT '头像', `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_unionid_app` (`unionid`, `app_type`), KEY `idx_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';
当用户从公众号登录时,先根据openid查找记录;如果openid不存在,再看unionid是否已存在。unionid存在则说明用户在别的端已经注册过,直接将该openid关联到已有用户记录即可,避免重复建号。unionid不存在才创建新用户。
在实际项目中,还需要注意用户信息合并的问题。比如用户在PC网站通过手机号注册过,后来在公众号通过微信授权登录。这时如果直接创建一个新用户,就会产生账号孤岛。比较好的做法是:在用户第一次微信授权时,提供“绑定已有账号”的入口,通过手机验证码确认身份后,将微信openid和unionid关联到已有用户ID上。之后再从任何端进入,都能识别到同一个用户。
此外,还需要单独保存网页授权的access_token,因为它的有效期只有两个小时,且刷新也有次数限制。建议在获取到unionid后,立即将用户资料写入数据库,不要依赖长期保存access_token。如果后续需要调用微信接口,应使用refresh_token刷新,而不是重复拉取用户信息。
总结
unionid是微信多端统一登录的核心,但它的获取前提是公众号已绑定开放平台,并且网页授权必须使用snsapi_userinfo。在开发中遇到unionid缺失时,可以从绑定关系、授权scope、缓存数据、回调域名这四个方向排查。搭建用户体系时,建议以unionid作为全局唯一标识,openid作为应用内标识,配合用户绑定表实现灵活的多端登录体验。按这套方案落地,能有效减少重复账号和数据割裂问题,让用户在不同端口之间无缝切换。