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);
}