Merge branch 'feature/REQ-2040' into 'master'

Feature/req 2040

See merge request universal/infrastructure/backend/nanopart!5
This commit is contained in:
金海洋 2024-01-10 06:32:16 +00:00
commit fd5932d02e
26 changed files with 969 additions and 42 deletions

View File

@ -1,5 +1,6 @@
# 项目介绍
一个通用的聚合服务:通用黑名单服务
一个通用的聚合服务:
* 通用黑白名单服务
# vm参数
## dev环境
@ -18,13 +19,19 @@
-DCUSTOM_ENV=test
-Dserver.port=8080
# 各模块结构
# 黑白名单中各模块结构
## all-login 登录白名单
~~~json
{
"phone": "手机号"
}
~~~
## ou_register_black_list 企业注册黑名单
~~~json
{
"phone": "手机号"
}
~~~
## attendance-pass 考勤黑名单
~~~json
{

View File

@ -22,5 +22,9 @@
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-common-domain</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,8 +1,11 @@
package cn.axzo.nanopart.api;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.api.annotation.CheckSign;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import cn.axzo.nanopart.api.request.BlackAndWhiteListExcelImportReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListInternalSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListPlatformSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListReq;
import cn.axzo.nanopart.api.response.BlackAndWhiteListExcelImportResp;
import cn.axzo.nanopart.api.response.BlackAndWhiteListResp;
@ -13,6 +16,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotBlank;
@ -84,4 +88,35 @@ public interface BlackAndWhiteListApi {
@PostMapping("api/black-white-list/import")
ApiResult<BlackAndWhiteListExcelImportResp> importBlackAndWhiteListExcel(@RequestParam(value = "type") @NotNull(message = "名单类型不能为空") ListTypeEnum type,
@RequestParam(value = "module") @NotBlank(message = "模块不能为空") String module, @RequestParam(value = "url", required = false) String url, @RequestPart(value = "file") MultipartFile file);
/**
* 平台同步黑白名单
* <p>
* 每次最多同步1000条记录且整体上同步策略采用全量覆盖策略
* 若总记录数超过1000条请自行分片多次调用
* </p>
*
* <p>
* 该接口需要验签参与签名的字段为header中的appKey,timestamp,
* 签名字段为header中的sign
* </p>
*
* @param req @{link BlackAndWhiteListPlatformSyncReq}
* @return true-同步成功false-同步失败
*/
@PostMapping("api/black-white-list/plat-sync")
ApiResult<Boolean> platformSync(@RequestBody @Validated BlackAndWhiteListPlatformSyncReq req);
/**
* TODO 该接口目前耗时较长待优化
* 内部自同步黑白名单
* <p>
* 例如将其他模块的黑名单同步到登录模块黑名单
* </p>
*
* @param req @{link BlackAndWhiteListInternalSyncReq}
* @return 同步结果成功返回同步记录总数
*/
@PostMapping("api/black-white-list/inner-sync")
ApiResult<Long> internalSync(@RequestBody @Validated BlackAndWhiteListInternalSyncReq req);
}

View File

@ -0,0 +1,42 @@
package cn.axzo.nanopart.api;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.api.request.SignatureGetReq;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.security.KeyPair;
import java.util.Map;
/**
* 签名工具类
*
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 16:53
*/
@FeignClient(name = "nanopart", url = "http://nanopart:8080")
public interface SignatureUtilApi {
/**
* 生成公私钥对
*
* @param algorithm 算法例如RSA
* @return 公私钥对
*/
@GetMapping("api/signature/generateKeyPair")
ApiResult<Map<String,String>> generateKeyPair(@RequestParam(value = "algorithm") String algorithm);
/**
* 获取签名
*
* @param req 请求
* @return 签名
*/
@PostMapping("api/signature/getSignature")
ApiResult<String> getSignature(@RequestBody @Validated SignatureGetReq req);
}

View File

@ -0,0 +1,29 @@
package cn.axzo.nanopart.api.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 签名验证注解
*
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 10:37
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckSign {
/**
* 签名字段默认为sign
*/
String signField() default "sign";
/**
* header中需要参与签名的字段若为空则除签名字段外全部参与签名
*/
String[] requireSignFields() default {};
}

View File

@ -0,0 +1,164 @@
package cn.axzo.nanopart.api.aspect;
import cn.axzo.framework.domain.web.BizException;
import cn.axzo.framework.domain.web.code.RespCode;
import cn.axzo.nanopart.api.annotation.CheckSign;
import cn.axzo.nanopart.api.common.util.StringUtil;
import cn.axzo.nanopart.api.config.SignManager;
import cn.hutool.core.util.ArrayUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 10:05
*/
@Slf4j
@Aspect
@Order(1)
@Component
public class CheckSignAspect {
@Resource
private SignManager signManager;
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestMapping() {
}
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {
log.info("post");
}
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void getMapping() {
}
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void putMapping() {
}
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void deleteMapping() {
}
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.PatchMapping)")
public void patchMapping() {
}
@Pointcut("requestMapping() || postMapping() || getMapping() || putMapping() || deleteMapping()|| patchMapping()")
public void mappingAnnotations() {
}
// @Pointcut(value = "@annotation(cn.axzo.nanopart.api.annotation.CheckSign)")
// public void checkSign() {
// }
/**
* 切入含有@CheckSign && @RestController 注解的类
*/
@Before(value = "@within(checkSign) && @within(restController)", argNames = "joinPoint,checkSign,restController")
public void classHandler(JoinPoint joinPoint, CheckSign checkSign, RestController restController) {
handle(joinPoint, checkSign);
}
/**
* 切入含有@checkSign && @RequestMapping/@PostMapping/@GetMapping/@PutMapping/@DeleteMapping/@PatchMapping 之一注解的方法
*/
@Before(value = "@annotation(checkSign)")
public void methodHandler(JoinPoint joinPoint, CheckSign checkSign) {
handle(joinPoint, checkSign);
}
@SneakyThrows
public void handle(JoinPoint joinPoint, CheckSign checkSign) {
// 验签
verifySignature(checkSign);
}
/**
* 验签
*
* @param checkSign 注解,包含签名字段名等配置
*/
private void verifySignature(CheckSign checkSign) {
HttpServletRequest httpRequest = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
// 获取调用方appKey检查是否是受信任调用方
String appKey = httpRequest.getHeader("appKey");
if (StringUtils.isEmpty(appKey)) {
throw new BizException(new RespCode("403", "验签失败"));
}
// 从请求头中提取签名
String sign = httpRequest.getHeader(checkSign.signField());
if (StringUtils.isEmpty(sign)) {
throw new BizException(new RespCode("403", "验签失败"));
}
// 从Header中获取需要验签的参数字段若字段值为空则该字段不计入签名计算
Map<String, String> attendFields = new HashMap<>();
// 无指定字段则所有字段参与计算签名
if (ArrayUtil.isEmpty(checkSign.requireSignFields())) {
Enumeration<String> headerNames = httpRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String value = httpRequest.getHeader(headerName);
attendFields.put(headerName, value);
}
} else {
// 有指定参与计算签名的字段则只提取指定字段参与计算签名
for (String field : checkSign.requireSignFields()) {
String value = httpRequest.getHeader(field);
attendFields.put(field, value);
}
}
// 对所有参与签名计算的字段去除空值和去重后按照字段名的ASCII码从小到大排序字典序
Map<String, Object> sortedAttendFields = attendFields.entrySet()
.stream()
.filter(entry -> !StringUtils.isEmpty(entry.getValue()))
.distinct()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap<String, Object>::new));
// 拼接所有签名数据为字符串
String data = StringUtil.extracted(sortedAttendFields);
log.info("参与签名计算data:{}", data);
try {
boolean verify = signManager.verify(appKey, data, sign);
if (!verify) {
throw new BizException(new RespCode("403", "验签失败"));
}
} catch (Exception e) {
throw new BizException(new RespCode("403", "验签失败"));
}
}
}

