跳转至

企业微信网页授权登录

核对日期:2026-07-29
适用场景:企业微信内 H5、自建应用工作台、聊天工具栏


1. 使用边界

企业微信网页授权用于识别“当前打开网页的人”:

  • 企业成员通常返回 userid
  • 非企业成员返回当前企业下的 openid
  • 若非成员是应用可见范围内成员的客户,还可能返回 external_userid
  • 只有 snsapi_privateinfo 且满足可见范围时,才返回用于读取敏感资料的 user_ticket

网页授权不是扫码登录。普通 PC 浏览器的企业微信登录应使用企业微信 Web 登录能力,而不是强行套用此流程。


2. 授权范围

scope 交互 结果
snsapi_base 静默 成员 userid 或非成员 openid
snsapi_privateinfo 手动确认 基础身份,并可能返回 user_ticket

snsapi_privateinfo 必须带 agentid,成员还必须在应用可见范围内。普通登录优先使用 snsapi_base,不要为读取头像等非核心资料扩大授权范围。


3. 完整流程

用户打开业务 H5
    -> 后端生成一次性 state,保存站内回跳路径
    -> 重定向企业微信授权地址
    -> 企业微信回调 code + state
    -> 后端校验并消费 state
    -> 用应用 access_token + code 获取访问用户身份
    -> 按 userid / openid 建立业务身份
    -> 签发业务会话并跳回站内页面

access_token、应用 Secret、code 和 user_ticket 都不能暴露给前端。


4. 构造授权地址

https://open.weixin.qq.com/connect/oauth2/authorize
?appid=CORPID
&redirect_uri=URL_ENCODED_CALLBACK
&response_type=code
&scope=snsapi_base
&state=STATE
&agentid=AGENTID
#wechat_redirect

参数要点:

  • appid 是企业 CorpID。
  • redirect_uri 对完整回调地址进行 URL 编码。
  • state 只支持字母和数字,最多 128 字节。
  • agentid 建议始终传入,snsapi_privateinfo 时必填。
  • 回调域名必须与该应用可信域名匹配。
/**
 * 创建企业微信授权地址,并在服务端保存一次性回跳状态。
 */
public String createAuthorizeUrl(String returnPath, String scope) {
    String safePath = returnPathPolicy.requireLocalPath(returnPath);
    String state = alphaNumericTokenGenerator.generate(32);
    oauthStateStore.save(state, safePath, Duration.ofMinutes(5));

    return "https://open.weixin.qq.com/connect/oauth2/authorize"
            + "?appid=" + properties.getCorpId()
            + "&redirect_uri=" + urlEncoder.encode(properties.getCallbackUrl())
            + "&response_type=code"
            + "&scope=" + scopePolicy.requireAllowed(scope)
            + "&state=" + state
            + "&agentid=" + properties.getAgentId()
            + "#wechat_redirect";
}

5. 获取访问用户身份

GET https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo
    ?access_token=ACCESS_TOKEN
    &code=CODE

成员响应:

{
  "errcode": 0,
  "errmsg": "ok",
  "userid": "zhangsan",
  "user_ticket": "USER_TICKET"
}

非成员响应:

{
  "errcode": 0,
  "errmsg": "ok",
  "openid": "OPENID",
  "external_userid": "EXTERNAL_USERID"
}

code 只能使用一次,5 分钟未使用自动过期。

/**
 * 使用授权 code 换取当前访问者身份。
 */
public WecomVisitor exchangeCode(String code) {
    String accessToken = accessTokenService.getApplicationToken();
    JsonNode response = restTemplate.getForObject(
            "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
                    + "?access_token={token}&code={code}",
            JsonNode.class,
            accessToken,
            code
    );
    wecomErrorChecker.requireSuccess(response);

    String userId = response.path("userid").asText(null);
    String openid = response.path("openid").asText(null);
    if (userId == null && openid == null) {
        throw new IllegalStateException("企业微信未返回访问者身份");
    }
    return WecomVisitor.from(response);
}

6. 处理回调并建立会话

/**
 * 校验企业微信授权回调,建立本系统登录态。
 */
@GetMapping("/api/wecom/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, "授权回调无效");
    }

    WecomVisitor visitor = wecomClient.exchangeCode(code);
    String userId = visitor.isEmployee()
            ? accountService.loginEmployee(properties.getCorpId(), visitor.getUserId())
            : accountService.loginExternalVisitor(properties.getCorpId(), visitor);

    sessionService.writeSecureCookie(response, userId);
    return new RedirectView(savedState.getReturnPath(), false);
}

内部成员和外部访问者应使用不同身份类型和权限策略,不能因为外部访问者拿到了 openid 就授予员工权限。


7. 账号与权限模型

推荐外部身份键:

WECOM_EMPLOYEE         corp_id + userid
WECOM_EXTERNAL_VISITOR corp_id + openid
WECOM_CUSTOMER         corp_id + external_userid

登录成功只说明身份可信,最终页面和数据权限仍由本系统根据成员状态、部门、角色和应用可见范围判断。

external_userid 只在满足客户联系可见范围时可能返回,不能作为所有外部用户登录的必填字段。


8. state 和回跳安全

  • state 使用密码学安全随机数,不包含用户 ID 和 URL。
  • Redis 中保存 state、回跳路径、发起时间和预期 scope。
  • state 最多消费一次,成功或失败后都删除。
  • 回跳路径只允许站内相对路径,拒绝 //evil.example、绝对 URL 和反斜杠绕过。
  • 登录完成后重新生成业务会话 ID,防止会话固定攻击。

9. 常见错误

错误 排查方向
40029 invalid code code 已消费、过期或属于另一应用
50001 redirect_url ... 回调域名与 access_token 对应应用的可信域名不匹配
没有 user_ticket scope 不是 privateinfo 或成员不在可见范围
只返回 openid 当前访问者不是企业成员
没有 external_userid 不是客户或跟进成员不在应用可见范围
循环授权 业务 Cookie 未写入、SameSite 或回跳地址配置错误

10. 上线检查

  • 普通登录使用最小范围 snsapi_base
  • 回调域名与应用可信域名完全匹配。
  • state 随机、短期且一次性消费。
  • 回跳地址只允许站内相对路径。
  • code、Secret、access_token 和 user_ticket 不进入前端及日志。
  • 成员与非成员使用不同身份和权限模型。
  • userid 按 CorpID 隔离。
  • 没有 external_userid 时仍能正确处理外部访问者。
  • 应用停用或成员离职后可撤销业务会话。

11. 官方文档