docs/wxcp_login_integration.md
@@ -114,11 +114,30 @@ | # | 配置位置 | 内容 | 缺失后果 | |---|----------|------|----------| | 1 | `application-<profile>.yaml` → `wx.cp.*` | corp-id、agent-id、secret、token、aes-key | 回调接口无法解密,URL 验证失败 | | 2 | `application-<profile>.yaml` → `justauth.type.WECHAT_ENTERPRISE` | client-id、client-secret、agent-id、redirect-uri | 调用 `social-auth-redirect` 抛 `NullPointerException` | | 3 | 数据库 `system_social_client` | social_type=30、user_type=1、client_id=CorpId、client_secret=Secret、agent_id、status=0、deleted=0 | `social-auth-redirect` 报"社交平台(30) 不存在" | | 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接收」: yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
@@ -99,8 +99,11 @@ // 获取 JustAuth 中已存在的 AuthRequest authRequest = getDefaultRequest(source); // 如果获取不到则尝试取自定义的 if (authRequest == null) { // 如果获取不到则尝试取自定义的。 // 注意:JustAuthProperties 的 extend 没有默认值,未配置 justauth.extend 时 getExtend() 返回 null。 // 此时应直接跳过自定义分支,交给下方抛 AuthException(可读的"不支持的平台"), // 而不是在这里 getEnumClass() 上空指针,让调用方拿到一个 500 NPE。 if (authRequest == null && properties.getExtend() != null) { authRequest = getExtendRequest(properties.getExtend().getEnumClass(), source); } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; @@ -26,6 +27,8 @@ import cn.iocoder.yudao.module.system.service.logger.LoginLogService; import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; import cn.iocoder.yudao.module.system.service.permission.PermissionService; import cn.iocoder.yudao.module.system.service.permission.RoleService; import cn.iocoder.yudao.module.system.service.social.SocialUserService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.anji.captcha.model.common.ResponseModel; @@ -43,6 +46,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Objects; import java.util.Set; import java.util.UUID; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -62,6 +66,14 @@ * 企业微信社交类型,对应 {@link cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum#WECHAT_ENTERPRISE} */ private static final Integer WECHAT_ENTERPRISE_TYPE = 30; /** * 企业微信扫码自动注册的账号,默认绑定的角色编码。 * * 需要在「系统管理 → 角色管理」里建好该编码的角色并勾选菜单,否则新账号登录后没有任何菜单。 * 编码而非角色编号:角色编号在本地/测试/生产各库不一致,写死会在换库后静默失效。 */ private static final String WXCP_DEFAULT_ROLE_CODE = "wxcp_user"; @Resource private AdminUserService userService; @@ -85,6 +97,10 @@ private PasswordEncoder passwordEncoder; @Resource private ApplicationEventPublisher eventPublisher; @Resource private RoleService roleService; @Resource private PermissionService permissionService; /** * 验证码的开关,默认为 true @@ -267,9 +283,35 @@ user.setStatus(CommonStatusEnum.ENABLE.getStatus()); userMapper.insert(user); // 5. 绑定默认角色 // 不绑的话账号能登录,但 get-permission-info 返回空菜单,进去一片空白; // 这种"能进但什么都看不到"的状态不会报错,很难定位,所以注册时就绑上 bindDefaultRole(user); return user; } /** * 给自动注册的账号绑定默认角色 * * 角色不存在时只打警告、不抛异常:一个配置遗漏不应该让扫码登录整条链路不可用, * 而且警告日志里已经写明了去哪儿补。 * * @param user 自动注册出来的用户 */ private void bindDefaultRole(AdminUserDO user) { RoleDO role = roleService.getRoleByCode(WXCP_DEFAULT_ROLE_CODE); if (role == null) { log.warn("[bindDefaultRole][角色编码({}) 不存在,用户({}) 将没有任何菜单权限;" + "请在「系统管理 → 角色管理」新建编码为 {} 的角色并分配菜单]", WXCP_DEFAULT_ROLE_CODE, user.getUsername(), WXCP_DEFAULT_ROLE_CODE); return; } permissionService.assignUserRole(user.getId(), Set.of(role.getId())); log.info("[bindDefaultRole][用户({}) 已绑定默认角色({}/{})]", user.getUsername(), role.getCode(), role.getName()); } @VisibleForTesting void validateCaptcha(AuthLoginReqVO reqVO) { ResponseModel response = doValidateCaptcha(reqVO); yudao-server/src/main/resources/application-local.yaml
@@ -242,18 +242,5 @@ signup-url-prefix: http://wcf.viphk.nnhk.cc qrcode-size: 400 # 报名二维码边长(像素),不配置默认 400 --- #################### 社交登录(JustAuth)配置 #################### # 企业微信扫码登录走 JustAuth。 # 注意:SocialClientServiceImpl.buildAuthRequest 会先用这里的配置构造 AuthRequest, # 再拿 system_social_client 表里同类型的记录覆盖 clientId / clientSecret / agentId。 # 所以这里必须先配一份,否则 AuthRequestFactory 取不到 WECHAT_ENTERPRISE 的配置会抛空指针。 justauth: enabled: true type: WECHAT_ENTERPRISE: # 企业微信扫码登录,对应 SocialTypeEnum.WECHAT_ENTERPRISE(30) client-id: ww2b7707c8d15ad73d client-secret: MhfEn_b6XfPtZHygmNEpUtecFTQgrnPgMude6jeFSsg agent-id: 1000003 # redirect-uri 刻意不配:由前端按当前浏览器地址动态传,配置固定值会导致 # 「redirect_uri 与配置的授权完成回调域名不一致」 # 注:企业微信扫码登录的 JustAuth 占位配置已上移到 application.yaml(对所有 profile 生效), # 不在此处重复配置,避免只有 local 有、换 profile 就抛 NPE。 yudao-server/src/main/resources/application.yaml
@@ -47,7 +47,7 @@ # 2. true: 启动时会对数据库中所有表进行更新操作,如果表存在,不做处理,反之,自动创建表 # 3. create_drop: 启动时自动创建表,关闭时自动删除表 # 4. drop_create: 启动时,删除旧表,再创建新表 database-schema-update: true # 设置为 false,可通过 https://github.com/flowable/flowable-sql 初始化 database-schema-update: false # 设置为 false,可通过 https://github.com/flowable/flowable-sql 初始化 db-history-used: true # flowable6 默认 true 生成信息表,无需手动设置 check-process-definitions: false # 设置为 false,禁用 /resources/processes 自动部署 BPMN XML 流程 history-level: audit # full:保存历史数据的最高级别,可保存全部流程相关细节,包括流程流转各节点参数 @@ -196,6 +196,14 @@ security: permit-all_urls: - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 - /admin-api/wxcp/callback/** # 企业微信回调接口,不需要登录 - /admin-api/wxcp/activity/signup-page # 活动报名落地页,扫码打开,不需要登录 - /admin-api/wxcp/activity/signup # 活动报名提交,不需要登录 # 报名页需要上传图片/附件,而 system 的上传接口默认要求登录。 # 这里只放开「公共上传」这一个已有接口(不新建 upload 接口), # 注意:放开后任何能访问到该地址的人都可以上传文件,属于有意的权衡。 # 如需收紧,可改为让报名页先走企业微信网页授权拿令牌后再上传。 - /admin-api/system/storage-blob/public-upload # 活动报名页附件上传 api-encrypt: enable: true # 是否开启 API 加密 algorithm: AES # 加密算法,支持 AES、RSA 等 @@ -275,4 +283,31 @@ message-bus: type: redis # 消息总线的类型 --- #################### 社交登录(JustAuth)配置 #################### # 企业微信扫码登录的「占位配置」,刻意放在 application.yaml 而非某个 profile, # 以保证所有 profile 都有,不会再出现「只有 local 配了、换 profile 就抛 NPE」。 # # 为什么必须存在:SocialClientServiceImpl.buildAuthRequest 会先用 yaml 里的配置构造 AuthRequest, # 再拿 system_social_client 表里同类型的记录覆盖 clientId / clientSecret / agentId。 # 而 YudaoJustAuthConfiguration 的 @ConditionalOnProperty(matchIfMissing = true) # 让 AuthRequestFactory 无论配没配都会创建,一旦这里缺失,调用链会回落到 # justauth.extend 分支,而 extend 默认为 null,直接抛 NullPointerException。 # # 注意:下面这些值只是让 AuthRequestFactory 能构造出 AuthWeChatEnterpriseQrcodeRequest 的占位, # 真正生效的凭据来自数据库 system_social_client(social_type=30、user_type=1)。 justauth: enabled: true type: WECHAT_ENTERPRISE: # 企业微信扫码登录,对应 SocialTypeEnum.WECHAT_ENTERPRISE(30) client-id: ww2b7707c8d15ad73d client-secret: MhfEn_b6XfPtZHygmNEpUtecFTQgrnPgMude6jeFSsg agent-id: 1000003 # redirect-uri 不配:由前端按当前浏览器地址动态传,SocialClientServiceImpl.getAuthorizeUrl # 会用 HttpUtils.replaceUrlQuery 把 redirect_uri 换成前端传进来的值。 # 所以必须同时打开下面这个开关 —— JustAuth 的 AuthDefaultRequest 构造时会调 # AuthChecker.checkConfig,redirectUri 为空且未忽略校验时直接抛 # AuthException: Illegal redirect uri [WECHAT_ENTERPRISE]。 ignore-check-redirect-uri: true debug: false