# 企业微信登录与通知 - 前端联调方案 ## 涉及页面 - 登录页(`src/views/_core/authentication/login.vue`) - 第三方授权回调中转页(`src/views/_core/authentication/social-login.vue`) - 二维码登录页(`src/views/_core/authentication/qrcode-login.vue`) ## 本次变更概述 | 变更 | 影响前端 | |------|----------| | 企业微信首次扫码**自动注册**账号,不再报"未绑定账号" | 是 —— 登录页需新增企业微信登录入口,并去掉首次登录的绑定弹窗分支 | | 登录成功后向用户企业微信推送**登录通知** | 否 —— 后端异步发送,前端无感知 | | 新增企业微信**回调接口**(URL 验证 + 消息接收) | 否 —— 企业微信服务器调用,与前端无关 | ## API | 方法 | 路径 | 说明 | 前端是否调用 | |------|------|------|--------------| | GET | `/admin-api/system/auth/social-auth-redirect` | 获取企业微信授权跳转地址 | 是 | | POST | `/admin-api/system/auth/social-login` | 使用授权 code 换取登录令牌 | 是 | | POST | `/admin-api/system/auth/login` | 账号密码登录(可附带社交绑定参数) | 是(保留) | | GET | `/admin-api/wxcp/callback` | 企业微信回调 URL 验证 | 否 | | POST | `/admin-api/wxcp/callback` | 企业微信消息/事件接收 | 否 | ### 1. 获取企业微信授权地址 **请求参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | type | Integer | 是 | 社交类型,企业微信固定传 **30** | | redirectUri | String | 是 | 授权回调地址,即前端 social-login 页面完整地址 | **响应:** ```json { "code": 0, "data": "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=xxx&agentid=xxx&redirect_uri=xxx&state=xxx" } ``` 前端拿到 `data` 后直接跳转该地址,用户扫码确认后企业微信会带 `code` 和 `state` 回跳到 `redirectUri`。 ### 2. 使用 code 换取登录令牌 **请求参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | type | Integer | 是 | 社交类型,企业微信固定传 **30** | | code | String | 是 | 企业微信回调带回的授权码 | | state | String | 是 | 企业微信回调带回的 state | **响应(首次登录与再次登录结构一致):** ```json { "code": 0, "data": { "userId": 123, "accessToken": "xxx", "refreshToken": "xxx", "expiresTime": "2026-09-29T12:00:00" } } ``` ## 业务流程与数据带入 1. 用户点击「企业微信登录」→ 调用 `social-auth-redirect`(type=30)→ 拿到跳转地址 → 浏览器跳转到企业微信扫码页 2. 用户扫码确认 → 企业微信回跳到 `redirectUri`,URL 携带 `code` 和 `state` → 由 social-login 页面承接 3. social-login 页面读取 URL 上的 `type`、`code`、`state` → 调用 `social-login` 4. 后端判断该企业微信账号是否已绑定系统用户: - **已绑定** → 直接签发令牌返回 - **未绑定(首次)** → 自动创建系统用户并绑定 → 签发令牌返回 5. 前端拿到 `accessToken` → 写入访问令牌存储 → 拉取权限信息 → 跳转首页 **关键点:第 4 步无论首次还是再次登录,返回结构完全一致,前端不需要区分两条分支。** ## 字段展示规则 | 字段 | 展示位置 | 说明 | |------|----------|------| | accessToken | 登录成功后写入访问令牌存储 | 后续请求头携带 | | refreshToken | 登录成功后写入访问令牌存储 | 令牌过期时刷新 | | userId | 登录成功后写入用户信息存储 | 用于个人中心等场景 | ## 业务规则说明 | 场景 | 规则 | |------|------| | 企业微信首次扫码(未绑定) | 自动创建系统用户,用户名为 `wx_` + 8 位随机十六进制,昵称取企业微信昵称,账号状态为启用,随机生成密码(用户无需知晓,仅用于占位) | | 首次注册后的默认权限 | 绑定角色 `wxcp_user`,**仅授予「会员管理」整棵子树**(根菜单 1075490 + 子菜单 + 孙菜单,共 15 条)。首页是前端静态路由、不依赖菜单表,任何账号都可见,无需授权。角色缺失时代码只记 warn 日志、不阻断登录,表现为「能进系统但菜单全空」 | | 企业微信再次扫码(已绑定) | 直接登录,不创建新用户 | | 非企业微信的社交平台首次登录 | 仍返回 `1_002_000_005`「未绑定账号,需要进行绑定」,需引导用户走账号密码登录并绑定 | | 账号密码登录时携带社交参数 | 走原有绑定逻辑,登录成功后该社交账号与账号绑定 | | 登录成功通知 | 通过企业微信发送一条文本卡片消息给该用户,内容含登录时间与登录方式;用户未绑定企业微信时跳过发送,不影响登录 | | 通知发送失败 | 仅记录日志,**不阻塞、不回滚登录**,用户仍可正常进入系统 | ## 注意事项 - 企业微信授权登录要求**已备案域名**且备案主体与企业微信认证主体一致,本地 `localhost` 调试需用内网穿透并保证域名备案。 - 首次自动注册会跳过"注册功能已关闭"配置项(`system.user.register-enabled`),但**仍受租户账号配额限制**,配额用尽时返回 `USER_COUNT_MAX`。 - 自动注册生成的用户名以 `wx_` 开头,便于与人工创建的账号区分;如需限制企业微信登录的用户范围,需在业务侧按用户名前缀或角色做二次管控。 - **默认角色 `wxcp_user` 必须先在库里存在**。该角色及其「会员管理」授权已写进 `docs/sql/wxcp_menu.sql` 末尾(合并脚本 `config_export_all_<日期>.sql` 阶段三末尾同步了一份),新库初始化会自动建出来;**已建过的老库要补执行这一段**,否则扫码新账号能登录但菜单空白。代码按**角色编码**查找而非角色编号,换库不会失效。 - 回调接口 `/admin-api/wxcp/callback` 已加入免登录白名单,**不需要也不应该带前端令牌**。 - 前端原有的「未绑定 → 弹出绑定账号密码弹窗」逻辑,对企业微信场景不再触发;若产品上仍希望首次登录先绑定既有账号,需要另行明确,当前实现为直接自动注册。 ## 部署配置清单(缺一不可) 企业微信登录要跑通,以下三处配置**必须同时存在**: | # | 配置位置 | 内容 | 缺失后果 | |---|----------|------|----------| | 1 | `application-.yaml` → `wx.cp.*` | corp-id、agent-id、secret、token、aes-key | 回调接口无法解密,URL 验证失败 | | 2 | `application.yaml` → `justauth.type.WECHAT_ENTERPRISE`(**全局,不随 profile 变**) | client-id、client-secret、agent-id、**`ignore-check-redirect-uri: true`** | 调用 `social-auth-redirect` 抛 `AuthException: Illegal redirect uri [WECHAT_ENTERPRISE]` | | 3 | 数据库 `system_social_client` | social_type=30、**user_type=2**、client_id=CorpId、client_secret=Secret、agent_id、status=0、deleted=0 | 只用 yaml 里的密钥,无法在后台改;填错 user_type 则该行查不到 | **第 2 项容易漏掉**:`AuthRequestFactory` 需要先从 yaml 拿到"社交平台基类配置"才能构造出对应的 `AuthRequest` 实例,之后才会用数据库里的密钥覆盖它。所以即使密钥全都配在数据库,yaml 里也必须保留一份占位配置。 **第 2 项里 `ignore-check-redirect-uri: true` 最容易漏**。yudao 的设计是 `redirect-uri` 不写死——`SocialClientServiceImpl.getAuthorizeUrl` 最后会用 `HttpUtils.replaceUrlQuery` 把 `redirect_uri` 换成前端按当前浏览器地址传进来的值。但 JustAuth 的 `AuthDefaultRequest` 构造时会调 `AuthChecker.checkConfig(AuthConfig, AuthSource)`: ```java if (config.isIgnoreCheckRedirectUri()) { return; // 有这个开关才放行 } if (StringUtils.isEmpty(redirectUri)) { throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source); } ``` 也就是说**不写 `redirect-uri` 的前提就是打开这个开关**,两者缺一不可。只删 `redirect-uri` 而漏了开关,扫码时会在 `AuthRequestFactory.getDefaultRequest` 构造 `AuthWeChatEnterpriseQrcodeRequest` 的那一刻抛 `AuthException: Illegal redirect uri [WECHAT_ENTERPRISE]`——异常发生在构造期,早于用数据库密钥覆盖的那一步,所以配置了 `system_social_client` 也救不了。 **第 2 项为什么放 `application.yaml` 而不是某个 profile**:`YudaoJustAuthConfiguration` 上的 `@ConditionalOnProperty(prefix = "justauth", value = {"enabled"}, havingValue = "true", matchIfMissing = true)` 让 `AuthRequestFactory` **无论配没配都会创建**。缺配置不等于"关闭社交登录",而是一个运行期才暴露的坑:请求走到 `AuthRequestFactory.get()` 时先按 `justauth.type` 查不到,再回落到 `justauth.extend` 分支,而 `JustAuthProperties.extend` 默认就是 `null`,直接 `NullPointerException`。 这段配置历史上只存在于 `application-local.yaml`,导致用 `dev` / `test` / `chdl` / `kyzn` / `wtxc` / `yzfx` / `zsjc` 任一 profile 启动时,企业微信扫码登录必炸,并且报的是 `AuthRequestFactory.get` 第 104 行的 NPE,很难一眼看出是配置缺失。上移到全局配置文件后,这一类问题从结构上被消除。 **代码层兜底**:`AuthRequestFactory.get()` 已加判空守卫——`properties.getExtend()` 为 null 时跳过自定义分支,由既有逻辑抛出可读的 `AuthException`,不再抛 `NullPointerException`。 ### 回调 URL 配置 在企业微信管理后台 → 应用管理 → 自建应用 → 「接收消息」→「设置API接收」: - **URL**:`https://<已备案域名>/admin-api/wxcp/callback` - **Token**:与 `wx.cp.token` 完全一致 - **EncodingAESKey**:与 `wx.cp.aes-key` 完全一致 点「保存」时企业微信会立即发起一次 GET 验证,返回 `echostr` 明文即为通过。