first commit

This commit is contained in:
zc
2026-02-27 10:16:46 +08:00
commit 0ee56404c2
705 changed files with 47675 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.ysoft</groupId>
<artifactId>ysoft-plugin</artifactId>
<version>3.6.0-SNAPSHOT</version>
</parent>
<artifactId>ysoft-plugin-open</artifactId>
<version>3.6.0-SNAPSHOT</version>
<description>能力开放插件包括应用管理、API开放授权、API开发等</description>
<licenses>
<license>
<name>GNU LESSER GENERAL PUBLIC LICENSE</name>
<url>http://www.gnu.org/licenses/lgpl.html</url>
</license>
</licenses>
</project>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.ysoft</groupId>
<artifactId>ysoft-plugin</artifactId>
<version>${revision}</version>
</parent>
<artifactId>ysoft-plugin-open</artifactId>
<description>能力开放插件包括应用管理、API开放授权、API开发等</description>
</project>

View File

@@ -0,0 +1,32 @@
package top.ysoft.admin.open.handler;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;
import org.springframework.stereotype.Component;
import top.ysoft.admin.open.util.OpenApiUtils;
import java.lang.reflect.Method;
import static cn.dev33.satoken.annotation.handler.SaCheckPermissionHandler._checkMethod;
/**
* 重定义注解 SaCheckPermission 的处理器
*
* @author chengzi
* @since 2024/10/25 12:03
*/
@Component
public class SaCheckPermissionHandler implements SaAnnotationHandlerInterface<SaCheckPermission> {
@Override
public Class<SaCheckPermission> getHandlerAnnotationClass() {
return SaCheckPermission.class;
}
@Override
public void checkMethod(SaCheckPermission at, Method method) {
if (!OpenApiUtils.isSignParamExists()) {
_checkMethod(at.type(), at.value(), at.mode(), at.orRole());
}
}
}

View File

@@ -0,0 +1,25 @@
package top.ysoft.admin.open.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import top.ysoft.admin.open.model.entity.AppDO;
import top.continew.starter.data.mp.base.BaseMapper;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
/**
* 应用 Mapper
*
* @author chengzi
* @since 2024/10/17 16:03
*/
public interface AppMapper extends BaseMapper<AppDO> {
/**
* 根据 Access Key 查询
*
* @param accessKey Access Key
* @return 应用信息
*/
@Select("select * from sys_app where access_key = #{accessKey}")
AppDO selectByAccessKey(@FieldEncrypt @Param("accessKey") String accessKey);
}

View File

@@ -0,0 +1,69 @@
package top.ysoft.admin.open.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
import top.ysoft.admin.common.model.entity.BaseDO;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 应用实体
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Data
@TableName("sys_app")
public class AppDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* Access Key访问密钥
*/
@FieldEncrypt
private String accessKey;
/**
* Secret Key私有密钥
*/
@FieldEncrypt
private String secretKey;
/**
* 失效时间
*/
private LocalDateTime expireTime;
/**
* 描述
*/
private String description;
/**
* 状态
*/
private DisEnableStatusEnum status;
/**
* 是否已过期
*
* @return true已过期false未过期
*/
public boolean isExpired() {
if (expireTime == null) {
return false;
}
return LocalDateTime.now().isAfter(expireTime);
}
}

View File

@@ -0,0 +1,31 @@
package top.ysoft.admin.open.model.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
/**
* 应用查询条件
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Data
@Schema(description = "应用查询条件")
public class AppQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 关键词
*/
@Schema(description = "关键词", example = "应用1")
@Query(columns = {"name", "description"}, type = QueryType.LIKE)
private String description;
}

View File

@@ -0,0 +1,67 @@
package top.ysoft.admin.open.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 创建或修改应用参数
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Data
@Schema(description = "创建或修改应用参数")
public class AppReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
@Schema(description = "名称", example = "应用1")
@NotBlank(message = "名称不能为空")
@Length(max = 100, message = "名称长度不能超过 {max} 个字符")
private String name;
/**
* 失效时间
*/
@Schema(description = "失效时间", example = "2023-08-08 23:59:59", type = "string")
@Future(message = "失效时间必须是未来时间")
private LocalDateTime expireTime;
/**
* 描述
*/
@Schema(description = "描述", example = "应用1描述信息")
@Length(max = 200, message = "描述长度不能超过 {max} 个字符")
private String description;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
private DisEnableStatusEnum status;
/**
* Access Key访问密钥
*/
@Schema(hidden = true)
private String accessKey;
/**
* Secret Key密钥
*/
@Schema(hidden = true)
private String secretKey;
}

