企业微信网页授权登录
核对日期: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. 获取访问用户身份
成员响应:
非成员响应:
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 时仍能正确处理外部访问者。
- 应用停用或成员离职后可撤销业务会话。