From cbc7e16453dd46a6eafc8ac508fb53a88d74a80e Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 08:08:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20IAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controller/DeptController.java | 19 +- .../system/props/IamSyncProperties.java | 3 + .../system/service/IDeptService.java | 7 + .../system/service/impl/DeptServiceImpl.java | 195 ++++++++++++++++++ .../system/service/impl/UserServiceImpl.java | 31 ++- .../src/main/resources/application.yml | 1 + doc/nacos/blade-prod.yaml | 9 + 7 files changed, 252 insertions(+), 13 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 11c4081..9cccc5d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -159,12 +159,23 @@ public class DeptController extends BladeController { return R.fail("操作失败"); } + /** + * 同步IAM组织。 + */ + @IsAdmin + @PostMapping("/sync-iam-organizations") + @ApiOperationSupport(order = 7) + @Operation(summary = "同步IAM组织") + public R syncIamOrganizations() { + return R.data(deptService.syncIamOrganizations()); + } + /** * 删除 */ @IsAdmin @PostMapping("/remove") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 8) @Operation(summary = "删除", description = "传入ids") public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { CacheUtil.clear(SYS_CACHE); @@ -177,7 +188,7 @@ public class DeptController extends BladeController { */ @PreAuth(AuthConstant.PERMIT_ALL) @GetMapping("/select") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 9) @Operation(summary = "下拉数据源", description = "传入id集合") public R> select(Long userId, String deptId) { if (Func.isNotEmpty(userId)) { @@ -194,7 +205,7 @@ public class DeptController extends BladeController { */ @PreAuth(AuthConstant.PERMIT_ALL) @GetMapping("/platform-company-select") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 10) @Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表") public R> platformCompanySelect() { return R.data(deptService.listPlatformCompany()); @@ -205,7 +216,7 @@ public class DeptController extends BladeController { */ @IsAdmin @GetMapping("/dept-leader-info") - @ApiOperationSupport(order = 10) + @ApiOperationSupport(order = 11) @Operation(summary = "获取部门的主管信息", description = "传入deptId") public R> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) { List list = deptService.deptLeaderInfo(deptId); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java index b956961..2cbc9a3 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java @@ -40,6 +40,9 @@ public class IamSyncProperties { /** IAM增量账号接口地址。 */ private String accountListUrl; + /** IAM组织接口地址。 */ + private String orgListUrl; + /** IAM接口Authorization请求头。 */ private String authorization; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java index 48e5102..9aef013 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IDeptService.java @@ -156,6 +156,13 @@ public interface IDeptService extends IService { */ boolean submit(Dept dept); + /** + * 从IAM同步管理租户组织。 + + * @return 同步处理的组织数量 + */ + int syncIamOrganizations(); + /** * 按名称与父级查询部门列表(限定当前会话租户) * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java index ff25a47..bf43f44 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java @@ -29,8 +29,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springblade.common.constant.DataStatusEnum; +import org.springblade.core.cache.utils.CacheUtil; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.support.Condition; import org.springblade.core.secure.utils.AuthUtil; @@ -44,6 +48,7 @@ import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.vo.DeptVO; import org.springblade.system.pojo.vo.UserVO; +import org.springblade.system.props.IamSyncProperties; import org.springblade.system.service.IDeptService; import org.springblade.system.service.IUserService; import org.springblade.system.wrapper.DeptWrapper; @@ -52,6 +57,13 @@ import org.springblade.thirdparty.oa.constant.OAConvertConstant; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; @@ -59,6 +71,7 @@ import java.util.stream.Collectors; import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT; import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT_PARENT; +import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE; /** * 服务实现类 @@ -72,8 +85,13 @@ public class DeptServiceImpl extends ServiceImpl implements ID private static final String TENANT_ID = "tenantId"; private static final String PARENT_ID = "parentId"; + private static final String IAM_SYNC_TENANT_ID = "000000"; + private static final Long IAM_SYNC_PARENT_ID = 1123598813738675201L; + private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); private final IUserService userService; + private final IamSyncProperties iamSyncProperties; + private final ObjectMapper objectMapper; @Override public Dept getDetail(Dept dept) { @@ -255,6 +273,183 @@ public class DeptServiceImpl extends ServiceImpl implements ID return saveOrUpdate(dept); } + @Override + @Transactional(rollbackFor = Exception.class) + public int syncIamOrganizations() { + int pageNumber = 1; + int fetchedCount = 0; + int syncedCount = 0; + int totalCount = -1; + int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50; + while (true) { + JsonNode dataNode = requestIamOrgPage(pageNumber, pageSize); + JsonNode orgList = dataNode.path("list"); + if (!orgList.isArray() || orgList.isEmpty()) { + break; + } + if (dataNode.has("total")) { + totalCount = dataNode.path("total").asInt(totalCount); + } + for (JsonNode orgNode : orgList) { + if (syncIamOrganization(orgNode)) { + syncedCount++; + } + } + fetchedCount += orgList.size(); + int responsePage = dataNode.path("page").asInt(pageNumber); + int responseSize = dataNode.path("size").asInt(pageSize); + if ((totalCount >= 0 && fetchedCount >= totalCount) + || orgList.size() < pageSize + || (totalCount >= 0 && responsePage * responseSize >= totalCount)) { + break; + } + pageNumber = responsePage + 1; + } + log.info("IAM组织同步完成,tenantId={}, fetchedCount={}, syncedCount={}", IAM_SYNC_TENANT_ID, fetchedCount, syncedCount); + return syncedCount; + } + + private JsonNode requestIamOrgPage(int pageNumber, int pageSize) { + try { + Map requestBody = new LinkedHashMap<>(); + requestBody.put("size", String.valueOf(pageSize)); + requestBody.put("page", String.valueOf(pageNumber)); + HttpRequest request = HttpRequest.newBuilder(URI.create(iamSyncProperties.getOrgListUrl())) + .timeout(Duration.ofSeconds(20)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())) + .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) + .build(); + HttpResponse response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new ServiceException(StringUtil.format("IAM组织接口调用失败,HTTP状态码:{}", response.statusCode())); + } + JsonNode responseNode = objectMapper.readTree(response.body()); + if (!"0".equals(responseNode.path("code").asText())) { + throw new ServiceException(StringUtil.format("IAM组织接口调用失败:{}", responseNode.path("msg").asText())); + } + JsonNode dataNode = responseNode.path("data"); + if (!dataNode.isObject()) { + throw new ServiceException("IAM组织接口返回数据格式错误"); + } + return dataNode; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + log.error("调用IAM组织接口被中断,page={}", pageNumber, exception); + throw new ServiceException("调用IAM组织接口被中断"); + } catch (IOException | IllegalArgumentException exception) { + log.error("调用IAM组织接口失败,page={}", pageNumber, exception); + throw new ServiceException("调用IAM组织接口失败"); + } + } + + private boolean syncIamOrganization(JsonNode orgNode) { + String orgCode = readIamText(orgNode, "orgCode", "org_code", "organizationCode", "organization_code", + "code", "app_org__org_code", "app_org__org_no", "app_org__organization_code", "app_org__code"); + String orgId = readIamText(orgNode, "orgId", "org_id", "id", "app_org__id", "app_org__org_id"); + if (StringUtil.isBlank(orgCode)) { + orgCode = orgId; + } + if (StringUtil.isBlank(orgCode)) { + log.warn("IAM组织缺少组织编码,跳过同步"); + return false; + } + if (orgCode.length() > 30) { + log.warn("IAM组织编码超过30个字符,跳过同步,orgCode={}", orgCode); + return false; + } + String orgName = readIamText(orgNode, "name", "orgName", "org_name", "organizationName", "organization_name", + "fullName", "app_org__name", "app_org__org_name", "app_org__org_full_name", "app_org__organization_name"); + if (StringUtil.isBlank(orgName)) { + log.warn("IAM组织缺少组织名称,跳过同步,orgCode={}", orgCode); + return false; + } + Integer status = readIamInt(orgNode, "status", "org_status", "app_org__status", "app_org__org_status") == 1 + ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode(); + Dept dept = getOne(Wrappers.lambdaQuery() + .eq(Dept::getTenantId, IAM_SYNC_TENANT_ID) + .eq(Dept::getDeptCode, orgCode), false); + if (dept == null) { + dept = new Dept(); + dept.setTenantId(IAM_SYNC_TENANT_ID); + dept.setParentId(IAM_SYNC_PARENT_ID); + dept.setAncestors(resolveIamParentAncestors()); + dept.setDeptCode(orgCode); + dept.setDeptName(orgName); + dept.setFullName(orgName); + dept.setShortName(orgName); + dept.setDeptCategory(1); + dept.setSort(0); + dept.setStatus(status); + dept.setIsDeleted(BladeConstant.DB_NOT_DELETED); + dept.setIsOa(1); + dept.setIsPlatformCompany(0); + dept.setSyncTime(new Date()); + boolean saved = save(dept); + if (saved) { + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + } + return saved; + } + boolean changed = !Objects.equals(dept.getDeptName(), orgName) || !Objects.equals(dept.getFullName(), orgName) + || !Objects.equals(dept.getShortName(), orgName) || !Objects.equals(dept.getStatus(), status) + || !Objects.equals(dept.getIsOa(), 1); + if (!changed) { + return true; + } + dept.setDeptName(orgName); + dept.setFullName(orgName); + dept.setShortName(orgName); + dept.setStatus(status); + dept.setIsOa(1); + dept.setSyncTime(new Date()); + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + return updateById(dept); + } + + private String resolveIamParentAncestors() { + Dept parent = getById(IAM_SYNC_PARENT_ID); + String ancestors = parent == null ? String.valueOf(BladeConstant.TOP_PARENT_ID) : parent.getAncestors(); + if (StringUtil.isBlank(ancestors)) { + ancestors = String.valueOf(BladeConstant.TOP_PARENT_ID); + } + return ancestors + StringPool.COMMA + IAM_SYNC_PARENT_ID; + } + + private String readIamText(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); + return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim(); + } + + private int readIamInt(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); + return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0); + } + + private JsonNode findIamNode(JsonNode node, String... fieldNames) { + for (String fieldName : fieldNames) { + JsonNode valueNode = node.get(fieldName); + if (valueNode != null && !valueNode.isNull()) { + return valueNode; + } + } + return null; + } + + private String normalizeAuthorizationHeader(String value) { + if (StringUtil.isBlank(value)) { + return StringPool.EMPTY; + } + if (StringUtil.startsWithIgnoreCase(value, "Basic ") || StringUtil.startsWithIgnoreCase(value, "Bearer ")) { + return value; + } + return "Basic " + value; + } + private void validateDeptCategory(Dept dept, Dept parent) { if (parent == null) { throw new ServiceException("请选择上级组织"); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index c738d73..3eb7d43 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -200,9 +200,7 @@ public class UserServiceImpl extends BaseServiceImpl implement .header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())) .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) - .build(); - log.info("Auth,{}", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())); - log.info("Authorization,{}", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())); + .build(); HttpResponse response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() < 200 || response.statusCode() >= 300) { @@ -228,19 +226,19 @@ public class UserServiceImpl extends BaseServiceImpl implement } private boolean syncIamAccount(JsonNode accountNode) { - String account = readIamText(accountNode, "accountNo"); + String account = readIamText(accountNode, "accountNo", "account_no", "app_account__account_no"); if (StringUtil.isBlank(account)) { log.warn("IAM账号缺少accountNo,跳过同步"); return false; } - String name = readIamText(accountNode, "name"); + String name = readIamText(accountNode, "name", "app_account__name"); if (StringUtil.isBlank(name)) { - name = readIamText(accountNode, "accountName"); + name = readIamText(accountNode, "accountName", "account_name", "app_account__account_name"); } if (StringUtil.isBlank(name)) { name = account; } - Integer status = accountNode.path("status").asInt(0) == 1 + Integer status = readIamInt(accountNode, "status", "app_account__status") == 1 ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode(); User user = userByAccount(IAM_SYNC_TENANT_ID, account); if (user == null) { @@ -281,11 +279,26 @@ public class UserServiceImpl extends BaseServiceImpl implement return updateById(user); } - private String readIamText(JsonNode node, String fieldName) { - JsonNode valueNode = node.get(fieldName); + private String readIamText(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim(); } + private int readIamInt(JsonNode node, String... fieldNames) { + JsonNode valueNode = findIamNode(node, fieldNames); + return valueNode == null || valueNode.isNull() ? 0 : valueNode.asInt(0); + } + + private JsonNode findIamNode(JsonNode node, String... fieldNames) { + for (String fieldName : fieldNames) { + JsonNode valueNode = node.get(fieldName); + if (valueNode != null && !valueNode.isNull()) { + return valueNode; + } + } + return null; + } + private String normalizeAuthorizationHeader(String value) { if (StringUtil.isBlank(value)) { return StringPool.EMPTY; diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml index f3492fa..1d8d630 100644 --- a/blade-service/blade-system/src/main/resources/application.yml +++ b/blade-service/blade-system/src/main/resources/application.yml @@ -28,6 +28,7 @@ spring: iam: sync: account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST} + org-list-url: ${IAM_SSO_ORG_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ORG_LIST} authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 16c1b02..7a749a0 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -107,3 +107,12 @@ baidu: powerjob: worker: server-address: 172.16.203.228:7700 + +# IAM账号与组织同步配置 +iam: + sync: + account-list-url: http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST + org-list-url: http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ORG_LIST + authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} + page-size: 50