View File

@ -0,0 +1,35 @@
package cn.axzo.nanopart.api.common.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.Map;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/1/4 10:53
*/
public class StringUtil {
/**
* 将Map中的参数拼接成字符串
*
* @param params 键值对
* @return 拼接后的字符串
*/
public static String extracted(Map<String, Object> params) {
if (CollectionUtils.isEmpty(params)) {
return StringUtils.EMPTY;
}
StringBuilder data = new StringBuilder();
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (data.length() > 0) {
data.append("&");
}
data.append(entry.getKey()).append("=").append(entry.getValue().toString());
}
return data.toString();
}
}

View File

@ -1,9 +1,10 @@
package cn.axzo.nanopart.api.config;
import cn.axzo.nanopart.api.constant.NanopartConstant;
import cn.axzo.nanopart.api.constant.BlackAndWhiteListConstant;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableFeignClients(NanopartConstant.BASIC_FEIGN_PACKAGE)
@EnableFeignClients(BlackAndWhiteListConstant.BASIC_FEIGN_PACKAGE)
public class NanopartApiAutoConfiguration {
}

View File

@ -0,0 +1,47 @@
package cn.axzo.nanopart.api.config;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 签名配置
*
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 14:33
*/
@Data
@Component
@ConfigurationProperties(prefix = "api.sign")
public class SignConfigProps {
/**
* 是否启用签名
*/
private Boolean enable = true;
private Map<String, KeyPair> keyPairs;
@Data
public static class KeyPair {
/**
* 签名算法
*/
private SignAlgorithm algorithm;
/**
* 私钥
*/
private String privateKey;
/**
* 公钥
*/
private String publicKey;
}
}

