diff --git a/blade-auth/pom.xml b/blade-auth/pom.xml index b088727..5640463 100644 --- a/blade-auth/pom.xml +++ b/blade-auth/pom.xml @@ -63,6 +63,10 @@ org.springblade blade-user-api + + org.springblade + blade-dict-api + org.springblade blade-system-api diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index 1e8a6f0..8763a21 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -45,6 +45,7 @@ import org.springblade.core.redis.cache.BladeRedis; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.StringUtil; import org.springblade.core.tool.utils.StringPool; +import org.springblade.system.cache.DictCache; import org.springblade.system.feign.IUserClient; import org.springblade.system.pojo.entity.User; import org.springblade.system.pojo.entity.UserInfo; @@ -86,7 +87,9 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { private static final String IAM_TOKEN_URI = "/iam/sso/token"; private static final String BEARER_PREFIX = "Bearer "; private static final String BASIC_PREFIX = "Basic "; - private static final String IAM_DEFAULT_ROLE_ID = "1123598816738675203"; + 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 final OAuth2ClientService clientService; private final IUserClient userClient; @@ -155,6 +158,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { log.warn("IAM统一身份认证请求缺少租户ID,accountNo={}", accountNo); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); } + log.info("IAM统一身份认证开始匹配本系统账号,tenantId={}, accountNo={}", tenantId, accountNo); UserInfo userInfo = loadOrCreateIamUser(request, profileResponse, tenantId, accountNo); OAuth2User user = TokenUtil.convertUser(userInfo, request); if (user == null) { @@ -174,7 +178,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return result.getData(); } if (result.isSuccess() && hasUser(result.getData())) { - log.info("IAM统一身份认证账号缺少可登录角色,开始补齐默认角色,tenantId={}, accountNo={}", tenantId, accountNo); + log.info("IAM统一身份认证账号缺少有效角色或部门,开始补齐默认配置,tenantId={}, accountNo={}", tenantId, accountNo); } else { log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}, querySuccess={}", tenantId, accountNo, result.isSuccess()); @@ -210,7 +214,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { } private boolean hasLoginAccess(UserInfo userInfo) { - return hasUser(userInfo) && hasRoles(userInfo); + return hasUser(userInfo) && hasRoles(userInfo) && hasValidAssignment(userInfo.getUser()); + } + + private boolean hasValidAssignment(User user) { + return StringUtil.isNotBlank(user.getRoleId()) + && !StringPool.MINUS_ONE.equals(user.getRoleId()) + && StringUtil.isNotBlank(user.getDeptId()) + && !StringPool.MINUS_ONE.equals(user.getDeptId()); } private User buildIamUser(IamSsoProfileResponse profileResponse, String tenantId, String accountNo) { @@ -221,8 +232,8 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { user.setPassword(UUID.randomUUID().toString()); user.setName(accountNo); user.setRealName(accountNo); - user.setRoleId(IAM_DEFAULT_ROLE_ID); - user.setDeptId(StringPool.MINUS_ONE); + user.setRoleId(resolveIamDefaultId(IAM_DEFAULT_ROLE_NAME)); + user.setDeptId(resolveIamDefaultId(IAM_DEFAULT_DEPT_NAME)); user.setPostId(StringPool.MINUS_ONE); user.setIsOa(1); user.setSyncTime(new Date()); @@ -231,6 +242,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return user; } + 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 UserInvalidException(StringUtil.format("IAM默认配置 [{}] 未配置有效键值", dictValue)); + } + return dictKey; + } + public boolean supports(OAuth2Request request) { return isIamRequest(request, false); } diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index 586debc..714ddbe 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -100,5 +100,5 @@ iam: system-client-id: ${IAM_SSO_SYSTEM_CLIENT_ID:saber3} system-client-secret: ${IAM_SSO_SYSTEM_CLIENT_SECRET:saber3_secret} redirect-uri: ${IAM_SSO_REDIRECT_URI:http://172.16.203.228:8000/callback} - authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + authorization: ${IAM_SSO_AUTHORIZATION:YjNlZmU0ODEwMzJiNGJhZTpkYzQ5NDY1NmQ4MzE0NThjODI5MzlmNzA2ZjliNDY3MQ==} profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java index bbad903..b55719f 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/SystemApplication.java @@ -28,13 +28,16 @@ package org.springblade.system; import org.springblade.core.cloud.client.BladeCloudApplication; import org.springblade.core.launch.BladeApplication; import org.springblade.core.launch.constant.AppConstant; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; +import org.springblade.system.props.IamSyncProperties; /** * 系统模块启动器 * @author Chill */ @ComponentScan(basePackages = {"org.springblade.system", "org.springblade.process"}) +@EnableConfigurationProperties(IamSyncProperties.class) @BladeCloudApplication public class SystemApplication { @@ -44,4 +47,3 @@ public class SystemApplication { } } - diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java index 328fd2a..a4b7b6b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/UserController.java @@ -157,6 +157,17 @@ public class UserController { return R.status(userService.submit(user)); } + /** + * 同步IAM账号。 + */ + @IsAdmin + @PostMapping("/sync-iam-accounts") + @ApiOperationSupport(order = 6) + @Operation(summary = "同步IAM账号") + public R 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}