1、新增IAM对接
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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_no,iamId={}", 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user