View File

@ -0,0 +1,75 @@
package cn.axzo.nanopart.api.config;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 签名管理器
*
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 15:41
*/
@Component
@RequiredArgsConstructor
public class SignManager {
private final SignConfigProps signConfigProps;
/**
* 生成签名
* 生成的签名为十六进制字符串
*
* @param appKey 调用方
* @param rawData 待签名数据格式所有需要参与签名计算的参数去重后按照字段名的ASCII码从小到大排序字典序使用URL键值对的格式即key1=value1&key2=value2拼接成字符串不包含签名字段
* @return 签名
*/
public String sign(String appKey, String rawData) {
Sign sign = getSignByAppKey(appKey);
if (Objects.isNull(sign)) {
return null;
}
return sign.signHex(rawData.getBytes(StandardCharsets.UTF_8));
}
/**
* 验证签名
*
* @param appKey 调用方
* @param rawData 待验证数据
* @param rawSign 签名
* @return true:验证成功 false:验证失败
*/
public boolean verify(String appKey, String rawData, String rawSign) {
Sign sign = getSignByAppKey(appKey);
if (Objects.isNull(sign)) {
return false;
}
return sign.verify(rawData.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex(rawSign));
}
/**
* 获取签名
*
* @param appKey 调用方
* @return 签名算法对象 {@link Sign}
*/
public Sign getSignByAppKey(String appKey) {
if (CollectionUtils.isEmpty(signConfigProps.getKeyPairs())) {
return null;
}
SignConfigProps.KeyPair keyPair = signConfigProps.getKeyPairs().get(appKey);
if (Objects.isNull(keyPair)) {
return null;
}
return SecureUtil.sign(keyPair.getAlgorithm(), keyPair.getPrivateKey(), keyPair.getPublicKey());
}
}

View File

