From 6d6c2c8f58c25be4c7555e3e9017f76f2a8e73c9 Mon Sep 17 00:00:00 2001 From: HopeLi <1278288511@qq.com> Date: Fri, 25 Jul 2025 16:20:30 +0800 Subject: [PATCH 1/6] =?UTF-8?q?0725=20ljc=20=20=E4=BF=AE=E6=94=B9TargetFil?= =?UTF-8?q?eUploadProperties=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ci/commons}/properties/TargetFileUploadProperties.java | 2 +- .../process/service/sftpFile/impl/SftpFileServiceImpl.java | 2 +- .../process/service/target/impl/TargetManagerServiceImpl.java | 2 +- .../service/testCase/impl/TestCaseManagerServiceImpl.java | 2 +- ops-server/src/test/java/cd/casic/server/ZipFileTest.java | 4 +--- 5 files changed, 5 insertions(+), 7 deletions(-) rename modules/{module-ci-process-biz/src/main/java/cd/casic/ci/process => module-ci-commons/src/main/java/cd/casic/ci/commons}/properties/TargetFileUploadProperties.java (95%) diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/properties/TargetFileUploadProperties.java b/modules/module-ci-commons/src/main/java/cd/casic/ci/commons/properties/TargetFileUploadProperties.java similarity index 95% rename from modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/properties/TargetFileUploadProperties.java rename to modules/module-ci-commons/src/main/java/cd/casic/ci/commons/properties/TargetFileUploadProperties.java index 9683d967..3f17edb6 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/properties/TargetFileUploadProperties.java +++ b/modules/module-ci-commons/src/main/java/cd/casic/ci/commons/properties/TargetFileUploadProperties.java @@ -1,4 +1,4 @@ -package cd.casic.ci.process.properties; +package cd.casic.ci.commons.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java index a513c8ca..c5e5524f 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java @@ -3,7 +3,7 @@ package cd.casic.ci.process.process.service.sftpFile.impl; import cd.casic.ci.commons.sftp.SftpClientUtils; 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 jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java index 03652702..1b167d1e 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java @@ -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.sftpFile.impl.SftpFileServiceImpl; 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.framework.commons.exception.ServiceException; import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants; diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/testCase/impl/TestCaseManagerServiceImpl.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/testCase/impl/TestCaseManagerServiceImpl.java index 124bd748..7ce6fb11 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/testCase/impl/TestCaseManagerServiceImpl.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/testCase/impl/TestCaseManagerServiceImpl.java @@ -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.service.testCase.TestCaseInfoService; 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.enums.GlobalErrorCodeConstants; import cd.casic.framework.commons.pojo.PageResult; diff --git a/ops-server/src/test/java/cd/casic/server/ZipFileTest.java b/ops-server/src/test/java/cd/casic/server/ZipFileTest.java index ab2ea76e..d385df77 100644 --- a/ops-server/src/test/java/cd/casic/server/ZipFileTest.java +++ b/ops-server/src/test/java/cd/casic/server/ZipFileTest.java @@ -1,10 +1,8 @@ package cd.casic.server; -import cd.casic.ci.process.properties.TargetFileUploadProperties; -import cd.casic.ci.process.util.SftpUploadUtil; +import cd.casic.ci.commons.properties.TargetFileUploadProperties; import com.jcraft.jsch.*; import jakarta.annotation.Resource; -import jodd.io.IOUtil; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; From cb22cd43c60bccade3f16e38e67512a391fb7c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=B2=E5=85=88=E7=94=9F?= <821039958@qq.com> Date: Fri, 25 Jul 2025 18:22:47 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=88=B0=E7=9B=AE=E6=A0=87=E4=B8=BB=E6=9C=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/module-ci-execute/pom.xml | 8 ++ .../docker/api/DockerImageController.java | 12 +++ .../convert/DockerImageConvert.java | 20 ----- .../docker/dataobject/model/DockerImage.java | 5 +- .../execute/docker/service/IImageService.java | 2 + .../docker/service/impl/ImageService.java | 89 +++++++++++++++++-- 6 files changed, 106 insertions(+), 30 deletions(-) delete mode 100644 modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerImageConvert.java diff --git a/modules/module-ci-execute/pom.xml b/modules/module-ci-execute/pom.xml index 3db1d4ef..0ea55e7a 100644 --- a/modules/module-ci-execute/pom.xml +++ b/modules/module-ci-execute/pom.xml @@ -27,6 +27,10 @@ cd.casic.boot spring-boot-starter-test + + cd.casic.boot + module-ci-machine + com.github.docker-java docker-java @@ -61,6 +65,10 @@ jakarta.ws.rs-api 3.1.0 + + com.jcraft + jsch + diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java index 6c2e43e3..f2b30a02 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/api/DockerImageController.java @@ -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.service.IImageService; 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 jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -47,4 +49,14 @@ public class DockerImageController { 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)); + } } diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerImageConvert.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerImageConvert.java deleted file mode 100644 index ee288638..00000000 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/convert/DockerImageConvert.java +++ /dev/null @@ -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); - -} diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerImage.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerImage.java index 02363a2c..2c5383c4 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerImage.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/dataobject/model/DockerImage.java @@ -1,8 +1,6 @@ package cd.casic.module.execute.docker.dataobject.model; -import cd.casic.framework.commons.dataobject.BaseDO; import lombok.Data; -import lombok.experimental.Accessors; /** * 本地镜像 @@ -12,8 +10,7 @@ import lombok.experimental.Accessors; * @since 2025/7/23 16:22 */ @Data -@Accessors(chain = true) -public class DockerImage extends BaseDO { +public class DockerImage { private String id; diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java index 3486116a..96eb3a01 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java @@ -109,4 +109,6 @@ public interface IImageService { int localImageUpload(DockerImage dockerImage); PageResult getLocalImagePage(DockerImagePageReqVO pageVO); + + Object localImagePush(String imageId, String machineId); } diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java index c34d787d..251cbab5 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java @@ -1,22 +1,33 @@ package cd.casic.module.execute.docker.service.impl; +import cd.casic.ci.commons.properties.TargetFileUploadProperties; import cd.casic.framework.commons.pojo.PageResult; import cd.casic.framework.mybatis.core.query.LambdaQueryWrapperX; import cd.casic.module.execute.docker.DockerClientFactory; 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.model.DockerImage; import cd.casic.module.execute.docker.dataobject.vo.DockerImagePageReqVO; import cd.casic.module.execute.docker.service.IImageService; +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.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.command.InspectImageResponse; import com.github.dockerjava.api.command.SaveImageCmd; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Image; import com.github.dockerjava.api.model.PruneType; +import com.jcraft.jsch.Session; import jakarta.annotation.Nonnull; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; @@ -24,11 +35,11 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Objects; +import java.util.UUID; /** * @description: 镜像的服务类 @@ -46,6 +57,15 @@ public class ImageService implements IImageService { @Resource private DockerImageDao dockerImageDao; + @Resource + private MachineInfoMapper machineInfoMapper; + + @Resource + private TargetFileUploadProperties fileUploadProperties; + + @Resource + private SecretKeyMapper secretKeyMapper; + @Override public List list(@Nonnull String clientId) { DockerClient dockerClient = dockerClientFactory.getdockerClient(clientId); @@ -144,7 +164,9 @@ public class ImageService implements IImageService { @Transactional(rollbackFor = Exception.class) @Override 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 @@ -153,4 +175,59 @@ public class ImageService implements IImageService { PageResult page = dockerImageDao.selectPage(pageVO, queryWrapperX.likeIfPresent(DockerImageDo::getName, pageVO.getName()).orderByDesc(DockerImageDo::getCreateTime)); return page; } + + /** + * 先下载后上传到目标主机 + * @param imageId + * @param machineId + * @return + */ + @Override + public Object localImagePush(String imageId, String machineId) { + if (Objects.isNull(imageId) || Objects.isNull(machineId)) { + return null; + } + DockerImageDo imageDo = dockerImageDao.selectById(imageId); + //1 远程下载 + // 1.1 建立连接 下载 + 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(); + // 2 建立连接 区分密码还是秘钥 + 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())); + } + Sftp uploadSftp = getSftp(targetFtp); + // 2.1 上传 + 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 null; + } + + /** + * + * 获取Sftp连接 + * @param config 连接配置 + * @return + */ + private Sftp getSftp(FtpConfig config){ + Session session; + if (Objects.nonNull(config.getSystemKey())) { + session = JschUtil.getSession(config.getHost(), config.getPort(), config.getUser(), config.getSystemKey().getBytes(StandardCharsets.UTF_8), config.getPassword().getBytes()); + } else { + session = JschUtil.getSession(config.getHost(), config.getPort(), config.getUser(), config.getPassword()); + } + return new Sftp(session); + } } From 2afeb554da870389a3953e821cd827d3501f747b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=B2=E5=85=88=E7=94=9F?= <821039958@qq.com> Date: Mon, 28 Jul 2025 11:50:50 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=88=B0=E7=9B=AE=E6=A0=87=E4=B8=BB=E6=9C=BA=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20=E5=BC=82=E5=B8=B8=E6=8D=95=E8=8E=B7=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execute/docker/service/IImageService.java | 2 +- .../docker/service/impl/ImageService.java | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java index 96eb3a01..33f87861 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/IImageService.java @@ -110,5 +110,5 @@ public interface IImageService { PageResult getLocalImagePage(DockerImagePageReqVO pageVO); - Object localImagePush(String imageId, String machineId); + Boolean localImagePush(String imageId, String machineId); } diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java index 251cbab5..7bebb873 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java @@ -1,6 +1,7 @@ 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.mybatis.core.query.LambdaQueryWrapperX; import cd.casic.module.execute.docker.DockerClientFactory; @@ -9,6 +10,7 @@ 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.vo.DockerImagePageReqVO; 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; @@ -183,9 +185,9 @@ public class ImageService implements IImageService { * @return */ @Override - public Object localImagePush(String imageId, String machineId) { + public Boolean localImagePush(String imageId, String machineId) { if (Objects.isNull(imageId) || Objects.isNull(machineId)) { - return null; + throw new ServerException(MachineErrorCodeConstants.MACHINE_INFO_TAG_NULL); } DockerImageDo imageDo = dockerImageDao.selectById(imageId); //1 远程下载 @@ -203,8 +205,8 @@ public class ImageService implements IImageService { targetFtp.setSystemKey(CryptogramUtil.doDecrypt(secretKeyDO.getPrivateKey())); targetFtp.setPassword(CryptogramUtil.doDecrypt(secretKeyDO.getPassword())); } - Sftp uploadSftp = getSftp(targetFtp); // 2.1 上传 + Sftp uploadSftp = getSftp(targetFtp); String suffix = FileUtil.getSuffix(srcPath); String fileName = imageDo.getName() + "." + suffix; ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(byteOut.toByteArray()); //转换 @@ -212,7 +214,7 @@ public class ImageService implements IImageService { uploadSftp.mkDirs(romPath);//递归创建 uploadSftp.upload(romPath, fileName, arrayInputStream); uploadSftp.close(); - return null; + return true; } /** @@ -221,13 +223,17 @@ public class ImageService implements IImageService { * @param config 连接配置 * @return */ - private Sftp getSftp(FtpConfig config){ + private Sftp getSftp(FtpConfig config) { Session session; if (Objects.nonNull(config.getSystemKey())) { session = JschUtil.getSession(config.getHost(), config.getPort(), config.getUser(), config.getSystemKey().getBytes(StandardCharsets.UTF_8), config.getPassword().getBytes()); } else { session = JschUtil.getSession(config.getHost(), config.getPort(), config.getUser(), config.getPassword()); } + if (!session.isConnected()) { + log.error("与主机IP:{} 建立SSH连接失败", config.getHost()); + throw new ServerException(500, "部署失败,请检测主机是否可用"); + } return new Sftp(session); } } From 28ce63a0e104e149e94197a8284e9303f2d4605e Mon Sep 17 00:00:00 2001 From: HopeLi <1278288511@qq.com> Date: Mon, 28 Jul 2025 16:02:09 +0800 Subject: [PATCH 4/6] =?UTF-8?q?0728=20ljc=20=20=E5=8D=87=E7=BA=A7=E4=B8=BA?= =?UTF-8?q?OIDC=E7=89=88=E6=9C=AC=EF=BC=8C=E8=8B=A5=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=8F=AF=E9=80=89=E6=8B=A9=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8Cversion=201.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/service/OAuth2GrantService.java | 11 ++ .../core/service/OAuth2GrantServiceImpl.java | 77 +++++++- .../security/dal/oauth2/OAuth2ClientDO.java | 10 ++ .../vo/vo/client/OAuth2ClientSaveReqVO.java | 6 + .../admin/oauth2/OAuth2OpenController.java | 165 +++++++++++++++--- .../admin/oauth2/OIDCUserInfoController.java | 56 ++++++ .../dataobject/oidc/OIDCUserInfoRespVO.java | 33 ++++ .../system/util/oauth2/OAuth2Utils.java | 76 ++++++++ .../oauth2/OAuth2OpenControllerTest.java | 12 +- 9 files changed, 413 insertions(+), 33 deletions(-) create mode 100644 modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OIDCUserInfoController.java create mode 100644 modules/module-system-biz/src/main/java/cd/casic/module/system/dal/dataobject/oidc/OIDCUserInfoRespVO.java diff --git a/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantService.java b/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantService.java index f4584f04..9eb058ab 100644 --- a/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantService.java +++ b/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantService.java @@ -112,4 +112,15 @@ public interface OAuth2GrantService { */ 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 scopes, String nonce); } diff --git a/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java b/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java index 5765c38d..22c86d8c 100644 --- a/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java +++ b/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java @@ -1,20 +1,29 @@ 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.security.dal.oauth2.OAuth2AccessTokenDO; import cd.casic.framework.security.dal.oauth2.OAuth2CodeDO; import cd.casic.framework.security.dal.user.AdminUserDO; import cd.casic.framework.security.oauth2.OAuth2CodeService; +import cd.casic.module.system.enums.ErrorCodeConstants; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cd.casic.framework.commons.enums.UserTypeEnum; -import cd.casic.module.system.enums.ErrorCodeConstants; +import com.nimbusds.jose.JOSEObjectType; +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 jakarta.annotation.Resource; +import java.util.Date; import java.util.List; 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 */ +@Slf4j @Service public class OAuth2GrantServiceImpl implements OAuth2GrantService { @@ -33,6 +43,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { private OAuth2CodeService oauth2CodeService; @Resource private AdminAuthService adminAuthService; + @Resource + private AdminUserService adminUserService; @Override public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, @@ -104,4 +116,61 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { return oauth2TokenService.removeAccessToken(accessToken) != null; } + @Override + public String grantIDToken(Long userId, Integer userType, String clientId, List 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:8080") // 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 ""; + } + } + } diff --git a/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/dal/oauth2/OAuth2ClientDO.java b/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/dal/oauth2/OAuth2ClientDO.java index 70cd4b19..0b00f8dc 100644 --- a/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/dal/oauth2/OAuth2ClientDO.java +++ b/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/dal/oauth2/OAuth2ClientDO.java @@ -106,4 +106,14 @@ public class OAuth2ClientDO extends BaseDO { */ private String additionalInformation; + /** + * 是否支持 OIDC + */ + private Boolean oidcSupport = false; + + /** + * 默认响应类型 + */ + private String defaultResponseType = "code"; + } diff --git a/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/vo/vo/client/OAuth2ClientSaveReqVO.java b/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/vo/vo/client/OAuth2ClientSaveReqVO.java index 8f5a9ba5..e13f9e71 100644 --- a/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/vo/vo/client/OAuth2ClientSaveReqVO.java +++ b/framework/spring-boot-starter-security/src/main/java/cd/casic/framework/security/vo/vo/client/OAuth2ClientSaveReqVO.java @@ -75,6 +75,12 @@ public class OAuth2ClientSaveReqVO { @Schema(description = "附加信息", example = "{yunai: true}") 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 格式") public boolean isAdditionalInformationJson() { return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation); diff --git a/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OAuth2OpenController.java b/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OAuth2OpenController.java index 86239b81..5eab326d 100644 --- a/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OAuth2OpenController.java +++ b/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OAuth2OpenController.java @@ -1,38 +1,41 @@ 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.pojo.CommonResult; import cd.casic.framework.commons.util.http.HttpUtils; 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.OAuth2OpenAuthorizeInfoRespVO; 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.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 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.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 org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; -import jakarta.servlet.http.HttpServletRequest; +import java.time.ZoneId; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Map; @@ -69,6 +72,8 @@ public class OAuth2OpenController { private OAuth2ApproveService oauth2ApproveService; @Resource private OAuth2TokenService oauth2TokenService; + @Resource + private AdminUserService adminUserService; /** * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法 @@ -181,13 +186,36 @@ public class OAuth2OpenController { @GetMapping("/authorize") @Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") @Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou") - public CommonResult authorize(@RequestParam("clientId") String clientId) { + public CommonResult 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 approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); +// // 拼接返回 +// return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); + + + + // 0. 校验用户已经登录。通过 Spring Security 实现 // 1. 获得 Client 客户端的信息 OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId); - // 2. 获得用户已经授权的信息 + + // 2. 检查是否为 OIDC 请求 + boolean isOIDCRequest = isOIDCRequest(responseType, client); + + // 3. 获得用户已经授权的信息 List approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); + + // 4. 如果是 OIDC 请求,确保 openid scope 被包含 + if (isOIDCRequest) { + // 可以在这里添加特殊的处理逻辑 + } + // 拼接返回 return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); } @@ -217,9 +245,11 @@ public class OAuth2OpenController { @RequestParam(value = "scope", required = false) String scope, @RequestParam("redirect_uri") String redirectUri, @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") - Map scopes = JsonUtils.parseObject(scope, Map.class); + + Map scopes = JsonUtils.parseObject(scope, Map.class); scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); // 0. 校验用户已经登录。通过 Spring Security 实现 @@ -229,6 +259,9 @@ public class OAuth2OpenController { OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); + // 检查是否为 OIDC 请求 + boolean isOIDCRequest = checkOIDCRequest(responseType, scopes); + // 2.1 假设 approved 为 null,说明是场景一 if (Boolean.TRUE.equals(autoApprove)) { // 如果无法自动授权通过,则返回空 url,前端不进行跳转 @@ -243,12 +276,17 @@ public class OAuth2OpenController { } } - // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 + // 3. 根据不同的响应类型处理 List 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)); + } 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)); } @@ -294,4 +332,85 @@ public class OAuth2OpenController { 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 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 handleOIDCResponse(OAuth2GrantTypeEnum grantTypeEnum, + Long userId, + OAuth2ClientDO client, + List 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 scopes, String nonce) { + return oauth2GrantService.grantIDToken(userId, getUserType(), clientId, scopes, nonce); + } } diff --git a/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OIDCUserInfoController.java b/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OIDCUserInfoController.java new file mode 100644 index 00000000..cf904f5a --- /dev/null +++ b/modules/module-system-biz/src/main/java/cd/casic/module/system/controller/admin/oauth2/OIDCUserInfoController.java @@ -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 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); + } +} diff --git a/modules/module-system-biz/src/main/java/cd/casic/module/system/dal/dataobject/oidc/OIDCUserInfoRespVO.java b/modules/module-system-biz/src/main/java/cd/casic/module/system/dal/dataobject/oidc/OIDCUserInfoRespVO.java new file mode 100644 index 00000000..173b398d --- /dev/null +++ b/modules/module-system-biz/src/main/java/cd/casic/module/system/dal/dataobject/oidc/OIDCUserInfoRespVO.java @@ -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; + + // 可根据需要添加更多标准字段 +} diff --git a/modules/module-system-biz/src/main/java/cd/casic/module/system/util/oauth2/OAuth2Utils.java b/modules/module-system-biz/src/main/java/cd/casic/module/system/util/oauth2/OAuth2Utils.java index d6dea293..c880aca9 100644 --- a/modules/module-system-biz/src/main/java/cd/casic/module/system/util/oauth2/OAuth2Utils.java +++ b/modules/module-system-biz/src/main/java/cd/casic/module/system/util/oauth2/OAuth2Utils.java @@ -100,4 +100,80 @@ public class OAuth2Utils { 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(); + } + } diff --git a/modules/module-system-biz/src/test/java/cd/casic/module/system/oauth2/OAuth2OpenControllerTest.java b/modules/module-system-biz/src/test/java/cd/casic/module/system/oauth2/OAuth2OpenControllerTest.java index 778cb0d3..be2091b1 100644 --- a/modules/module-system-biz/src/test/java/cd/casic/module/system/oauth2/OAuth2OpenControllerTest.java +++ b/modules/module-system-biz/src/test/java/cd/casic/module/system/oauth2/OAuth2OpenControllerTest.java @@ -205,7 +205,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves); // 调用 - CommonResult result = oauth2OpenController.authorize(clientId); + CommonResult result = oauth2OpenController.authorize(clientId,null); // 断言 assertEquals(0, result.getCode()); assertPojoEquals(client, result.getData().getClient()); @@ -218,7 +218,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { public void testApproveOrDeny_grantTypeError() { // 调用,并断言 assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null, - null, null, null, null), + null, null, null, null,null), new ErrorCode(400, "response_type 参数值只允许 code 和 token")); } @@ -237,7 +237,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, - scope, redirectUri, true, state); + scope, redirectUri, true, state,null); // 断言 assertEquals(0, result.getCode()); assertNull(result.getData()); @@ -258,7 +258,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, - scope, redirectUri, false, state); + scope, redirectUri, false, state,null); // 断言 assertEquals(0, result.getCode()); 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 result = oauth2OpenController.approveOrDeny(responseType, clientId, - scope, redirectUri, true, state); + scope, redirectUri, true, state,null); // 断言 assertEquals(0, result.getCode()); assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算 @@ -319,7 +319,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { // 调用 CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, - scope, redirectUri, false, state); + scope, redirectUri, false, state,null); // 断言 assertEquals(0, result.getCode()); assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData()); From 5f56977ded66d33efca10e352b5e06b8f8a018e2 Mon Sep 17 00:00:00 2001 From: HopeLi <1278288511@qq.com> Date: Tue, 29 Jul 2025 09:42:56 +0800 Subject: [PATCH 5/6] =?UTF-8?q?0729=20ljc=20=20=E5=8D=87=E7=BA=A7=E4=B8=BA?= =?UTF-8?q?OIDC=E7=89=88=E6=9C=AC=EF=BC=8C=E8=8B=A5=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=8F=AF=E9=80=89=E6=8B=A9=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8Cversion=201.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/tenant/core/service/OAuth2GrantServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java b/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java index 22c86d8c..8e037042 100644 --- a/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java +++ b/framework/spring-boot-starter-biz-tenant/src/main/java/cd/casic/framework/tenant/core/service/OAuth2GrantServiceImpl.java @@ -133,7 +133,7 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { // JWT 载荷 JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder() - .issuer("http://localhost:8080") // iss: 发行者 + .issuer("http://localhost:48080") // iss: 发行者 .subject(String.valueOf(userId)) // sub: 主题 .audience(clientId) // aud: 受众 .expirationTime(new Date(System.currentTimeMillis() + 3600000)) // exp: 过期时间 (1小时) From 1381d632f05fb19f58933d111bd17d58ef5faba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=B2=E5=85=88=E7=94=9F?= <821039958@qq.com> Date: Tue, 29 Jul 2025 10:45:38 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=88=B0=E7=9B=AE=E6=A0=87=E4=B8=BB=E6=9C=BA=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=20=E8=AE=BE=E7=BD=AE=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=97=B6=E9=97=B4=EF=BC=8C=E4=B8=8D=E5=8F=AF=E7=94=A8=E6=97=B6?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docker/service/impl/ImageService.java | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java index 7bebb873..88f13a2b 100644 --- a/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java +++ b/modules/module-ci-execute/src/main/java/cd/casic/module/execute/docker/service/impl/ImageService.java @@ -179,7 +179,7 @@ public class ImageService implements IImageService { } /** - * 先下载后上传到目标主机 + * 先下载后上传到目标主机 先确定目标主机是否可连接在继续 * @param imageId * @param machineId * @return @@ -189,15 +189,7 @@ public class ImageService implements IImageService { if (Objects.isNull(imageId) || Objects.isNull(machineId)) { throw new ServerException(MachineErrorCodeConstants.MACHINE_INFO_TAG_NULL); } - DockerImageDo imageDo = dockerImageDao.selectById(imageId); - //1 远程下载 - // 1.1 建立连接 下载 - 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(); - // 2 建立连接 区分密码还是秘钥 + // 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)) { @@ -205,8 +197,17 @@ public class ImageService implements IImageService { targetFtp.setSystemKey(CryptogramUtil.doDecrypt(secretKeyDO.getPrivateKey())); targetFtp.setPassword(CryptogramUtil.doDecrypt(secretKeyDO.getPassword())); } - // 2.1 上传 + // 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()); //转换 @@ -225,14 +226,16 @@ public class ImageService implements IImageService { */ private Sftp getSftp(FtpConfig config) { Session session; - if (Objects.nonNull(config.getSystemKey())) { - session = JschUtil.getSession(config.getHost(), config.getPort(), config.getUser(), config.getSystemKey().getBytes(StandardCharsets.UTF_8), config.getPassword().getBytes()); - } else { - session = JschUtil.getSession(config.getHost(), config.getPort(), config.getUser(), config.getPassword()); - } - if (!session.isConnected()) { + 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 ServerException(500, "部署失败,请检测主机是否可用"); + throw new RuntimeException("部署失败,请检测主机是否可用", e); } return new Sftp(session); }