调整 IAM

This commit is contained in:
2026-09-16 08:08:09 +08:00
parent ad496fdf28
commit cbc7e16453
7 changed files with 252 additions and 13 deletions
@@ -159,12 +159,23 @@ public class DeptController extends BladeController {
return R.fail("操作失败"); return R.fail("操作失败");
} }
/**
* 同步IAM组织。
*/
@IsAdmin
@PostMapping("/sync-iam-organizations")
@ApiOperationSupport(order = 7)
@Operation(summary = "同步IAM组织")
public R<Integer> syncIamOrganizations() {
return R.data(deptService.syncIamOrganizations());
}
/** /**
* 删除 * 删除
*/ */
@IsAdmin @IsAdmin
@PostMapping("/remove") @PostMapping("/remove")
@ApiOperationSupport(order = 7) @ApiOperationSupport(order = 8)
@Operation(summary = "删除", description = "传入ids") @Operation(summary = "删除", description = "传入ids")
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
CacheUtil.clear(SYS_CACHE); CacheUtil.clear(SYS_CACHE);
@@ -177,7 +188,7 @@ public class DeptController extends BladeController {
*/ */
@PreAuth(AuthConstant.PERMIT_ALL) @PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/select") @GetMapping("/select")
@ApiOperationSupport(order = 8) @ApiOperationSupport(order = 9)
@Operation(summary = "下拉数据源", description = "传入id集合") @Operation(summary = "下拉数据源", description = "传入id集合")
public R<List<Dept>> select(Long userId, String deptId) { public R<List<Dept>> select(Long userId, String deptId) {
if (Func.isNotEmpty(userId)) { if (Func.isNotEmpty(userId)) {
@@ -194,7 +205,7 @@ public class DeptController extends BladeController {
*/ */
@PreAuth(AuthConstant.PERMIT_ALL) @PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/platform-company-select") @GetMapping("/platform-company-select")
@ApiOperationSupport(order = 9) @ApiOperationSupport(order = 10)
@Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表") @Operation(summary = "平台公司下拉", description = "返回是否平台公司=是的部门列表")
public R<List<Dept>> platformCompanySelect() { public R<List<Dept>> platformCompanySelect() {
return R.data(deptService.listPlatformCompany()); return R.data(deptService.listPlatformCompany());
@@ -205,7 +216,7 @@ public class DeptController extends BladeController {
*/ */
@IsAdmin @IsAdmin
@GetMapping("/dept-leader-info") @GetMapping("/dept-leader-info")
@ApiOperationSupport(order = 10) @ApiOperationSupport(order = 11)
@Operation(summary = "获取部门的主管信息", description = "传入deptId") @Operation(summary = "获取部门的主管信息", description = "传入deptId")
public R<List<UserVO>> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) { public R<List<UserVO>> deptLeaderInfo(@Parameter(description = "部门id", required = true) @RequestParam Long deptId) {
List<UserVO> list = deptService.deptLeaderInfo(deptId); List<UserVO> list = deptService.deptLeaderInfo(deptId);
@@ -40,6 +40,9 @@ public class IamSyncProperties {
/** IAM增量账号接口地址。 */ /** IAM增量账号接口地址。 */
private String accountListUrl; private String accountListUrl;
/** IAM组织接口地址。 */
private String orgListUrl;
/** IAM接口Authorization请求头。 */ /** IAM接口Authorization请求头。 */
private String authorization; private String authorization;
@@ -156,6 +156,13 @@ public interface IDeptService extends IService<Dept> {
*/ */
boolean submit(Dept dept); boolean submit(Dept dept);
/**
* 从IAM同步管理租户组织。
* @return 同步处理的组织数量
*/
int syncIamOrganizations();
/** /**
* 按名称与父级查询部门列表(限定当前会话租户) * 按名称与父级查询部门列表(限定当前会话租户)
* *
@@ -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.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.log.exception.ServiceException;
import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Condition;
import org.springblade.core.secure.utils.AuthUtil; 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.entity.User;
import org.springblade.system.pojo.vo.DeptVO; import org.springblade.system.pojo.vo.DeptVO;
import org.springblade.system.pojo.vo.UserVO; import org.springblade.system.pojo.vo.UserVO;
import org.springblade.system.props.IamSyncProperties;
import org.springblade.system.service.IDeptService; import org.springblade.system.service.IDeptService;
import org.springblade.system.service.IUserService; import org.springblade.system.service.IUserService;
import org.springblade.system.wrapper.DeptWrapper; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Pattern; 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;
import static org.springblade.core.tenant.TenantGuard.EntityType.DEPT_PARENT; 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<DeptMapper, Dept> implements ID
private static final String TENANT_ID = "tenantId"; private static final String TENANT_ID = "tenantId";
private static final String PARENT_ID = "parentId"; 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 IUserService userService;
private final IamSyncProperties iamSyncProperties;
private final ObjectMapper objectMapper;
@Override @Override
public Dept getDetail(Dept dept) { public Dept getDetail(Dept dept) {
@@ -255,6 +273,183 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return saveOrUpdate(dept); 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<String, String> 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<String> 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.<Dept>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) { private void validateDeptCategory(Dept dept, Dept parent) {
if (parent == null) { if (parent == null) {
throw new ServiceException("请选择上级组织"); throw new ServiceException("请选择上级组织");
@@ -200,9 +200,7 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
.header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization())) .header("Auth", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization()))
.header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization())) .header("Authorization", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization()))
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8)) .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8))
.build(); .build();
log.info("Auth{}", normalizeAuthorizationHeader(iamSyncProperties.getProfileAuthorization()));
log.info("Authorization{}", normalizeAuthorizationHeader(iamSyncProperties.getAuthorization()));
HttpResponse<String> response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); HttpResponse<String> response = IAM_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) { if (response.statusCode() < 200 || response.statusCode() >= 300) {
@@ -228,19 +226,19 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
} }
private boolean syncIamAccount(JsonNode accountNode) { 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)) { if (StringUtil.isBlank(account)) {
log.warn("IAM账号缺少accountNo,跳过同步"); log.warn("IAM账号缺少accountNo,跳过同步");
return false; return false;
} }
String name = readIamText(accountNode, "name"); String name = readIamText(accountNode, "name", "app_account__name");
if (StringUtil.isBlank(name)) { if (StringUtil.isBlank(name)) {
name = readIamText(accountNode, "accountName"); name = readIamText(accountNode, "accountName", "account_name", "app_account__account_name");
} }
if (StringUtil.isBlank(name)) { if (StringUtil.isBlank(name)) {
name = account; 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(); ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode();
User user = userByAccount(IAM_SYNC_TENANT_ID, account); User user = userByAccount(IAM_SYNC_TENANT_ID, account);
if (user == null) { if (user == null) {
@@ -281,11 +279,26 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> implement
return updateById(user); return updateById(user);
} }
private String readIamText(JsonNode node, String fieldName) { private String readIamText(JsonNode node, String... fieldNames) {
JsonNode valueNode = node.get(fieldName); JsonNode valueNode = findIamNode(node, fieldNames);
return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim(); 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) { private String normalizeAuthorizationHeader(String value) {
if (StringUtil.isBlank(value)) { if (StringUtil.isBlank(value)) {
return StringPool.EMPTY; return StringPool.EMPTY;
@@ -28,6 +28,7 @@ spring:
iam: iam:
sync: sync:
account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST} 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=} authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==}
page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50} page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50}
+9
View File
@@ -107,3 +107,12 @@ baidu:
powerjob: powerjob:
worker: worker:
server-address: 172.16.203.228:7700 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