@ -7,7 +7,7 @@ package cn.axzo.nanopart.api.constant;
* @modifiedBy
* @version: 1.0
*/
public class NanopartConstant {
public class BlackAndWhiteListConstant {
public static final String BASIC_FEIGN_PACKAGE = "cn.axzo.nanopart.api";
public static final String PHONE_REGEXP = "^1[3456789]\\d{9}$";

View File

@ -1,7 +1,7 @@
package cn.axzo.nanopart.api.request;
import cn.axzo.nanopart.api.constant.NanopartConstant;
import cn.axzo.nanopart.api.constant.BlackAndWhiteListConstant;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@ -21,6 +21,6 @@ public class BlackAndWhiteListInReq {
@NotBlank(message = "模块名称不能为空格")
private String module;
@Pattern(regexp = NanopartConstant.PHONE_REGEXP,message = "手机号格式错误")
@Pattern(regexp = BlackAndWhiteListConstant.PHONE_REGEXP,message = "手机号格式错误")
private String phone;
}

View File

@ -0,0 +1,53 @@
package cn.axzo.nanopart.api.request;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 黑白名单内部自同步请求
*
* @author chenwenjian
* @version 1.0
* @date 2024/1/3 16:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BlackAndWhiteListInternalSyncReq {
@NotNull(message = "同步类型不能为空")
private ListTypeEnum type;
/**
* 同步源模块
*/
@NotBlank(message = "同步源模块不能为空")
private String sourceModule;
/**
* 同步源字段
*/
@NotEmpty(message = "同步源字段不能为空")
private List<String> sourceFields;
/**
* 同步目标模块
*/
@NotBlank(message = "同步目标模块不能为空")
private String targetModule;
/**
* 同步目标字段
*/
@NotEmpty(message = "同步目标字段不能为空")
private List<String> targetFields;
}

View File

@ -0,0 +1,80 @@
package cn.axzo.nanopart.api.request;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
/**
* 黑白名单平台同步请求
*
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 18:17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BlackAndWhiteListPlatformSyncReq {
/**
* 名单类型0为黑名单1为白名单
*/
@NotNull(message = "名单类型错误")
private ListTypeEnum type;
/**
* 模块名自主定义cms-login
*/
@NotBlank(message = "模块名不能为空")
private String module;
/**
* 参数[{"name":"username","value":"admin"},{"name":"password","value":"123456"}]
* 单次同步数量最多1000条
*/
@NotEmpty(message = "参数不能为空")
private List<Map<String, Object>> params;
/**
* 分片号从1开始默认是1若等于1则先删除所有上次同步的名单然后新增本次的名单若大于1时则直接新增到黑白名单中
*/
@Min(value = 1, message = "分片号必须大于1")
@NotNull(message = "分片大小不能为空")
private Long fragment = 1L;
/**
* 是否同步至登录黑名单
*/
private Boolean isSyncLogin;
/**
* 同步至登录黑名单时params中代表手机号的字段名userPhone
* <p>
* 因为目前登录黑名单以手机号作为判断依据所以当isSyncLogin为true时此字段必填
* </p>
*/
private String syncLoginField;
/**
* 是否同步至注册黑名单
*/
private Boolean isSyncRegister;
/**
* 同步至注册黑名单时params中代表手机号的字段名userPhone
* <p>
* 因为目前注册黑名单以手机号作为判断依据所以当isSyncRegister为true时此字段必填
* </p>
*/
private String syncRegisterField;
}

View File

@ -1,7 +1,10 @@
package cn.axzo.nanopart.api.request;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@ -15,6 +18,9 @@ import java.util.Map;
* @version: 1.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BlackAndWhiteListReq {
/**

View File

@ -0,0 +1,35 @@
package cn.axzo.nanopart.api.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.Map;
/**
* 签名获取请求
* @author chenwenjian
* @version 1.0
* @date 2024/1/4 10:15
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SignatureGetReq {
/**
* 应用key例如nanopart
*/
@NotBlank(message = "appKey不能为空")
private String appKey;
/**
* 请求参数需要参与计算签名的数据例如{"id":123456,"name":"张三"}
*/
@NotEmpty(message = "params不能为空")
private Map<String,Object> params;
}

View File

@ -33,5 +33,13 @@
<artifactId>black-list-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.pokonyan</groupId>
<artifactId>pokonyan</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,19 @@
package cn.axzo.nanopart.server.constant;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/1/3 11:28
*/
public class BlackAndWhiteListConstant {
/**
* 所有平台登录黑名单模块名
*/
public static final String ALL_LOGIN = "all-login";
/**
* 企业注册黑名单模块名
*/
public static final String OU_REGISTER_BLACK_LIST = "ou_register_black_list";
}

View File

@ -3,13 +3,17 @@ package cn.axzo.nanopart.server.controller;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.api.BlackAndWhiteListApi;
import cn.axzo.nanopart.api.annotation.CheckSign;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import cn.axzo.nanopart.api.request.BlackAndWhiteListExcelImportReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListInternalSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListPlatformSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListReq;
import cn.axzo.nanopart.api.response.BlackAndWhiteListExcelImportResp;
import cn.axzo.nanopart.api.response.BlackAndWhiteListResp;
import cn.axzo.nanopart.server.dao.entity.SaasBlackWhiteList;
import cn.axzo.nanopart.server.service.BlackAndWhiteListService;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
@ -44,12 +48,12 @@ public class BlackAndWhiteListController implements BlackAndWhiteListApi {
@Override
public ApiResult<Long> create(BlackAndWhiteListReq req) {
return blackAndWhiteListService.create(req);
return ApiResult.ok(blackAndWhiteListService.create(req));
}
@Override
public ApiResult<Void> delete(BlackAndWhiteListReq req) {
return blackAndWhiteListService.delete(req);
return ApiResult.ok(blackAndWhiteListService.delete(req));
}
@Override
@ -59,12 +63,12 @@ public class BlackAndWhiteListController implements BlackAndWhiteListApi {
@Override
public ApiResult<List<BlackAndWhiteListResp>> detail(BlackAndWhiteListReq req) {
return blackAndWhiteListService.detail(req);
return ApiResult.ok(blackAndWhiteListService.detail(req));
}
@Override
public ApiResult<Boolean> isInBlackOrWhiteList(BlackAndWhiteListReq req) {
return blackAndWhiteListService.isInBlackOrWhiteList(req);
return ApiResult.ok(blackAndWhiteListService.isInBlackOrWhiteList(req));
}
@Override
@ -73,4 +77,29 @@ public class BlackAndWhiteListController implements BlackAndWhiteListApi {
return ApiResult.ok(blackAndWhiteListService.importBlackAndWhiteListExcel(type, module, url, file));
}
/**
* 平台同步黑白名单
*
* @param req @{link BlackAndWhiteListPlatformSyncReq}
* @return
*/
@CheckSign(requireSignFields = {"appKey", "timestamp"})
@Override
public ApiResult<Boolean> platformSync(BlackAndWhiteListPlatformSyncReq req) {
log.info("平台同步参数req = {}", JSONUtil.toJsonStr(req));
return ApiResult.ok(blackAndWhiteListService.platformSync(req));
}
/**
* 内部自同步黑白名单
*
* @param req @{link BlackAndWhiteListInternalSyncReq}
* @return 同步结果的记录id
*/
@CheckSign(requireSignFields = {"appKey", "timestamp"})
@Override
public ApiResult<Long> internalSync(BlackAndWhiteListInternalSyncReq req) {
return ApiResult.ok(blackAndWhiteListService.internalSync(req));
}
}

View File

@ -0,0 +1,66 @@
package cn.axzo.nanopart.server.controller;
import cn.axzo.framework.domain.ServiceException;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.api.SignatureUtilApi;
import cn.axzo.nanopart.api.config.SignManager;
import cn.axzo.nanopart.api.request.SignatureGetReq;
import cn.axzo.nanopart.api.common.util.StringUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 16:59
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class SignatureUtilController implements SignatureUtilApi {
private final SignManager signManager;
/**
* 生成密钥对
*
* @param algorithm 算法
* @return 密钥对
*/
@Override
public ApiResult<Map<String, String>> generateKeyPair(String algorithm) {
log.info("algorithm = {}", algorithm);
Map<String, String> keyPair = new HashMap<>();
try {
KeyPair generateKeyPair = SecureUtil.generateKeyPair(algorithm);
keyPair.put("publicKey", Base64.encode(generateKeyPair.getPublic().getEncoded()));
keyPair.put("privateKey", Base64.encode(generateKeyPair.getPrivate().getEncoded()));
} catch (Exception e) {
throw new ServiceException("密钥对生成失败");
}
log.info("keyPair = {}", JSONUtil.toJsonStr(keyPair));
return ApiResult.ok(keyPair);
}
/**
* 获取签名
*
* @param req 请求
* @return 签名
*/
@Override
public ApiResult<String> getSignature(SignatureGetReq req) {
String data = StringUtil.extracted(req.getParams());
return ApiResult.ok(signManager.sign(req.getAppKey(), data));
}
}

View File

@ -1,11 +1,15 @@
package cn.axzo.nanopart.server.dao.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.Map;
@ -16,7 +20,11 @@ import java.util.Map;
* @modifiedBy
* @version: 1.0
*/
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@TableName(value = "saas_black_white_list",autoResultMap = true)
public class SaasBlackWhiteList extends BaseEntity<SaasBlackWhiteList> {

View File

@ -7,6 +7,7 @@ import cn.axzo.nanopart.server.dao.entity.SaasBlackWhiteList;
import cn.axzo.nanopart.server.dao.mapper.BlackAndWhiteListMapper;
import cn.azxo.framework.common.utils.StringUtils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
@ -35,9 +36,6 @@ public class BlackAndWhiteListRepository extends ServiceImpl<BlackAndWhiteListMa
public Long create(BlackAndWhiteListReq req) {
SaasBlackWhiteList blackWhiteList = BeanMapper.copyBean(req, SaasBlackWhiteList.class, (req1, saasBlackWhiteList) -> saasBlackWhiteList.setType(req1.getType().getValue()));
Date nowTime = new Date(System.currentTimeMillis());
blackWhiteList.setCreateAt(nowTime);
blackWhiteList.setUpdateAt(nowTime);
blackAndWhiteListMapper.insert(blackWhiteList);
return blackWhiteList.getId();
}
@ -50,7 +48,7 @@ public class BlackAndWhiteListRepository extends ServiceImpl<BlackAndWhiteListMa
.eq(SaasBlackWhiteList::getIsDelete, 0)
.set(SaasBlackWhiteList::getIsDelete, saasBlackWhiteList.getId())
.update();
log.info("deleted record:{}", JSONObject.valueToString(saasBlackWhiteList));
log.info("deleted record:{}", JSONUtil.toJsonStr(saasBlackWhiteList));
});
}
return null;
@ -83,4 +81,50 @@ public class BlackAndWhiteListRepository extends ServiceImpl<BlackAndWhiteListMa
.set(SaasBlackWhiteList::getIsDelete, 1)
.update();
}
/**
* 按模块删除名单
*
* @param type 名单类型
* @param module 模块名
* @return 删除结果true删除成功false删除失败
*/
public Boolean deleteByModule(Integer type, String module) {
return lambdaUpdate()
.eq(SaasBlackWhiteList::getType, type)
.eq(SaasBlackWhiteList::getModule, module)
.eq(SaasBlackWhiteList::getIsDelete, 0)
.set(SaasBlackWhiteList::getIsDelete, 1)
.update();
}
/**
* 更新名单参数
*
* @param id 名单id
* @param param 名单参数
* @return 更新结果true更新成功false更新失败
*/
public boolean updateParamById(Long id, Map<String, Object> param) {
return lambdaUpdate()
.eq(SaasBlackWhiteList::getId, id)
.eq(SaasBlackWhiteList::getIsDelete, 0)
.set(SaasBlackWhiteList::getParam, JSONUtil.toJsonStr(param))
.update();
}
/**
* 根据模块名获取名单
*
* @param type 名单类型
* @param module 模块名
* @return 名单列表
*/
public List<SaasBlackWhiteList> getListByModule(Integer type, String module) {
return lambdaQuery()
.eq(SaasBlackWhiteList::getType, type)
.eq(SaasBlackWhiteList::getModule, module)
.eq(SaasBlackWhiteList::getIsDelete, 0)
.list();
}
}

