跳转至

微信公众号 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 命中其他业务账号时,进入验证合并流程,不静默迁移订单和权限。

推荐身份键:

create unique index uk_official_account_identity
    on external_identity(provider, app_id, open_id);

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 和用户资料已脱敏。

11. 官方文档