View File

@@ -0,0 +1,63 @@
package top.ysoft.admin.open.model.resp;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.ysoft.admin.common.model.resp.BaseDetailResp;
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 应用详情信息
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "应用详情信息")
public class AppDetailResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
@Schema(description = "名称", example = "应用1")
@ExcelProperty(value = "名称", order = 2)
private String name;
/**
* Access Key访问密钥
*/
@Schema(description = "Access Key访问密钥", example = "YjUyMGJjYjIxNTE0NDAxMWE1NmRiY2")
@ExcelProperty(value = "Access Key", order = 3)
private String accessKey;
/**
* 失效时间
*/
@Schema(description = "失效时间", example = "2023-08-08 08:08:08", type = "string")
@ExcelProperty(value = "失效时间", order = 4)
private LocalDateTime expireTime;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
@ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 5)
private DisEnableStatusEnum status;
/**
* 描述
*/
@Schema(description = "描述", example = "应用1描述信息")
@ExcelProperty(value = "描述", order = 6)
private String description;
}

View File

@@ -0,0 +1,54 @@
package top.ysoft.admin.open.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.ysoft.admin.common.model.resp.BaseDetailResp;
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 应用信息
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Data
@Schema(description = "应用信息")
public class AppResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 名称
*/
@Schema(description = "名称", example = "应用1")
private String name;
/**
* Access Key访问密钥
*/
@Schema(description = "Access Key访问密钥", example = "YjUyMGJjYjIxNTE0NDAxMWE1NmRiY2")
private String accessKey;
/**
* 失效时间
*/
@Schema(description = "失效时间", example = "2023-08-08 08:08:08", type = "string")
private LocalDateTime expireTime;
/**
* 状态
*/
@Schema(description = "状态", example = "1")
private DisEnableStatusEnum status;
/**
* 描述
*/
@Schema(description = "描述", example = "应用1描述信息")
private String description;
}

View File

@@ -0,0 +1,34 @@
package top.ysoft.admin.open.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 应用密钥信息
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Data
@Schema(description = "应用密钥信息")
public class AppSecretResp implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* Access Key访问密钥
*/
@Schema(description = "Access Key访问密钥", example = "YjUyMGJjYjIxNTE0NDAxMWE1NmRiY2")
private String accessKey;
/**
* Secret Key私有密钥
*/
@Schema(description = "Secret Key私有密钥", example = "MDI2YzQ3YTU1NGEyNDM1ZWIwNTU5NmNjNmZjM2M2Nzg=")
private String secretKey;
}

View File

@@ -0,0 +1,42 @@
package top.ysoft.admin.open.service;
import top.ysoft.admin.open.model.entity.AppDO;
import top.ysoft.admin.open.model.query.AppQuery;
import top.ysoft.admin.open.model.req.AppReq;
import top.ysoft.admin.open.model.resp.AppDetailResp;
import top.ysoft.admin.open.model.resp.AppResp;
import top.ysoft.admin.open.model.resp.AppSecretResp;
import top.continew.starter.extension.crud.service.BaseService;
/**
* 应用业务接口
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
public interface AppService extends BaseService<AppResp, AppDetailResp, AppQuery, AppReq> {
/**
* 获取密钥
*
* @param id ID
* @return 密钥信息
*/
AppSecretResp getSecret(Long id);
/**
* 重置密钥
*
* @param id ID
*/
void resetSecret(Long id);
/**
* 根据 Access Key 查询
*
* @param accessKey Access Key
* @return 应用信息
*/
AppDO getByAccessKey(String accessKey);
}

View File