View File

@ -2,6 +2,8 @@ package cn.axzo.nanopart.server.service;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import cn.axzo.nanopart.api.request.BlackAndWhiteListInternalSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListPlatformSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListReq;
import cn.axzo.nanopart.api.response.BlackAndWhiteListExcelImportResp;
import cn.axzo.nanopart.api.response.BlackAndWhiteListResp;
@ -19,15 +21,19 @@ import java.util.List;
*/
public interface BlackAndWhiteListService {
ApiResult<Long> create(BlackAndWhiteListReq req);
Long create(BlackAndWhiteListReq req);
ApiResult<Void> delete(BlackAndWhiteListReq req);
Void delete(BlackAndWhiteListReq req);
ApiResult<List<BlackAndWhiteListResp>> detail(BlackAndWhiteListReq req);
List<BlackAndWhiteListResp> detail(BlackAndWhiteListReq req);
ApiResult<Boolean> isInBlackOrWhiteList(BlackAndWhiteListReq req);
Boolean isInBlackOrWhiteList(BlackAndWhiteListReq req);
BlackAndWhiteListExcelImportResp importBlackAndWhiteListExcel(ListTypeEnum type, String module, String url, MultipartFile file);
Boolean deleteByIds(List<Long> ids);
Boolean platformSync(BlackAndWhiteListPlatformSyncReq req);
Long internalSync(BlackAndWhiteListInternalSyncReq req);
}

