syncIamAccounts() {
+ return R.data(userService.syncIamAccounts());
+ }
+
/**
* 修改
*/
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
new file mode 100644
index 0000000..b956961
--- /dev/null
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/props/IamSyncProperties.java
@@ -0,0 +1,52 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.system.props;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * IAM账号同步配置。
+ *
+ * @author Chill
+ */
+@Data
+@ConfigurationProperties(prefix = "iam.sync")
+public class IamSyncProperties {
+
+ /** IAM增量账号接口地址。 */
+ private String accountListUrl;
+
+ /** IAM接口Authorization请求头。 */
+ private String authorization;
+
+ /** IAM接口Auth请求头。 */
+ private String profileAuthorization;
+
+ /** 单页请求数量。 */
+ private int pageSize = 50;
+
+}
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java
index 35899e0..366db18 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java
@@ -78,6 +78,13 @@ public interface IUserService extends BaseService {
*/
boolean submit(User user);
+ /**
+ * 从IAM同步管理租户账号。
+ *
+ * @return 同步处理的账号数量
+ */
+ int syncIamAccounts();
+
/**
* 修改用户(租户守卫校验用户归属,含账号 / 手机查重)
*
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 8428dfe..3cc6099 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
@@ -31,7 +31,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.common.constant.DataStatusEnum;
import org.bouncycastle.util.encoders.Hex;
import org.springblade.common.constant.ParamConstant;
import org.springblade.common.constant.TenantConstant;
@@ -63,6 +67,7 @@ import org.springblade.system.pojo.entity.*;
import org.springblade.system.pojo.enums.DictEnum;
import org.springblade.system.pojo.enums.UserType;
import org.springblade.system.pojo.vo.UserVO;
+import org.springblade.system.props.IamSyncProperties;
import org.springblade.system.service.IRoleService;
import org.springblade.system.service.IUserDeptService;
import org.springblade.system.service.IUserOauthService;
@@ -72,6 +77,11 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
+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.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -79,6 +89,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import static org.springblade.common.constant.ParamConstant.DEFAULT_PARAM_PASSWORD;
import static org.springblade.core.cache.constant.CacheConstant.USER_CACHE;
@@ -91,12 +103,17 @@ import static org.springblade.core.tenant.TenantGuard.EntityType.USER;
*/
@Service
@AllArgsConstructor
+@Slf4j
public class UserServiceImpl extends BaseServiceImpl implements IUserService {
private static final String GUEST_NAME = "guest";
private static final String PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
private static final int RANDOM_PASSWORD_LENGTH = 8;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
- private static final Long IAM_DEFAULT_ROLE_ID = 1123598816738675203L;
+ private static final String IAM_DEFAULT_DICT_CODE = "iam_default";
+ private static final String IAM_DEFAULT_ROLE_NAME = "默认角色";
+ private static final String IAM_DEFAULT_DEPT_NAME = "默认部门";
+ private static final String IAM_SYNC_TENANT_ID = "000000";
+ private static final HttpClient IAM_HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
private final IUserDeptService userDeptService;
private final UserDataScopeMapper userDataScopeMapper;
@@ -106,6 +123,8 @@ public class UserServiceImpl extends BaseServiceImpl implement
private final BladeTenantProperties tenantProperties;
private final OAuth2Properties properties;
+ private final IamSyncProperties iamSyncProperties;
+ private final ObjectMapper objectMapper;
@Override
@@ -133,6 +152,147 @@ public class UserServiceImpl extends BaseServiceImpl implement
return saveUser(user);
}
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int syncIamAccounts() {
+ int pageNumber = 1;
+ int fetchedCount = 0;
+ int syncedCount = 0;
+ int totalCount = -1;
+ int pageSize = iamSyncProperties.getPageSize() > 0 ? iamSyncProperties.getPageSize() : 50;
+ while (true) {
+ JsonNode dataNode = requestIamAccountPage(pageNumber, pageSize);
+ JsonNode accountList = dataNode.path("list");
+ if (!accountList.isArray() || accountList.isEmpty()) {
+ break;
+ }
+ if (dataNode.has("total")) {
+ totalCount = dataNode.path("total").asInt(totalCount);
+ }
+ for (JsonNode accountNode : accountList) {
+ if (syncIamAccount(accountNode)) {
+ syncedCount++;
+ }
+ }
+ fetchedCount += accountList.size();
+ int responsePage = dataNode.path("page").asInt(pageNumber);
+ int responseSize = dataNode.path("size").asInt(pageSize);
+ if ((totalCount >= 0 && fetchedCount >= totalCount)
+ || accountList.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 requestIamAccountPage(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.getAccountListUrl()))
+ .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 syncIamAccount(JsonNode accountNode) {
+ String account = readIamText(accountNode, "accountNo");
+ if (StringUtil.isBlank(account)) {
+ log.warn("IAM账号缺少accountNo,跳过同步");
+ return false;
+ }
+ String name = readIamText(accountNode, "name");
+ if (StringUtil.isBlank(name)) {
+ name = readIamText(accountNode, "accountName");
+ }
+ if (StringUtil.isBlank(name)) {
+ name = account;
+ }
+ Integer status = accountNode.path("status").asInt(0) == 1
+ ? DataStatusEnum.ENABLE.getCode() : DataStatusEnum.DISABLE.getCode();
+ User user = userByAccount(IAM_SYNC_TENANT_ID, account);
+ if (user == null) {
+ user = new User();
+ user.setTenantId(IAM_SYNC_TENANT_ID);
+ user.setAccount(account);
+ user.setName(name);
+ user.setRealName(name);
+ user.setRoleId(resolveIamDefaultRoleId());
+ user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
+ user.setPostId(StringPool.MINUS_ONE);
+ user.setUserType(UserType.WEB.getCategory());
+ user.setStatus(status);
+ user.setIsOa(1);
+ user.setSyncTime(new Date());
+ applyUserDefaults(user);
+ return saveUser(user);
+ }
+ boolean changed = !Objects.equals(user.getName(), name) || !Objects.equals(user.getRealName(), name)
+ || !Objects.equals(user.getStatus(), status) || !Objects.equals(user.getIsOa(), 1);
+ if (isMissingIamRole(user.getRoleId())) {
+ user.setRoleId(resolveIamDefaultRoleId());
+ changed = true;
+ }
+ if (isMissingIamAssignment(user.getDeptId())) {
+ user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
+ changed = true;
+ }
+ if (!changed) {
+ return true;
+ }
+ user.setName(name);
+ user.setRealName(name);
+ user.setStatus(status);
+ user.setIsOa(1);
+ user.setSyncTime(new Date());
+ CacheUtil.clear(USER_CACHE);
+ return updateById(user);
+ }
+
+ private String readIamText(JsonNode node, String fieldName) {
+ JsonNode valueNode = node.get(fieldName);
+ return valueNode == null || valueNode.isNull() ? StringPool.EMPTY : valueNode.asText().trim();
+ }
+
+ 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;
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(User user) {
@@ -572,6 +732,10 @@ public class UserServiceImpl extends BaseServiceImpl implement
existingUser.setRoleId(defaultRoleId);
changed = true;
}
+ if (isMissingIamAssignment(existingUser.getDeptId())) {
+ existingUser.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME));
+ changed = true;
+ }
if (existingUser.getIsOa() == null || existingUser.getIsOa() != 1) {
existingUser.setIsOa(1);
existingUser.setSyncTime(new Date());
@@ -604,13 +768,19 @@ public class UserServiceImpl extends BaseServiceImpl implement
}
private String resolveIamDefaultRoleId() {
- Role role = roleService.getOne(Wrappers.lambdaQuery()
- .eq(Role::getId, IAM_DEFAULT_ROLE_ID)
- .eq(Role::getIsDeleted, BladeConstant.DB_NOT_DELETED));
- if (role == null || role.getId() == null) {
- throw new ServiceException("IAM用户默认角色不存在,roleId=" + IAM_DEFAULT_ROLE_ID);
+ return resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME);
+ }
+
+ private String resolveIamDefaultId(String dictValue) {
+ String dictKey = DictCache.getKey(IAM_DEFAULT_DICT_CODE, dictValue);
+ if (StringUtil.isBlank(dictKey) || StringPool.MINUS_ONE.equals(dictKey)) {
+ throw new ServiceException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue));
}
- return String.valueOf(role.getId());
+ return dictKey;
+ }
+
+ private boolean isMissingIamAssignment(String value) {
+ return StringUtil.isBlank(value) || StringPool.MINUS_ONE.equals(value);
}
@Override
diff --git a/blade-service/blade-system/src/main/resources/application.yml b/blade-service/blade-system/src/main/resources/application.yml
index 28509cf..549ebd0 100644
--- a/blade-service/blade-system/src/main/resources/application.yml
+++ b/blade-service/blade-system/src/main/resources/application.yml
@@ -23,3 +23,11 @@ spring:
url: ${blade.datasource.${spring.profiles.active}.url}
username: ${blade.datasource.${spring.profiles.active}.username}
password: ${blade.datasource.${spring.profiles.active}.password}
+
+# IAM账号同步
+iam:
+ sync:
+ account-list-url: ${IAM_SSO_ACCOUNT_LIST_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_IDM_ACCOUNT_LIST}
+ authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
+ profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}
+ page-size: ${IAM_SSO_ACCOUNT_PAGE_SIZE:50}