@@ -0,0 +1,71 @@
package top.ysoft.admin.open.service.impl;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.ysoft.admin.open.mapper.AppMapper;
import top.ysoft.admin.open.model.entity.AppDO;
import top.ysoft.admin.open.model.query.AppQuery;
import top.ysoft.admin.open.model.req.AppReq;
import top.ysoft.admin.open.model.resp.AppDetailResp;
import top.ysoft.admin.open.model.resp.AppResp;
import top.ysoft.admin.open.model.resp.AppSecretResp;
import top.ysoft.admin.open.service.AppService;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
/**
* 应用业务实现
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Service
@RequiredArgsConstructor
public class AppServiceImpl extends BaseServiceImpl<AppMapper, AppDO, AppResp, AppDetailResp, AppQuery, AppReq> implements AppService {
@Override
public void beforeAdd(AppReq req) {
req.setAccessKey(Base64.encode(IdUtil.fastSimpleUUID())
.replace(StringConstants.SLASH, StringConstants.EMPTY)
.replace(StringConstants.PLUS, StringConstants.EMPTY)
.substring(0, 30));
req.setSecretKey(this.generateSecret());
}
@Override
public AppSecretResp getSecret(Long id) {
AppDO app = super.getById(id);
AppSecretResp appSecretResp = new AppSecretResp();
appSecretResp.setAccessKey(app.getAccessKey());
appSecretResp.setSecretKey(app.getSecretKey());
return appSecretResp;
}
@Override
public void resetSecret(Long id) {
super.getById(id);
AppDO app = new AppDO();
app.setSecretKey(this.generateSecret());
baseMapper.update(app, Wrappers.lambdaQuery(AppDO.class).eq(AppDO::getId, id));
}
@Override
public AppDO getByAccessKey(String accessKey) {
return baseMapper.selectByAccessKey(accessKey);
}
/**
* 生成密钥
*
* @return 密钥
*/
private String generateSecret() {
return Base64.encode(IdUtil.fastSimpleUUID())
.replace(StringConstants.SLASH, StringConstants.EMPTY)
.replace(StringConstants.PLUS, StringConstants.EMPTY);
}
}

View File

@@ -0,0 +1,60 @@
package top.ysoft.admin.open.sign;
import cn.dev33.satoken.sign.SaSignTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
import top.ysoft.admin.open.model.entity.AppDO;
import top.ysoft.admin.open.service.AppService;
import top.continew.starter.core.validation.ValidationUtils;
import java.util.Map;
/**
* API 参数签名算法
*
* @author chengzi
* @author Charles7c
* @since 2024/10/17 16:03
*/
@Component
@RequiredArgsConstructor
public class OpenApiSignTemplate extends SaSignTemplate {
private final AppService appService;
public static final String ACCESS_KEY = "accessKey";
@Override
public void checkParamMap(Map<String, String> paramMap) {
// 获取必须的参数
String timestampValue = paramMap.get(timestamp);
String nonceValue = paramMap.get(nonce);
String signValue = paramMap.get(sign);
String accessKeyValue = paramMap.get(ACCESS_KEY);
// 校验
ValidationUtils.throwIfBlank(timestampValue, "timestamp不能为空");
ValidationUtils.throwIfBlank(nonceValue, "nonce不能为空");
ValidationUtils.throwIfBlank(signValue, "sign不能为空");
ValidationUtils.throwIfBlank(accessKeyValue, "accessKey不能为空");
AppDO app = appService.getByAccessKey(accessKeyValue);
ValidationUtils.throwIfNull(app, "accessKey非法");
ValidationUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, app.getStatus(), "应用已被禁用, 请联系管理员");
ValidationUtils.throwIf(app.isExpired(), "应用已过期, 请联系管理员");
// 依次校验三个参数
super.checkTimestamp(Long.parseLong(timestampValue));
super.checkNonce(nonceValue);
paramMap.put(key, app.getSecretKey());
super.checkSign(paramMap, signValue);
}
@Override
public String createSign(Map<String, ?> paramMap) {
ValidationUtils.throwIfEmpty(paramMap.get(key), "秘钥缺失, 请检查应用配置");
// 移除 sign 参数
paramMap.remove(sign);
// 计算签名
return super.abstractStr(super.joinParamsDictSort(paramMap));
}
}

View File

@@ -0,0 +1,31 @@
package top.ysoft.admin.open.util;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.sign.SaSignTemplate;
import java.util.List;
/**
* Open Api 工具类
*
* @author chengzi
* @author Charles7c
* @since 2024/10/25 15:31
*/
public class OpenApiUtils {
private OpenApiUtils() {
}
/**
* 判断请求是否包含sign参数
*
* @return 是否包含sign参数true包含false不包含
*/
public static boolean isSignParamExists() {
SaRequest saRequest = SaHolder.getRequest();
List<String> paramNames = saRequest.getParamNames();
return paramNames.stream().anyMatch(SaSignTemplate.sign::equals);
}
}