View File

@ -2,20 +2,26 @@ package cn.axzo.nanopart.server.service.impl;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.api.constant.enums.ListTypeEnum;
import cn.axzo.nanopart.api.request.BlackAndWhiteListInternalSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListPlatformSyncReq;
import cn.axzo.nanopart.api.request.BlackAndWhiteListReq;
import cn.axzo.nanopart.api.response.BlackAndWhiteListExcelImportResp;
import cn.axzo.nanopart.api.response.BlackAndWhiteListResp;
import cn.axzo.nanopart.server.constant.BlackAndWhiteListConstant;
import cn.axzo.nanopart.server.dao.entity.SaasBlackWhiteList;
import cn.axzo.nanopart.server.dao.repository.BlackAndWhiteListRepository;
import cn.axzo.nanopart.server.service.BlackAndWhiteListService;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@ -24,11 +30,14 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
@ -40,11 +49,18 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Service
@RefreshScope
@RequiredArgsConstructor
public class BlackAndWhiteListServiceImpl implements BlackAndWhiteListService {
private final BlackAndWhiteListRepository blackAndWhiteListRepository;
/**
* 黑白名单外部平台单次最大同步记录数
*/
@Value("${sync.fragmentSize:1000}")
private Long fragmentSize;
/**
* 新增一条黑白名单记录
*
@ -52,17 +68,17 @@ public class BlackAndWhiteListServiceImpl implements BlackAndWhiteListService {
* @return 记录id
*/
@Override
public ApiResult<Long> create(BlackAndWhiteListReq req) {
public Long create(BlackAndWhiteListReq req) {
// 黑白名单创建互斥,若存在于黑名单中则不允许存在于白名单中反之亦然
BlackAndWhiteListReq reverseReq = BeanMapper.copyBean(req, BlackAndWhiteListReq.class, (req1, req2) -> req2.setType((req1.getType().equals(ListTypeEnum.BLACK_LIST)) ? ListTypeEnum.WHITE_LIST : ListTypeEnum.BLACK_LIST));
if (isInBlackOrWhiteList(reverseReq).getData()) {
if (isInBlackOrWhiteList(reverseReq)) {
throw new ServiceException("该记录已存在于" + reverseReq.getType().getDescription() + "中,不能创建对应" + req.getType().getDescription());
}
// 同类型名单内唯一性校验
if (isInBlackOrWhiteList(req).getData()) {
if (isInBlackOrWhiteList(req)) {
throw new ServiceException("该记录已存在于" + req.getType().getDescription() + "");
}
return ApiResult.ok(blackAndWhiteListRepository.create(req));
return blackAndWhiteListRepository.create(req);
}
/**
@ -72,8 +88,8 @@ public class BlackAndWhiteListServiceImpl implements BlackAndWhiteListService {
* @return null
*/
@Override
public ApiResult<Void> delete(BlackAndWhiteListReq req) {
return ApiResult.ok(blackAndWhiteListRepository.delete(req));
public Void delete(BlackAndWhiteListReq req) {
return blackAndWhiteListRepository.delete(req);
}
/**
@ -83,10 +99,9 @@ public class BlackAndWhiteListServiceImpl implements BlackAndWhiteListService {
* @return 记录列表
*/
@Override
public ApiResult<List<BlackAndWhiteListResp>> detail(BlackAndWhiteListReq req) {
public List<BlackAndWhiteListResp> detail(BlackAndWhiteListReq req) {
List<SaasBlackWhiteList> saasBlackWhiteLists = blackAndWhiteListRepository.detail(req);
List<BlackAndWhiteListResp> blackAndWhiteListResps = BeanMapper.copyList(saasBlackWhiteLists, BlackAndWhiteListResp.class);
return ApiResult.ok(blackAndWhiteListResps);
return BeanMapper.copyList(saasBlackWhiteLists, BlackAndWhiteListResp.class);
}
/**
@ -96,9 +111,9 @@ public class BlackAndWhiteListServiceImpl implements BlackAndWhiteListService {
* @return 记录存在返回true否则返回false
*/
@Override
public ApiResult<Boolean> isInBlackOrWhiteList(BlackAndWhiteListReq req) {
public Boolean isInBlackOrWhiteList(BlackAndWhiteListReq req) {
List<SaasBlackWhiteList> blackWhiteLists = blackAndWhiteListRepository.detail(req);
return ApiResult.ok(CollectionUtil.isNotEmpty(blackWhiteLists));
return CollectionUtil.isNotEmpty(blackWhiteLists);
}
/**
@ -144,6 +159,123 @@ public class BlackAndWhiteListServiceImpl implements BlackAndWhiteListService {
return true;
}
/**
* 平台同步黑白名单
*
* @param req {@link BlackAndWhiteListPlatformSyncReq}
* @return true-同步成功false-同步失败
*/
@Override
public Boolean platformSync(BlackAndWhiteListPlatformSyncReq req) {
if (Objects.isNull(req.getType())) {
throw new ServiceException("请选择正确的名单类型");
}
if (req.getParams().size() > fragmentSize) {
throw new ServiceException("请主动进行分片多次同步,单次同步数小于" + fragmentSize + "条数据");
}
// 第一个分片需要先删除所有对应模块的名单再新增
if (req.getFragment() == 1 ){
deleteByModule(req.getType(),req.getModule());
}
// 新增
List<SaasBlackWhiteList> collect = req.getParams().stream().map(param -> SaasBlackWhiteList.builder()
.type(req.getType().getValue())
.module(req.getModule())
.param(param)
.build()).collect(Collectors.toList());
boolean saved = blackAndWhiteListRepository.saveBatch(collect);
// 同步至登录黑名单
if (Objects.nonNull(req.getIsSyncLogin()) && req.getIsSyncLogin()){
internalSync(BlackAndWhiteListInternalSyncReq.builder()
.type(req.getType())
.sourceModule(req.getModule())
.sourceFields(Collections.singletonList(req.getSyncLoginField()))
.targetModule(BlackAndWhiteListConstant.ALL_LOGIN).build());
}
// 同步至注册黑名单
if (Objects.nonNull(req.getIsSyncRegister()) && req.getIsSyncRegister()){
internalSync(BlackAndWhiteListInternalSyncReq.builder()
.type(req.getType())
.sourceModule(req.getModule())
.sourceFields(Collections.singletonList(req.getSyncRegisterField()))
.targetModule(BlackAndWhiteListConstant.OU_REGISTER_BLACK_LIST).build());
}
return saved;
}
/**
* 内部自同步黑白名单
*
* @param req {@link BlackAndWhiteListInternalSyncReq}
* @return 同步成功的记录总条数
*/
public Long internalSync(BlackAndWhiteListInternalSyncReq req) {
AtomicReference<Long> count = new AtomicReference<>(0L);
// 获取所有源记录
List<SaasBlackWhiteList> sourceList = blackAndWhiteListRepository.getListByModule(req.getType().getValue(), req.getSourceModule());
sourceList.stream().map(s -> {
// 抽取出需要同步的字段值
HashMap<String, Object> map = new HashMap<>();
req.getSourceFields().forEach(sf -> map.put(sf, s.getParam().get(sf)));
return map;
}).forEach(m -> {
// 判断是否存在目标记录不存在则新增一条
if (!isInBlackOrWhiteList(BlackAndWhiteListReq.builder()
.type(req.getType())
.module(req.getTargetModule())
.param(m)
.build())) {
create(BlackAndWhiteListReq.builder()
.type(req.getType())
.module(req.getTargetModule())
.param(m)
.build());
count.getAndSet(count.get() + 1);
}
});
return count.get();
}
/**
* 获取指定模块名的所有黑/白名单记录
*
* @param type 名单类型
* @param module 模块名
* @return /白名单记录列表
*/
public List<BlackAndWhiteListResp> getListByModule(ListTypeEnum type, String module) {
return BeanMapper.copyList(blackAndWhiteListRepository.getListByModule(type.getValue(), module), BlackAndWhiteListResp.class);
}
/**
* 更新指定id的黑白名单记录
*
* @param id 记录id
* @param param 记录参数
* @return true-更新成功false-更新失败
*/
public boolean updateParamById(Long id, Map<String, Object> param) {
return blackAndWhiteListRepository.updateParamById(id, param);
}
/**
* 删除指定模块名的所有黑/白名单记录
*
* @param type 名单类型
* @param module 模块名
* @return true-删除成功false-删除失败
*/
public Boolean deleteByModule(ListTypeEnum type, String module) {
if (Objects.isNull(type)) {
throw new ServiceException("待删除名单类型不能为空");
}
if (!StringUtils.hasText(module)) {
throw new ServiceException("待删除模块名不能为空");
}
return blackAndWhiteListRepository.deleteByModule(type.getValue(), module);
}
/**
* 使用EasyExcel读取Excel数据
*

View File

@ -4,12 +4,14 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@MapperScan(value = {"cn.axzo.**.mapper"})
@SpringBootApplication
@EnableFeignClients(basePackages = {
"cn.axzo.nanopart.api"
})
@EnableAspectJAutoProxy()
public class NanopartApplication {
public static void main(String[] args) {
SpringApplication.run(NanopartApplication.class, args);

View File

@ -4,7 +4,7 @@ spring:
cloud:
nacos:
config:
server-addr: ${NACOS_HOST:dev-nacos.axzo.cn}:${NACOS_PORT:80}
server-addr: ${NACOS_HOST:https://dev-nacos.axzo.cn}:${NACOS_PORT:443}
file-extension: yaml
namespace: ${NACOS_NAMESPACE_ID:35eada10-9574-4db8-9fea-bc6a4960b6c7}
prefix: ${spring.application.name}