做过微信生态开发的同学基本都踩过一个坑:用户在公众号里的openid和在小程序里的openid完全不一样,如果直接拿openid做用户唯一标识,同一台手机上的同一个人会被识别成两个甚至三个不同用户。微信官方给出的解决方案就是unionid机制,只要公众号、小程序、APP都绑定到同一个微信开放平台账号下,通过特定接口就能拿到同一个unionid,用它作为全局用户标识。本文围绕网页授权、小程序登录、APP授权登录三条链路,把获取unionid的完整方案讲清楚。

unionid的底层机制与绑定前提
首先要理解unionid的发放逻辑。unionid并不是某个接口单独计算出来的值,而是微信开放平台层面的产物。当同一个微信用户在使用绑定了同一开放平台账号下的公众号、小程序、APP时,微信会返回一个相同的unionid。这个值在开放平台账号维度是唯一的,也就是说,判断“是不是同一个人”的依据就是unionid是否一致。
绑定是整个方案的前提,也是最容易被忽略的一步。具体要求是:登录微信开放平台,在管理中心里创建一个账号(需要认证,认证费300元),然后把公众号和小程序都绑定到这个开放平台账号下。APP侧则是在创建移动应用后自动归属到该开放平台。如果公众号没有绑定开放平台,网页授权的返回数据里就不会出现unionid字段,很多开发者卡在这一步,代码查了半天没问题,其实是绑定没做。
需要注意的一点是,绑定关系存在一定延迟,绑定后立即测试可能仍拿不到unionid,一般等几分钟到半小时再试。另外,如果业务中还涉及微信支付、企业微信,尽量在同一套开放平台体系下统一管理,避免后期账号体系混乱。
公众号网页授权获取unionid
网页授权是公众号场景下最常见的链路。以snsapi_userinfo方式的授权为例,用户同意授权后,通过code换取access_token时,返回的JSON里除了openid、access_token、refresh_token等字段,绑定了开放平台的公众号会额外返回unionid字段。这是最直接的获取方式,一次接口调用就同时拿到openid和unionid。
完整流程分四步:构造授权链接引导用户跳转、用户同意后携带code回调、后端用code换access_token、解析返回结果。下面是构造授权链接和后端换取的示例代码:
// 构造授权跳转链接(Java示例)
String redirectUri = URLEncoder.encode("https://yourdomain.com/wx/callback", "UTF-8");
String authUrl = "https://open.weixin.qq.com/connect/oauth2/authorize"
+ "?appid=" + APPID
+ "&redirect_uri=" + redirectUri
+ "&response_type=code"
+ "&scope=snsapi_userinfo"
+ "&state=123#wechat_redirect";
// 后端用code换取access_token与unionid
String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid=" + APPID
+ "&secret=" + APPSECRET
+ "&code=" + code
+ "&grant_type=authorization_code";
JSONObject result = HttpUtil.getForJson(tokenUrl);
String openid = result.getString("openid");
String unionid = result.getString("unionid"); // 绑定开放平台后才会返回这里有几个易错点值得展开说明。第一,snsapi_base静默授权换取的access_token结果里同样可能包含unionid,前提是公众号绑定了开放平台,所以如果只需要识别用户身份而不需要头像昵称,用静默授权就够了,用户体验更好。第二,网页授权的access_token和普通调用接口用的access_token是两个完全不同的东西,前者通过sns接口获取,后者通过cgi-bin/token接口获取,混用会导致各种莫名其妙的错误。第三,code只能使用一次且五分钟内有效,回调处理要做幂等,避免重复换token报错。
小程序与APP端获取unionid
小程序的链路和公众号不同。用户打开小程序后,前端调用wx.login拿到临时登录凭证code,后端拿code、appid、appsecret请求code2Session接口,返回openid、session_key,绑定了开放平台的小程序同样会直接返回unionid。如果code2Session没返回unionid,优先检查小程序是否已绑定开放平台,其次确认用户是否关注过公众号等满足UnionID下发条件的情况。
// 小程序前端
wx.login({
success(res) {
wx.request({
url: 'https://yourdomain.com/api/login',
method: 'POST',
data: { code: res.code }
});
}
});
// 后端调用code2Session(以Node.js为例)
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`;
const data = await (await fetch(url)).json();
// data.openid、data.unionid、data.session_keyAPP端的微信登录走的是开放平台的OAuth接口。客户端拉起微信授权后拿到code,后端用code换取access_token,返回结果里同样包含openid和unionid。由于APP本身就属于开放平台,unionid几乎是必返回的,链路上反而是三端里最省心的一个。
三端账号统一落库与合并策略
拿到unionid之后,关键在于怎么设计用户表。推荐的做法是用户主表以unionid作为核心唯一键,另建一张渠道表记录openid,字段包括unionid、openid、渠道类型(official、miniapp、app)、昵称头像等。这样无论用户从哪个端进来,都能通过unionid定位到同一个用户记录,而各端的openid分别保存,用于调用各端专属接口,比如公众号模板消息需要用公众号的openid。
还有一种常见场景是老系统已经积累了大量只存openid的用户数据,此时需要做数据迁移合并。思路是:给老用户表增加unionid字段,用户下次登录时通过上述链路补全unionid,再按unionid做增量合并。合并时注意保留注册时间最早的记录作为主账号,将其他重复账号的订单、积分等业务数据迁移过来,最后用unionid作为唯一索引兜底,防止再次出现重复。
最后提醒一点,unionid属于用户敏感信息,落库时建议加密存储,接口传输走HTTPS,日志里不要明文打印完整的unionid和openid。整体方案的核心就一句话:所有端绑定同一个开放平台账号,以unionid为主键打通账号,openid按渠道分表存储,这样一套用户体系就能覆盖全部终端。