feat(REQ-2040): 添加获取签名接口,优化验签aop逻辑

This commit is contained in:
chenwenjian 2024-01-04 14:37:56 +08:00
parent dd9fd06516
commit 96f59473ac
7 changed files with 201 additions and 38 deletions

View File

@ -1,7 +1,9 @@
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;
@ -14,11 +16,13 @@ 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;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.List;
/**

View File

@ -1,8 +1,12 @@
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;
@ -24,6 +28,15 @@ public interface SignatureUtilApi {
* @param algorithm 算法例如RSA
* @return 公私钥对
*/
@GetMapping("/api/signature/generateKeyPair")
@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

@ -3,28 +3,34 @@ 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.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
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.lang.reflect.Method;
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
@ -35,33 +41,76 @@ import java.util.Objects;
@Aspect
@Order(1)
@Component
@RequiredArgsConstructor
public class CheckSignAspect {
private final SignManager signManager;
@Resource
private SignManager signManager;
@Pointcut(value = "@annotation(cn.axzo.nanopart.api.annotation.CheckSign)")
public void checkSign() {
@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("checkSign()")
public void verifySignature(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
CheckSign authorizeAnnotation = method.getAnnotation(CheckSign.class);
log.info("before......");
verifySignature(authorizeAnnotation);
@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", "验签失败"));
@ -73,38 +122,37 @@ public class CheckSignAspect {
throw new BizException(new RespCode("403", "验签失败"));
}
// 从Header中获取需要验签的参数字段
StringBuilder data = new StringBuilder();
// 从Header中获取需要验签的参数字段若字段值为空则该字段不计入签名计算
Map<String, String> attendFields = new HashMap<>();
// 无指定字段则所有字段参与计算签名
if (ArrayUtil.isEmpty(checkSign.requireSignFields())) {
Enumeration<String> headerNames = httpRequest.getHeaderNames();
if (CollectionUtils.isEmpty(Collections.list(headerNames))) {
throw new BizException(new RespCode("403", "验签失败"));
}
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String value = httpRequest.getHeader(headerName);
if (StringUtils.isBlank(value)) {
throw new BizException(new RespCode("403", "验签失败"));
}
data.append(headerName).append("=").append(value).append("&");
attendFields.put(headerName, value);
}
} else {
// 有指定参与计算签名的字段则只提取指定字段参与计算签名
for (String field : checkSign.requireSignFields()) {
String value = httpRequest.getHeader(field);
if (StringUtils.isBlank(value)) {
throw new BizException(new RespCode("403", "验签失败"));
}
data.append(field).append("=").append(value);
// 判断最后一个字段如果是最后一个字段则不拼接"&"
if (field.equals(checkSign.requireSignFields()[checkSign.requireSignFields().length - 1])) {
data.append("&");
}
attendFields.put(field, value);
}
}
log.info("data:{}", data);
// 对所有参与签名计算的字段去除空值和去重后按照字段名的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.toString(), sign);
boolean verify = signManager.verify(appKey, data, sign);
if (!verify) {
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

@ -28,7 +28,7 @@ public class SignManager {
* 生成的签名为十六进制字符串
*
* @param appKey 调用方
* @param rawData 待签名数据
* @param rawData 待签名数据格式所有需要参与签名计算的参数去重后按照字段名的ASCII码从小到大排序字典序使用URL键值对的格式即key1=value1&key2=value2拼接成字符串不包含签名字段
* @return 签名
*/
public String sign(String appKey, String rawData) {

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

@ -3,9 +3,13 @@ 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;
@ -20,7 +24,17 @@ import java.util.Map;
*/
@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);
@ -35,4 +49,18 @@ public class SignatureUtilController implements SignatureUtilApi {
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));
}
}