微信公众号 OAuth 与用户信息
核对日期:2026-07-29
适用场景:微信内 H5 网页登录、公众号用户识别与账号绑定
1. 能力边界
公众号网页授权可以获得当前公众号下的 openid,在用户主动同意 snsapi_userinfo 时还可读取公开资料。它不能直接获得手机号。
| scope | 交互 | 可获得信息 |
|---|---|---|
snsapi_base |
静默 | 当前公众号的 openid |
snsapi_userinfo |
用户确认 | openid、昵称、头像等授权资料 |
OAuth 网页授权 access_token 与公众号全局 access_token 是两种不同凭证,不能混用。
2. 完整流程
用户在微信中打开 H5
-> 后端生成一次性 state 并保存原始站内路径
-> 重定向微信 OAuth 授权页
-> 微信回调 code + state
-> 后端校验并消费 state
-> 使用 code 换取网页授权 access_token 和 openid
-> 按需获取用户资料
-> 建立业务会话
-> 重定向到经过白名单校验的站内路径
授权回调应由后端处理,AppSecret 和 OAuth access_token 不进入浏览器。
3. 构造授权地址
https://open.weixin.qq.com/connect/oauth2/authorize
?appid=APPID
&redirect_uri=URL_ENCODED_CALLBACK
&response_type=code
&scope=snsapi_base
&state=OPAQUE_STATE
#wechat_redirect
后端生成一次性 state:
/**
* 创建公众号授权地址,并将回跳目标保存在服务端。
*/
public String createAuthorizeUrl(String returnPath, String scope) {
String safeReturnPath = returnPathPolicy.requireLocalPath(returnPath);
String state = randomTokenGenerator.generate(32);
oauthStateStore.save(state, safeReturnPath, Duration.ofMinutes(5));
String callback = urlEncoder.encode(properties.getOAuthCallbackUrl());
return "https://open.weixin.qq.com/connect/oauth2/authorize"
+ "?appid=" + properties.getAppId()
+ "&redirect_uri=" + callback
+ "&response_type=code"
+ "&scope=" + scope
+ "&state=" + state
+ "#wechat_redirect";
}
scope 只允许服务端枚举值,不能由前端传入任意字符串。回跳目标只允许 /orders/123 这类站内相对路径,禁止外部 URL。
4. code 换取网页授权 Token
GET https://api.weixin.qq.com/sns/oauth2/access_token
?appid=APPID
&secret=APPSECRET
&code=CODE
&grant_type=authorization_code
成功响应的关键字段:
{
"access_token": "OAUTH_ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "OAUTH_REFRESH_TOKEN",
"openid": "OPENID",
"scope": "snsapi_userinfo",
"is_snapshotuser": 0,
"unionid": "UNIONID"
}
服务端调用:
/**
* 使用一次性 code 换取公众号网页授权身份。
*/
public OAuthIdentity exchangeCode(String code) {
JsonNode response = restTemplate.getForObject(
"https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid={appid}&secret={secret}"
+ "&code={code}&grant_type=authorization_code",
JsonNode.class,
properties.getAppId(),
properties.getAppSecret(),
code
);
wechatErrorChecker.requireSuccess(response);
return OAuthIdentity.from(response);
}
code 只能使用一次。网络结果不明确时不要盲目重复消费,应让用户重新发起授权。
5. 获取用户信息
只有授权范围包含 snsapi_userinfo 时才调用:
GET https://api.weixin.qq.com/sns/userinfo
?access_token=OAUTH_ACCESS_TOKEN
&openid=OPENID
&lang=zh_CN
/**
* 在用户已授予 snsapi_userinfo 时获取公开资料。
*/
public WechatProfile getProfile(OAuthIdentity identity) {
if (!identity.hasScope("snsapi_userinfo")) {
throw new IllegalStateException("当前授权不包含用户资料权限");
}
JsonNode response = restTemplate.getForObject(
"https://api.weixin.qq.com/sns/userinfo"
+ "?access_token={token}&openid={openid}&lang=zh_CN",
JsonNode.class,
identity.getAccessToken(),
identity.getOpenid()
);
wechatErrorChecker.requireSuccess(response);
return WechatProfile.from(response);
}
昵称和头像会变化,只适合展示,不能作为账号合并或授权判断依据。
6. 回调与业务登录
/**
* 校验 OAuth 回调并签发本系统登录会话。
*/
@GetMapping("/api/wechat/oauth/callback")
public RedirectView callback(String code, String state, HttpServletResponse response) {
OAuthState savedState = oauthStateStore.consume(state);
if (savedState == null || code == null || code.isEmpty()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "授权回调无效");
}
OAuthIdentity identity = wechatOAuthClient.exchangeCode(code);
String userId = accountService.loginByOfficialAccount(identity);
businessSessionService.writeSecureCookie(response, userId);
return new RedirectView(savedState.getReturnPath(), false);
}
state 必须一次性消费并设置短有效期,用于防止登录 CSRF。不能把回跳 URL 直接明文塞进 state 后原样跳转。
7. Token 刷新
网页授权 Token 过期且业务确实需要继续读取用户资料时,可使用 refresh token:
GET https://api.weixin.qq.com/sns/oauth2/refresh_token
?appid=APPID
&grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
多数“只识别并登录一次”的场景无需长期保存 OAuth Token。登录完成后签发自己的业务会话,可减少敏感凭证保存范围。
8. 与 UnionID 和业务账号关联
- openid 唯一范围是当前公众号 AppID,数据库唯一键应包含 AppID。
- 公众号已绑定开放平台且满足返回条件时,响应可能带 UnionID。
- UnionID 缺失时仍按 AppID + openid 正常创建或登录。
- 发现 UnionID 命中其他业务账号时,进入验证合并流程,不静默迁移订单和权限。
推荐身份键:
9. 常见问题
| 问题 | 原因与处理 |
|---|---|
| 回调域名错误 | 在公众号后台配置网页授权域名,不包含协议和具体路径 |
redirect_uri 不一致 |
对完整回调地址进行一次正确 URL 编码 |
| 用户资料接口报错 | 实际 scope 是 snsapi_base 或 OAuth Token 已失效 |
| 拿不到手机号 | 公众号 OAuth 本身不返回手机号,需独立验证流程 |
| openid 对不上 | 混用了不同公众号或小程序的 openid |
| Token 无效 | 混用了网页 OAuth Token 与公众号全局 Token |
10. 安全检查
- AppSecret、OAuth Token 和 refresh token 仅在后端保存。
- state 随机、短期、一次性消费。
- 回跳地址仅允许站内相对路径。
- code 不记录、不复用,失败后重新发起授权。
- openid 按 AppID 隔离。
-
snsapi_base场景不调用用户资料接口。 - 昵称头像不参与权限和账号合并判断。
- OAuth 完成后使用自己的业务会话。
- 日志中的 openid、UnionID 和用户资料已脱敏。