Merge branch 'master' of http://1.14.125.6:3000/mianbin/ops-pro
This commit is contained in:
commit
8e36c94130
@ -112,4 +112,15 @@ public interface OAuth2GrantService {
|
|||||||
*/
|
*/
|
||||||
boolean revokeToken(String clientId, String accessToken);
|
boolean revokeToken(String clientId, String accessToken);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 ID Token
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @param clientId 客户端ID
|
||||||
|
* @param scopes 授权范围
|
||||||
|
* @return ID Token
|
||||||
|
*/
|
||||||
|
String grantIDToken(Long userId, Integer userType, String clientId, List<String> scopes, String nonce);
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,29 @@
|
|||||||
package cd.casic.framework.tenant.core.service;
|
package cd.casic.framework.tenant.core.service;
|
||||||
|
|
||||||
|
|
||||||
|
import cd.casic.framework.commons.enums.UserTypeEnum;
|
||||||
|
import cd.casic.framework.datapermission.service.user.AdminUserService;
|
||||||
import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
|
import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
|
||||||
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
|
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
|
||||||
import cd.casic.framework.security.dal.oauth2.OAuth2CodeDO;
|
import cd.casic.framework.security.dal.oauth2.OAuth2CodeDO;
|
||||||
import cd.casic.framework.security.dal.user.AdminUserDO;
|
import cd.casic.framework.security.dal.user.AdminUserDO;
|
||||||
import cd.casic.framework.security.oauth2.OAuth2CodeService;
|
import cd.casic.framework.security.oauth2.OAuth2CodeService;
|
||||||
|
import cd.casic.module.system.enums.ErrorCodeConstants;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cd.casic.framework.commons.enums.UserTypeEnum;
|
import com.nimbusds.jose.JOSEObjectType;
|
||||||
import cd.casic.module.system.enums.ErrorCodeConstants;
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
|
import com.nimbusds.jose.JWSHeader;
|
||||||
|
import com.nimbusds.jose.JWSSigner;
|
||||||
|
import com.nimbusds.jose.crypto.MACSigner;
|
||||||
|
import com.nimbusds.jwt.JWTClaimsSet;
|
||||||
|
import com.nimbusds.jwt.SignedJWT;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
|
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
|
||||||
@ -24,6 +33,7 @@ import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exc
|
|||||||
*
|
*
|
||||||
* @author mianbin modified from yudao
|
* @author mianbin modified from yudao
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
||||||
|
|
||||||
@ -33,6 +43,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
|||||||
private OAuth2CodeService oauth2CodeService;
|
private OAuth2CodeService oauth2CodeService;
|
||||||
@Resource
|
@Resource
|
||||||
private AdminAuthService adminAuthService;
|
private AdminAuthService adminAuthService;
|
||||||
|
@Resource
|
||||||
|
private AdminUserService adminUserService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
|
public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
|
||||||
@ -104,4 +116,61 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
|||||||
return oauth2TokenService.removeAccessToken(accessToken) != null;
|
return oauth2TokenService.removeAccessToken(accessToken) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String grantIDToken(Long userId, Integer userType, String clientId, List<String> scopes, String nonce) {
|
||||||
|
try {
|
||||||
|
// 获取用户信息
|
||||||
|
AdminUserDO user = adminUserService.getUser(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new IllegalArgumentException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 nimbus-jose-jwt 库生成 ID Token
|
||||||
|
// JWT 头部
|
||||||
|
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256)
|
||||||
|
.type(JOSEObjectType.JWT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// JWT 载荷
|
||||||
|
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
|
||||||
|
.issuer("http://localhost:48080") // iss: 发行者
|
||||||
|
.subject(String.valueOf(userId)) // sub: 主题
|
||||||
|
.audience(clientId) // aud: 受众
|
||||||
|
.expirationTime(new Date(System.currentTimeMillis() + 3600000)) // exp: 过期时间 (1小时)
|
||||||
|
.issueTime(new Date()) // iat: 签发时间
|
||||||
|
.claim("auth_time", new Date()); // auth_time: 认证时间
|
||||||
|
|
||||||
|
// 添加 nonce(如果提供)
|
||||||
|
if (StrUtil.isNotBlank(nonce)) {
|
||||||
|
claimsBuilder.claim("nonce", nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 OIDC 标准声明
|
||||||
|
if (scopes.contains("profile")) {
|
||||||
|
claimsBuilder.claim("name", user.getNickname());
|
||||||
|
claimsBuilder.claim("preferred_username", user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.contains("email")) {
|
||||||
|
claimsBuilder.claim("email", user.getEmail());
|
||||||
|
claimsBuilder.claim("email_verified", StrUtil.isNotBlank(user.getEmail()));
|
||||||
|
}
|
||||||
|
|
||||||
|
JWTClaimsSet claimsSet = claimsBuilder.build();
|
||||||
|
|
||||||
|
// 签名
|
||||||
|
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
|
||||||
|
|
||||||
|
// 创建 HMAC signer (实际项目中应使用配置的密钥)
|
||||||
|
JWSSigner signer = new MACSigner("your-oidc-secret-key-min-32-chars"); // 密钥至少32字符
|
||||||
|
|
||||||
|
signedJWT.sign(signer);
|
||||||
|
|
||||||
|
return signedJWT.serialize();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成 ID Token 失败", e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -106,4 +106,14 @@ public class OAuth2ClientDO extends BaseDO {
|
|||||||
*/
|
*/
|
||||||
private String additionalInformation;
|
private String additionalInformation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否支持 OIDC
|
||||||
|
*/
|
||||||
|
private Boolean oidcSupport = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认响应类型
|
||||||
|
*/
|
||||||
|
private String defaultResponseType = "code";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,12 @@ public class OAuth2ClientSaveReqVO {
|
|||||||
@Schema(description = "附加信息", example = "{yunai: true}")
|
@Schema(description = "附加信息", example = "{yunai: true}")
|
||||||
private String additionalInformation;
|
private String additionalInformation;
|
||||||
|
|
||||||
|
@Schema(description = "是否支持 OIDC", example = "true")
|
||||||
|
private Boolean oidcSupport = false;
|
||||||
|
|
||||||
|
@Schema(description = "默认响应类型", example = "code id_token")
|
||||||
|
private String defaultResponseType = "code";
|
||||||
|
|
||||||
@AssertTrue(message = "附加信息必须是 JSON 格式")
|
@AssertTrue(message = "附加信息必须是 JSON 格式")
|
||||||
public boolean isAdditionalInformationJson() {
|
public boolean isAdditionalInformationJson() {
|
||||||
return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation);
|
return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cd.casic.ci.process.properties;
|
package cd.casic.ci.commons.properties;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
@ -27,6 +27,10 @@
|
|||||||
<groupId>cd.casic.boot</groupId>
|
<groupId>cd.casic.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cd.casic.boot</groupId>
|
||||||
|
<artifactId>module-ci-machine</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.docker-java</groupId>
|
<groupId>com.github.docker-java</groupId>
|
||||||
<artifactId>docker-java</artifactId>
|
<artifactId>docker-java</artifactId>
|
||||||
@ -61,6 +65,10 @@
|
|||||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||||
<version>3.1.0</version>
|
<version>3.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.jcraft</groupId>
|
||||||
|
<artifactId>jsch</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import cd.casic.module.execute.docker.dataobject.model.DockerImage;
|
|||||||
import cd.casic.module.execute.docker.dataobject.vo.DockerImagePageReqVO;
|
import cd.casic.module.execute.docker.dataobject.vo.DockerImagePageReqVO;
|
||||||
import cd.casic.module.execute.docker.service.IImageService;
|
import cd.casic.module.execute.docker.service.IImageService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@ -47,4 +49,14 @@ public class DockerImageController {
|
|||||||
return success(imageService.getLocalImagePage(pageVO));
|
return success(imageService.getLocalImagePage(pageVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/localImagePush")
|
||||||
|
@Operation(summary = "镜像推送,或者部署到目标服务器")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = "imageId", description = "镜像id"),
|
||||||
|
@Parameter(name = "machineId", description = "主机id")
|
||||||
|
})
|
||||||
|
@PreAuthorize("@ss.hasPermission('docker:images:localImagePush')")
|
||||||
|
public CommonResult localImagePush(@RequestParam(value = "imageId") String imageId, @RequestParam("machineId") String machineId) {
|
||||||
|
return success(imageService.localImagePush(imageId,machineId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package cd.casic.module.execute.docker.dataobject.convert;
|
|
||||||
|
|
||||||
import cd.casic.module.execute.docker.dataobject.dto.DockerImageDo;
|
|
||||||
import cd.casic.module.execute.docker.dataobject.model.DockerImage;
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
import org.mapstruct.factory.Mappers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author Yuru Pu
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2025/7/23 17:16
|
|
||||||
*/
|
|
||||||
@Mapper(componentModel = "spring")
|
|
||||||
public interface DockerImageConvert {
|
|
||||||
DockerImageConvert INSTANCE = Mappers.getMapper(DockerImageConvert.class);
|
|
||||||
|
|
||||||
DockerImageDo convert(DockerImage dockerImage);
|
|
||||||
|
|
||||||
}
|
|
@ -1,8 +1,6 @@
|
|||||||
package cd.casic.module.execute.docker.dataobject.model;
|
package cd.casic.module.execute.docker.dataobject.model;
|
||||||
|
|
||||||
import cd.casic.framework.commons.dataobject.BaseDO;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本地镜像
|
* 本地镜像
|
||||||
@ -12,8 +10,7 @@ import lombok.experimental.Accessors;
|
|||||||
* @since 2025/7/23 16:22
|
* @since 2025/7/23 16:22
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Accessors(chain = true)
|
public class DockerImage {
|
||||||
public class DockerImage extends BaseDO {
|
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
|
@ -109,4 +109,6 @@ public interface IImageService {
|
|||||||
int localImageUpload(DockerImage dockerImage);
|
int localImageUpload(DockerImage dockerImage);
|
||||||
|
|
||||||
PageResult<DockerImageDo> getLocalImagePage(DockerImagePageReqVO pageVO);
|
PageResult<DockerImageDo> getLocalImagePage(DockerImagePageReqVO pageVO);
|
||||||
|
|
||||||
|
Boolean localImagePush(String imageId, String machineId);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
package cd.casic.module.execute.docker.service.impl;
|
package cd.casic.module.execute.docker.service.impl;
|
||||||
|
|
||||||
|
import cd.casic.ci.commons.properties.TargetFileUploadProperties;
|
||||||
|
import cd.casic.framework.commons.exception.ServerException;
|
||||||
import cd.casic.framework.commons.pojo.PageResult;
|
import cd.casic.framework.commons.pojo.PageResult;
|
||||||
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import cd.casic.module.execute.docker.DockerClientFactory;
|
import cd.casic.module.execute.docker.DockerClientFactory;
|
||||||
import cd.casic.module.execute.docker.dao.DockerImageDao;
|
import cd.casic.module.execute.docker.dao.DockerImageDao;
|
||||||
import cd.casic.module.execute.docker.dataobject.convert.DockerImageConvert;
|
|
||||||
import cd.casic.module.execute.docker.dataobject.dto.DockerImageDo;
|
import cd.casic.module.execute.docker.dataobject.dto.DockerImageDo;
|
||||||
import cd.casic.module.execute.docker.dataobject.model.DockerImage;
|
import cd.casic.module.execute.docker.dataobject.model.DockerImage;
|
||||||
import cd.casic.module.execute.docker.dataobject.vo.DockerImagePageReqVO;
|
import cd.casic.module.execute.docker.dataobject.vo.DockerImagePageReqVO;
|
||||||
import cd.casic.module.execute.docker.service.IImageService;
|
import cd.casic.module.execute.docker.service.IImageService;
|
||||||
|
import cd.casic.module.machine.contants.MachineErrorCodeConstants;
|
||||||
|
import cd.casic.module.machine.dal.dataobject.MachineInfoDO;
|
||||||
|
import cd.casic.module.machine.dal.dataobject.SecretKeyDO;
|
||||||
|
import cd.casic.module.machine.dal.mysql.MachineInfoMapper;
|
||||||
|
import cd.casic.module.machine.dal.mysql.SecretKeyMapper;
|
||||||
|
import cd.casic.module.machine.enums.AuthenticationType;
|
||||||
|
import cd.casic.module.machine.utils.CryptogramUtil;
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import cn.hutool.extra.ftp.FtpConfig;
|
||||||
|
import cn.hutool.extra.ssh.JschUtil;
|
||||||
|
import cn.hutool.extra.ssh.Sftp;
|
||||||
import com.github.dockerjava.api.DockerClient;
|
import com.github.dockerjava.api.DockerClient;
|
||||||
import com.github.dockerjava.api.command.InspectImageResponse;
|
import com.github.dockerjava.api.command.InspectImageResponse;
|
||||||
import com.github.dockerjava.api.command.SaveImageCmd;
|
import com.github.dockerjava.api.command.SaveImageCmd;
|
||||||
import com.github.dockerjava.api.exception.NotFoundException;
|
import com.github.dockerjava.api.exception.NotFoundException;
|
||||||
import com.github.dockerjava.api.model.Image;
|
import com.github.dockerjava.api.model.Image;
|
||||||
import com.github.dockerjava.api.model.PruneType;
|
import com.github.dockerjava.api.model.PruneType;
|
||||||
|
import com.jcraft.jsch.Session;
|
||||||
import jakarta.annotation.Nonnull;
|
import jakarta.annotation.Nonnull;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -24,11 +37,11 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
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.FileOutputStream;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 镜像的服务类
|
* @description: 镜像的服务类
|
||||||
@ -46,6 +59,15 @@ public class ImageService implements IImageService {
|
|||||||
@Resource
|
@Resource
|
||||||
private DockerImageDao dockerImageDao;
|
private DockerImageDao dockerImageDao;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private MachineInfoMapper machineInfoMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TargetFileUploadProperties fileUploadProperties;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SecretKeyMapper secretKeyMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Image> list(@Nonnull String clientId) {
|
public List<Image> list(@Nonnull String clientId) {
|
||||||
DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
|
DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId);
|
||||||
@ -144,7 +166,9 @@ public class ImageService implements IImageService {
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
public int localImageUpload(DockerImage dockerImage) {
|
public int localImageUpload(DockerImage dockerImage) {
|
||||||
return dockerImageDao.insert(DockerImageConvert.INSTANCE.convert(dockerImage));
|
DockerImageDo imageDo = new DockerImageDo();
|
||||||
|
BeanUtil.copyProperties(dockerImage,imageDo);
|
||||||
|
return dockerImageDao.insert(imageDo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -153,4 +177,66 @@ public class ImageService implements IImageService {
|
|||||||
PageResult<DockerImageDo> page = dockerImageDao.selectPage(pageVO, queryWrapperX.likeIfPresent(DockerImageDo::getName, pageVO.getName()).orderByDesc(DockerImageDo::getCreateTime));
|
PageResult<DockerImageDo> page = dockerImageDao.selectPage(pageVO, queryWrapperX.likeIfPresent(DockerImageDo::getName, pageVO.getName()).orderByDesc(DockerImageDo::getCreateTime));
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 先下载后上传到目标主机 先确定目标主机是否可连接在继续
|
||||||
|
* @param imageId
|
||||||
|
* @param machineId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Boolean localImagePush(String imageId, String machineId) {
|
||||||
|
if (Objects.isNull(imageId) || Objects.isNull(machineId)) {
|
||||||
|
throw new ServerException(MachineErrorCodeConstants.MACHINE_INFO_TAG_NULL);
|
||||||
|
}
|
||||||
|
// 1 建立连接 区分密码还是秘钥
|
||||||
|
MachineInfoDO machineInfoDO = machineInfoMapper.selectById(machineId);
|
||||||
|
FtpConfig targetFtp = FtpConfig.create().setHost(machineInfoDO.getHostIp()).setPort(machineInfoDO.getSshPort()).setUser(machineInfoDO.getUsername()).setPassword(CryptogramUtil.doDecrypt(machineInfoDO.getPassword()));
|
||||||
|
if (AuthenticationType.of(machineInfoDO.getAuthenticationType()).equals(AuthenticationType.SECRET_KEY)) {
|
||||||
|
SecretKeyDO secretKeyDO = secretKeyMapper.selectById(machineInfoDO.getSecretKeyId());
|
||||||
|
targetFtp.setSystemKey(CryptogramUtil.doDecrypt(secretKeyDO.getPrivateKey()));
|
||||||
|
targetFtp.setPassword(CryptogramUtil.doDecrypt(secretKeyDO.getPassword()));
|
||||||
|
}
|
||||||
|
// 2 拿到目标主机连接
|
||||||
|
Sftp uploadSftp = getSftp(targetFtp);
|
||||||
|
|
||||||
|
//远程下载
|
||||||
|
DockerImageDo imageDo = dockerImageDao.selectById(imageId);
|
||||||
|
Sftp sftp = getSftp(FtpConfig.create().setHost(fileUploadProperties.getRemoteHost()).setPort(fileUploadProperties.getRemotePort()).setUser(fileUploadProperties.getUsername()).setPassword(fileUploadProperties.getPassword()));
|
||||||
|
String srcPath = imageDo.getPath();
|
||||||
|
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
||||||
|
sftp.download(srcPath, byteOut); //远程下载到中间缓存区
|
||||||
|
sftp.close();
|
||||||
|
//远程上传
|
||||||
|
String suffix = FileUtil.getSuffix(srcPath);
|
||||||
|
String fileName = imageDo.getName() + "." + suffix;
|
||||||
|
ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(byteOut.toByteArray()); //转换
|
||||||
|
String romPath = "/home/image/" + UUID.randomUUID()+"/";
|
||||||
|
uploadSftp.mkDirs(romPath);//递归创建
|
||||||
|
uploadSftp.upload(romPath, fileName, arrayInputStream);
|
||||||
|
uploadSftp.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* 获取Sftp连接
|
||||||
|
* @param config 连接配置
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private Sftp getSftp(FtpConfig config) {
|
||||||
|
Session session;
|
||||||
|
Integer timeOut = 5000;
|
||||||
|
try {
|
||||||
|
if (Objects.nonNull(config.getSystemKey())) {
|
||||||
|
session = JschUtil.openSession(config.getHost(), config.getPort(), config.getUser(), config.getSystemKey().getBytes(StandardCharsets.UTF_8), config.getPassword().getBytes(), timeOut);
|
||||||
|
} else {
|
||||||
|
session = JschUtil.openSession(config.getHost(), config.getPort(), config.getUser(), config.getPassword(), timeOut);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("与主机IP:{} 建立SSH连接失败", config.getHost());
|
||||||
|
throw new RuntimeException("部署失败,请检测主机是否可用", e);
|
||||||
|
}
|
||||||
|
return new Sftp(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package cd.casic.ci.process.process.service.sftpFile.impl;
|
|||||||
|
|
||||||
import cd.casic.ci.commons.sftp.SftpClientUtils;
|
import cd.casic.ci.commons.sftp.SftpClientUtils;
|
||||||
import cd.casic.ci.process.process.service.sftpFile.SftpFileService;
|
import cd.casic.ci.process.process.service.sftpFile.SftpFileService;
|
||||||
import cd.casic.ci.process.properties.TargetFileUploadProperties;
|
import cd.casic.ci.commons.properties.TargetFileUploadProperties;
|
||||||
import com.amazonaws.util.IOUtils;
|
import com.amazonaws.util.IOUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
@ -15,7 +15,7 @@ import cd.casic.ci.process.process.dataObject.target.TargetVersion;
|
|||||||
import cd.casic.ci.process.process.service.pipeline.PipelineService;
|
import cd.casic.ci.process.process.service.pipeline.PipelineService;
|
||||||
import cd.casic.ci.process.process.service.sftpFile.impl.SftpFileServiceImpl;
|
import cd.casic.ci.process.process.service.sftpFile.impl.SftpFileServiceImpl;
|
||||||
import cd.casic.ci.process.process.service.target.TargetManagerService;
|
import cd.casic.ci.process.process.service.target.TargetManagerService;
|
||||||
import cd.casic.ci.process.properties.TargetFileUploadProperties;
|
import cd.casic.ci.commons.properties.TargetFileUploadProperties;
|
||||||
import cd.casic.ci.process.util.SftpUploadUtil;
|
import cd.casic.ci.process.util.SftpUploadUtil;
|
||||||
import cd.casic.framework.commons.exception.ServiceException;
|
import cd.casic.framework.commons.exception.ServiceException;
|
||||||
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
|
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
|
||||||
|
@ -13,7 +13,7 @@ import cd.casic.ci.process.process.dataObject.testCase.TestCaseInfo;
|
|||||||
import cd.casic.ci.process.process.dataObject.testCase.TestCaseManager;
|
import cd.casic.ci.process.process.dataObject.testCase.TestCaseManager;
|
||||||
import cd.casic.ci.process.process.service.testCase.TestCaseInfoService;
|
import cd.casic.ci.process.process.service.testCase.TestCaseInfoService;
|
||||||
import cd.casic.ci.process.process.service.testCase.TestCaseManagerService;
|
import cd.casic.ci.process.process.service.testCase.TestCaseManagerService;
|
||||||
import cd.casic.ci.process.properties.TargetFileUploadProperties;
|
import cd.casic.ci.commons.properties.TargetFileUploadProperties;
|
||||||
import cd.casic.framework.commons.exception.ServiceException;
|
import cd.casic.framework.commons.exception.ServiceException;
|
||||||
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
|
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
|
||||||
import cd.casic.framework.commons.pojo.PageResult;
|
import cd.casic.framework.commons.pojo.PageResult;
|
||||||
|
@ -1,38 +1,41 @@
|
|||||||
package cd.casic.module.system.controller.admin.oauth2;
|
package cd.casic.module.system.controller.admin.oauth2;
|
||||||
|
|
||||||
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
|
|
||||||
import cd.casic.framework.security.dal.oauth2.OAuth2ApproveDO;
|
|
||||||
import cd.casic.framework.security.dal.oauth2.OAuth2ClientDO;
|
|
||||||
import cn.hutool.core.lang.Assert;
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cd.casic.framework.commons.enums.UserTypeEnum;
|
import cd.casic.framework.commons.enums.UserTypeEnum;
|
||||||
import cd.casic.framework.commons.pojo.CommonResult;
|
import cd.casic.framework.commons.pojo.CommonResult;
|
||||||
import cd.casic.framework.commons.util.http.HttpUtils;
|
import cd.casic.framework.commons.util.http.HttpUtils;
|
||||||
import cd.casic.framework.commons.util.json.JsonUtils;
|
import cd.casic.framework.commons.util.json.JsonUtils;
|
||||||
|
import cd.casic.framework.datapermission.service.user.AdminUserService;
|
||||||
|
import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
|
||||||
|
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
|
||||||
|
import cd.casic.framework.security.dal.oauth2.OAuth2ApproveDO;
|
||||||
|
import cd.casic.framework.security.dal.oauth2.OAuth2ClientDO;
|
||||||
|
import cd.casic.framework.security.oauth2.OAuth2ApproveService;
|
||||||
|
import cd.casic.framework.security.oauth2.OAuth2ClientService;
|
||||||
import cd.casic.framework.security.vo.vo.open.OAuth2OpenAccessTokenRespVO;
|
import cd.casic.framework.security.vo.vo.open.OAuth2OpenAccessTokenRespVO;
|
||||||
import cd.casic.framework.security.vo.vo.open.OAuth2OpenAuthorizeInfoRespVO;
|
import cd.casic.framework.security.vo.vo.open.OAuth2OpenAuthorizeInfoRespVO;
|
||||||
import cd.casic.framework.security.vo.vo.open.OAuth2OpenCheckTokenRespVO;
|
import cd.casic.framework.security.vo.vo.open.OAuth2OpenCheckTokenRespVO;
|
||||||
|
import cd.casic.framework.tenant.core.service.OAuth2GrantService;
|
||||||
import cd.casic.module.system.convert.oauth2.OAuth2OpenConvert;
|
import cd.casic.module.system.convert.oauth2.OAuth2OpenConvert;
|
||||||
import cd.casic.module.system.enums.oauth2.OAuth2GrantTypeEnum;
|
import cd.casic.module.system.enums.oauth2.OAuth2GrantTypeEnum;
|
||||||
import cd.casic.framework.security.oauth2.OAuth2ApproveService;
|
|
||||||
import cd.casic.framework.security.oauth2.OAuth2ClientService;
|
|
||||||
import cd.casic.framework.tenant.core.service.OAuth2GrantService;
|
|
||||||
import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
|
|
||||||
import cd.casic.module.system.util.oauth2.OAuth2Utils;
|
import cd.casic.module.system.util.oauth2.OAuth2Utils;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.Parameters;
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import java.time.ZoneId;
|
||||||
import jakarta.annotation.security.PermitAll;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -69,6 +72,8 @@ public class OAuth2OpenController {
|
|||||||
private OAuth2ApproveService oauth2ApproveService;
|
private OAuth2ApproveService oauth2ApproveService;
|
||||||
@Resource
|
@Resource
|
||||||
private OAuth2TokenService oauth2TokenService;
|
private OAuth2TokenService oauth2TokenService;
|
||||||
|
@Resource
|
||||||
|
private AdminUserService adminUserService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
|
* 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
|
||||||
@ -181,13 +186,36 @@ public class OAuth2OpenController {
|
|||||||
@GetMapping("/authorize")
|
@GetMapping("/authorize")
|
||||||
@Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
|
@Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
|
||||||
@Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou")
|
@Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou")
|
||||||
public CommonResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId) {
|
public CommonResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId,
|
||||||
|
@RequestParam(value = "response_type", required = false) String responseType) {
|
||||||
|
// // 0. 校验用户已经登录。通过 Spring Security 实现
|
||||||
|
//
|
||||||
|
// // 1. 获得 Client 客户端的信息
|
||||||
|
// OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
|
||||||
|
// // 2. 获得用户已经授权的信息
|
||||||
|
// List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
|
||||||
|
// // 拼接返回
|
||||||
|
// return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 0. 校验用户已经登录。通过 Spring Security 实现
|
// 0. 校验用户已经登录。通过 Spring Security 实现
|
||||||
|
|
||||||
// 1. 获得 Client 客户端的信息
|
// 1. 获得 Client 客户端的信息
|
||||||
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
|
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
|
||||||
// 2. 获得用户已经授权的信息
|
|
||||||
|
// 2. 检查是否为 OIDC 请求
|
||||||
|
boolean isOIDCRequest = isOIDCRequest(responseType, client);
|
||||||
|
|
||||||
|
// 3. 获得用户已经授权的信息
|
||||||
List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
|
List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
|
||||||
|
|
||||||
|
// 4. 如果是 OIDC 请求,确保 openid scope 被包含
|
||||||
|
if (isOIDCRequest) {
|
||||||
|
// 可以在这里添加特殊的处理逻辑
|
||||||
|
}
|
||||||
|
|
||||||
// 拼接返回
|
// 拼接返回
|
||||||
return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
|
return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
|
||||||
}
|
}
|
||||||
@ -217,9 +245,11 @@ public class OAuth2OpenController {
|
|||||||
@RequestParam(value = "scope", required = false) String scope,
|
@RequestParam(value = "scope", required = false) String scope,
|
||||||
@RequestParam("redirect_uri") String redirectUri,
|
@RequestParam("redirect_uri") String redirectUri,
|
||||||
@RequestParam(value = "auto_approve") Boolean autoApprove,
|
@RequestParam(value = "auto_approve") Boolean autoApprove,
|
||||||
@RequestParam(value = "state", required = false) String state) {
|
@RequestParam(value = "state", required = false) String state,
|
||||||
|
@RequestParam(value = "nonce", required = false) String nonce) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
|
|
||||||
|
Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
|
||||||
scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
|
scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
|
||||||
// 0. 校验用户已经登录。通过 Spring Security 实现
|
// 0. 校验用户已经登录。通过 Spring Security 实现
|
||||||
|
|
||||||
@ -229,6 +259,9 @@ public class OAuth2OpenController {
|
|||||||
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
|
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
|
||||||
grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
|
grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
|
||||||
|
|
||||||
|
// 检查是否为 OIDC 请求
|
||||||
|
boolean isOIDCRequest = checkOIDCRequest(responseType, scopes);
|
||||||
|
|
||||||
// 2.1 假设 approved 为 null,说明是场景一
|
// 2.1 假设 approved 为 null,说明是场景一
|
||||||
if (Boolean.TRUE.equals(autoApprove)) {
|
if (Boolean.TRUE.equals(autoApprove)) {
|
||||||
// 如果无法自动授权通过,则返回空 url,前端不进行跳转
|
// 如果无法自动授权通过,则返回空 url,前端不进行跳转
|
||||||
@ -243,12 +276,17 @@ public class OAuth2OpenController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向
|
// 3. 根据不同的响应类型处理
|
||||||
List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
|
List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
|
||||||
if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
|
if (isOIDCRequest && responseType.contains("id_token")) {
|
||||||
|
// 处理包含 ID Token 的响应类型
|
||||||
|
return handleOIDCResponse(grantTypeEnum, getLoginUserId(), client, approveScopes, redirectUri, state, responseType, nonce);
|
||||||
|
} else if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
|
||||||
return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
|
return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
|
||||||
|
} else if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
|
||||||
|
return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
|
||||||
}
|
}
|
||||||
// 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向
|
|
||||||
return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
|
return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,4 +332,85 @@ public class OAuth2OpenController {
|
|||||||
return clientIdAndSecret;
|
return clientIdAndSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加判断是否为 OIDC 请求的方法
|
||||||
|
private boolean isOIDCRequest(String responseType, OAuth2ClientDO client) {
|
||||||
|
// 检查客户端是否支持 OIDC
|
||||||
|
if (client.getOidcSupport() == null || !client.getOidcSupport()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 response_type 是否包含 OIDC 相关类型
|
||||||
|
if (responseType != null &&
|
||||||
|
(responseType.contains("id_token") || "token".equals(responseType))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查客户端默认响应类型
|
||||||
|
if (client.getDefaultResponseType() != null &&
|
||||||
|
(client.getDefaultResponseType().contains("id_token") ||
|
||||||
|
"token".equals(client.getDefaultResponseType()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 检查是否为 OIDC 请求
|
||||||
|
private boolean checkOIDCRequest(String responseType, Map<String, Boolean> scopes) {
|
||||||
|
// 检查 scope 中是否包含 openid
|
||||||
|
if (scopes.containsKey("openid")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 responseType 是否包含 id_token
|
||||||
|
if (responseType != null && responseType.contains("id_token")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 处理 OIDC 响应
|
||||||
|
private CommonResult<String> handleOIDCResponse(OAuth2GrantTypeEnum grantTypeEnum,
|
||||||
|
Long userId,
|
||||||
|
OAuth2ClientDO client,
|
||||||
|
List<String> scopes,
|
||||||
|
String redirectUri,
|
||||||
|
String state,
|
||||||
|
String responseType,
|
||||||
|
String nonce) {
|
||||||
|
if (responseType.equals("code id_token")) {
|
||||||
|
// Hybrid Flow
|
||||||
|
String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(
|
||||||
|
userId, getUserType(), client.getClientId(), scopes, redirectUri, state);
|
||||||
|
|
||||||
|
// 创建 ID Token
|
||||||
|
String idToken = createIDTokenWithNonce(userId, client.getClientId(), scopes, nonce);
|
||||||
|
|
||||||
|
return success(OAuth2Utils.buildHybridRedirectUri(redirectUri, authorizationCode, idToken, state));
|
||||||
|
} else if (responseType.equals("id_token")) {
|
||||||
|
// Implicit Flow with ID Token only
|
||||||
|
String idToken = createIDTokenWithNonce(userId, client.getClientId(), scopes, nonce);
|
||||||
|
return success(OAuth2Utils.buildIDTokenRedirectUri(redirectUri, idToken, state));
|
||||||
|
} else if (responseType.equals("id_token token")) {
|
||||||
|
// Implicit Flow with Access Token and ID Token
|
||||||
|
OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(
|
||||||
|
userId, getUserType(), client.getClientId(), scopes);
|
||||||
|
String idToken = createIDTokenWithNonce(userId, client.getClientId(), scopes, nonce);
|
||||||
|
return success(OAuth2Utils.buildImplicitWithIDTokenRedirectUri(
|
||||||
|
redirectUri, accessTokenDO.getAccessToken(), idToken, state, Date.from(accessTokenDO.getExpiresTime().atZone(ZoneId.systemDefault()).toInstant())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当没有匹配的响应类型时,抛出异常
|
||||||
|
throw exception0(BAD_REQUEST.getCode(), StrUtil.format("不支持的 OIDC 响应类型: {}", responseType));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 ID Token
|
||||||
|
// 添加带 nonce 的 ID Token 创建方法
|
||||||
|
private String createIDTokenWithNonce(Long userId, String clientId, List<String> scopes, String nonce) {
|
||||||
|
return oauth2GrantService.grantIDToken(userId, getUserType(), clientId, scopes, nonce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
package cd.casic.module.system.controller.admin.oauth2;
|
||||||
|
|
||||||
|
import cd.casic.framework.commons.pojo.CommonResult;
|
||||||
|
import cd.casic.framework.datapermission.service.user.AdminUserService;
|
||||||
|
import cd.casic.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import cd.casic.framework.security.dal.user.AdminUserDO;
|
||||||
|
import cd.casic.module.system.dal.dataobject.oidc.OIDCUserInfoRespVO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author HopeLi
|
||||||
|
* @version v1.0
|
||||||
|
* @ClassName OIDCUserInfoController
|
||||||
|
* @Date: 2025/7/28 10:56
|
||||||
|
* @Description:
|
||||||
|
*/
|
||||||
|
@Tag(name = "管理后台 - OIDC 用户信息")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/system/oidc")
|
||||||
|
@Validated
|
||||||
|
@Slf4j
|
||||||
|
public class OIDCUserInfoController {
|
||||||
|
@Resource
|
||||||
|
private AdminUserService userService;
|
||||||
|
|
||||||
|
@GetMapping("/userinfo")
|
||||||
|
@Operation(summary = "获取 OIDC 用户信息")
|
||||||
|
@PreAuthorize("@ss.hasScope('openid')")
|
||||||
|
public CommonResult<OIDCUserInfoRespVO> getUserInfo() {
|
||||||
|
// 获得用户基本信息
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
AdminUserDO user = userService.getUser(userId);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return CommonResult.error(404, "用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
OIDCUserInfoRespVO userInfo = new OIDCUserInfoRespVO();
|
||||||
|
userInfo.setSub(String.valueOf(user.getId()));
|
||||||
|
userInfo.setName(user.getNickname());
|
||||||
|
userInfo.setNickname(user.getNickname());
|
||||||
|
userInfo.setEmail(user.getEmail());
|
||||||
|
// 如果有头像字段可以设置
|
||||||
|
// userInfo.setPicture(user.getAvatar());
|
||||||
|
|
||||||
|
return CommonResult.success(userInfo);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package cd.casic.module.system.dal.dataobject.oidc;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author HopeLi
|
||||||
|
* @version v1.0
|
||||||
|
* @ClassName OIDCUserInfoRespVO
|
||||||
|
* @Date: 2025/7/28 10:21
|
||||||
|
* @Description:
|
||||||
|
*/
|
||||||
|
@Schema(description = "OIDC 用户信息 Response VO")
|
||||||
|
@Data
|
||||||
|
public class OIDCUserInfoRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "用户子标识", example = "123")
|
||||||
|
private String sub;
|
||||||
|
|
||||||
|
@Schema(description = "用户名", example = "tudou")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "用户昵称", example = "土豆")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱", example = "tudou@iocoder.cn")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "头像地址", example = "https://www.iocoder.cn/avatar.jpg")
|
||||||
|
private String picture;
|
||||||
|
|
||||||
|
// 可根据需要添加更多标准字段
|
||||||
|
}
|
@ -100,4 +100,80 @@ public class OAuth2Utils {
|
|||||||
return StrUtil.split(scope, ' ');
|
return StrUtil.split(scope, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为 OIDC 请求
|
||||||
|
*/
|
||||||
|
public static boolean isOIDCRequest(String responseType, String scope) {
|
||||||
|
// 检查 scope 中是否包含 openid
|
||||||
|
if (StrUtil.isNotBlank(scope) && scope.contains("openid")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 responseType 是否包含 id_token
|
||||||
|
if (StrUtil.isNotBlank(responseType) && responseType.contains("id_token")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建带有 ID Token 的重定向 URI (response_type=id_token)
|
||||||
|
*/
|
||||||
|
public static String buildIDTokenRedirectUri(String redirectUri, String idToken, String state) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(redirectUri);
|
||||||
|
if (!redirectUri.contains("?")) {
|
||||||
|
sb.append("?");
|
||||||
|
} else {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append("id_token=").append(idToken);
|
||||||
|
if (StrUtil.isNotBlank(state)) {
|
||||||
|
sb.append("&state=").append(state);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建带有 Access Token 和 ID Token 的重定向 URI (response_type=id_token token)
|
||||||
|
*/
|
||||||
|
public static String buildImplicitWithIDTokenRedirectUri(String redirectUri, String accessToken,
|
||||||
|
String idToken, String state, Date expiresTime) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(redirectUri);
|
||||||
|
if (!redirectUri.contains("?")) {
|
||||||
|
sb.append("?");
|
||||||
|
} else {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append("access_token=").append(accessToken);
|
||||||
|
sb.append("&id_token=").append(idToken);
|
||||||
|
sb.append("&token_type=Bearer");
|
||||||
|
sb.append("&expires_in=").append((expiresTime.getTime() - System.currentTimeMillis()) / 1000);
|
||||||
|
if (StrUtil.isNotBlank(state)) {
|
||||||
|
sb.append("&state=").append(state);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建混合流程的重定向 URI (response_type=code id_token)
|
||||||
|
*/
|
||||||
|
public static String buildHybridRedirectUri(String redirectUri, String code, String idToken, String state) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(redirectUri);
|
||||||
|
if (!redirectUri.contains("?")) {
|
||||||
|
sb.append("?");
|
||||||
|
} else {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append("code=").append(code);
|
||||||
|
sb.append("&id_token=").append(idToken);
|
||||||
|
if (StrUtil.isNotBlank(state)) {
|
||||||
|
sb.append("&state=").append(state);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -205,7 +205,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
|||||||
when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves);
|
when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves);
|
||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId);
|
CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId,null);
|
||||||
// 断言
|
// 断言
|
||||||
assertEquals(0, result.getCode());
|
assertEquals(0, result.getCode());
|
||||||
assertPojoEquals(client, result.getData().getClient());
|
assertPojoEquals(client, result.getData().getClient());
|
||||||
@ -218,7 +218,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
|||||||
public void testApproveOrDeny_grantTypeError() {
|
public void testApproveOrDeny_grantTypeError() {
|
||||||
// 调用,并断言
|
// 调用,并断言
|
||||||
assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
|
assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
|
||||||
null, null, null, null),
|
null, null, null, null,null),
|
||||||
new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
|
new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
|||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||||
scope, redirectUri, true, state);
|
scope, redirectUri, true, state,null);
|
||||||
// 断言
|
// 断言
|
||||||
assertEquals(0, result.getCode());
|
assertEquals(0, result.getCode());
|
||||||
assertNull(result.getData());
|
assertNull(result.getData());
|
||||||
@ -258,7 +258,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
|||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||||
scope, redirectUri, false, state);
|
scope, redirectUri, false, state,null);
|
||||||
// 断言
|
// 断言
|
||||||
assertEquals(0, result.getCode());
|
assertEquals(0, result.getCode());
|
||||||
assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData());
|
assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData());
|
||||||
@ -287,7 +287,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
|||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||||
scope, redirectUri, true, state);
|
scope, redirectUri, true, state,null);
|
||||||
// 断言
|
// 断言
|
||||||
assertEquals(0, result.getCode());
|
assertEquals(0, result.getCode());
|
||||||
assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算
|
assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算
|
||||||
@ -319,7 +319,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
|||||||
|
|
||||||
// 调用
|
// 调用
|
||||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||||
scope, redirectUri, false, state);
|
scope, redirectUri, false, state,null);
|
||||||
// 断言
|
// 断言
|
||||||
assertEquals(0, result.getCode());
|
assertEquals(0, result.getCode());
|
||||||
assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData());
|
assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData());
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package cd.casic.server;
|
package cd.casic.server;
|
||||||
|
|
||||||
import cd.casic.ci.process.properties.TargetFileUploadProperties;
|
import cd.casic.ci.commons.properties.TargetFileUploadProperties;
|
||||||
import cd.casic.ci.process.util.SftpUploadUtil;
|
|
||||||
import com.jcraft.jsch.*;
|
import com.jcraft.jsch.*;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jodd.io.IOUtil;
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user