1、新增IAM对接

This commit is contained in:
2026-07-14 11:54:56 +08:00
parent 23e5e4f57c
commit 5949b9c88c
4 changed files with 435 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.auth.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Map;
/**
* IAM统一身份认证用户信息响应
*
* @author Chill
*/
@Data
public class IamSsoProfileResponse {
/**
* IAM用户主账号
*/
private String id;
/**
* IAM用户扩展属性
*/
private Map<String, Object> attributes;
/**
* 获取本系统账号
*
* @return 本系统账号
*/
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getAccountNo() {
if (attributes == null) {
return null;
}
Object accountNo = attributes.get("account_no");
return accountNo == null ? null : String.valueOf(accountNo);
}
}

View File

@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.auth.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* IAM统一身份认证Token响应
*
* @author Chill
*/
@Data
public class IamSsoTokenResponse {
/**
* IAM访问令牌
*/
@JsonProperty("access_token")
private String accessToken;
/**
* IAM刷新令牌
*/
@JsonProperty("refresh_token")
private String refreshToken;
}

View File

@@ -0,0 +1,244 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.auth.granter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springblade.auth.dto.IamSsoProfileResponse;
import org.springblade.auth.dto.IamSsoTokenResponse;
import org.springblade.auth.props.IamSsoProperties;
import org.springblade.auth.utils.TokenUtil;
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.granter.AuthorizationCodeGranter;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.UserInfo;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
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.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* IAM统一身份认证授权器
*
* @author Chill
*/
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class IamSsoTokenGranter extends AuthorizationCodeGranter {
private static final String IAM_GRANT_TYPE = "authorization_code";
private static final String BEARER_PREFIX = "Bearer ";
private final IUserClient userClient;
private final IamSsoProperties properties;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
public IamSsoTokenGranter(OAuth2ClientService clientService,
OAuth2UserService userService,
PasswordHandler passwordHandler,
BladeRedis bladeRedis,
IUserClient userClient,
IamSsoProperties properties,
ObjectMapper objectMapper) {
super(clientService, userService, passwordHandler, bladeRedis);
this.userClient = userClient;
this.properties = properties;
this.objectMapper = objectMapper;
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
}
@Override
public OAuth2User user(OAuth2Request request) {
if (!isIamRequest(request)) {
return super.user(request);
}
validateRequest(request);
// 先校验本系统客户端IAM的redirect_uri由后续远端接口校验。
client(request);
IamSsoTokenResponse tokenResponse = requestIamToken(request);
String accessToken = tokenResponse.getAccessToken();
if (StringUtil.isBlank(accessToken)) {
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
}
IamSsoProfileResponse profileResponse = requestIamProfile(accessToken);
String accountNo = profileResponse.getAccountNo();
if (StringUtil.isBlank(accountNo)) {
log.warn("IAM统一身份认证用户信息缺少account_noiamId={}", profileResponse.getId());
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
}
R<UserInfo> result = userClient.userInfo(request.getTenantId(), accountNo);
if (!result.isSuccess() || result.getData() == null) {
log.warn("IAM统一身份认证未匹配到本系统账号tenantId={}, accountNo={}", request.getTenantId(), accountNo);
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
}
OAuth2User user = TokenUtil.convertUser(result.getData(), request);
if (user == null) {
throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
}
user.setClient(client(request));
return user;
}
private boolean isIamRequest(OAuth2Request request) {
return StringUtil.isNotBlank(properties.getRedirectUri()) && StringUtil.equals(properties.getRedirectUri(), request.getRedirectUri());
}
private void validateRequest(OAuth2Request request) {
if (StringUtil.isBlank(request.getCode())) {
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
}
if (StringUtil.isAnyBlank(
properties.getTokenUrl(),
properties.getProfileUrl(),
properties.getClientId(),
properties.getClientSecret(),
properties.getRedirectUri()
)) {
throw new UserInvalidException("IAM统一身份认证配置不完整");
}
}
private IamSsoTokenResponse requestIamToken(OAuth2Request request) {
try {
HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(properties.getTokenUrl()))
.timeout(Duration.ofSeconds(10))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(HttpHeaders.AUTHORIZATION, authorizationHeader())
.POST(HttpRequest.BodyPublishers.ofString(buildTokenBody(request), StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = httpClient.send(
httpRequest,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
);
if (response.statusCode() < 200 || response.statusCode() >= 300) {
log.warn("IAM统一身份认证换取Token失败status={}", response.statusCode());
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
}
return objectMapper.readValue(response.body(), IamSsoTokenResponse.class);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
} catch (IOException | IllegalArgumentException exception) {
log.warn("IAM统一身份认证换取Token异常", exception);
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
}
}
private IamSsoProfileResponse requestIamProfile(String accessToken) {
try {
HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(buildProfileUrl(accessToken)))
.timeout(Duration.ofSeconds(10))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, profileAuthorizationHeader(accessToken))
.GET()
.build();
HttpResponse<String> response = httpClient.send(
httpRequest,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
);
if (response.statusCode() < 200 || response.statusCode() >= 300) {
log.warn("IAM统一身份认证获取用户信息失败status={}", response.statusCode());
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
}
return objectMapper.readValue(response.body(), IamSsoProfileResponse.class);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
} catch (IOException | IllegalArgumentException exception) {
log.warn("IAM统一身份认证获取用户信息异常", exception);
throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
}
}
private String buildTokenBody(OAuth2Request request) {
Map<String, String> params = new LinkedHashMap<>();
params.put("grant_type", IAM_GRANT_TYPE);
params.put("client_id", properties.getClientId());
params.put("client_secret", properties.getClientSecret());
params.put("code", request.getCode());
params.put("redirect_uri", properties.getRedirectUri());
return params.entrySet().stream()
.map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
.collect(Collectors.joining("&"));
}
private String buildProfileUrl(String accessToken) {
String separator = properties.getProfileUrl().contains("?") ? "&" : "?";
return properties.getProfileUrl() + separator + "access_token=" + encode(accessToken);
}
private String authorizationHeader() {
if (StringUtil.isNotBlank(properties.getAuthorization())) {
return properties.getAuthorization();
}
String token = Base64.getEncoder().encodeToString(
(properties.getClientId() + ":" + properties.getClientSecret()).getBytes(StandardCharsets.UTF_8)
);
return "Basic " + token;
}
private String profileAuthorizationHeader(String accessToken) {
if (StringUtil.isNotBlank(properties.getProfileAuthorization())) {
return properties.getProfileAuthorization();
}
return BEARER_PREFIX + accessToken;
}
private String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.auth.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* IAM统一身份认证配置
*
* @author Chill
*/
@Data
@ConfigurationProperties(prefix = "iam.sso")
public class IamSsoProperties {
/**
* IAM换取Token接口地址
*/
private String tokenUrl;
/**
* IAM获取用户信息接口地址
*/
private String profileUrl;
/**
* IAM客户端ID
*/
private String clientId;
/**
* IAM客户端密钥
*/
private String clientSecret;
/**
* IAM回调地址
*/
private String redirectUri;
/**
* IAM认证头。为空时默认使用 Basic Base64(clientId:clientSecret)
*/
private String authorization;
/**
* IAM用户信息认证头。为空时默认使用 Bearer accessToken
*/
private String profileAuthorization;
}