feat(REQ-2040): 签名支持

This commit is contained in:
chenwenjian 2024-01-02 17:12:53 +08:00
parent 105506e35c
commit dfb191f736
7 changed files with 359 additions and 0 deletions

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

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

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,145 @@
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.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.Enumeration;
import java.util.Objects;
/**
* @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() {
}
@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() {
}
/**
* 切入含有@CheckSign && @RestController 注解的类
*/
@Before(value = "@within(checkSign) && @within(restController)")
public void classHandler(JoinPoint joinPoint, CheckSign checkSign, RestController restController) {
handle(joinPoint, checkSign);
}
/**
* 切入含有@checkSign && @RequestMapping/@PostMapping/@GetMapping/@PutMapping/@DeleteMapping/@PatchMapping 之一注解的方法
*/
@Before(value = "@annotation(checkSign) && mappingAnnotations()")
public void methodHandler(JoinPoint joinPoint, CheckSign checkSign) {
handle(joinPoint, checkSign);
}
@SneakyThrows
public void handle(JoinPoint joinPoint, CheckSign checkSign) {
// 验签
verifySignature(checkSign);
}
private void verifySignature(CheckSign checkSign) {
HttpServletRequest httpRequest = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
// 获取调用方
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中获取需要验签的参数字段
StringBuilder data = new StringBuilder();
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("&");
}
} 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).append("&");
}
}
log.info("data:{}", data);
try {
boolean verify = signManager.verify(appKey, data.toString(), sign);
if (!verify) {
throw new BizException(new RespCode("403", "验签失败"));
}
} catch (Exception e) {
throw new BizException(new RespCode("403", "验签失败"));
}
}
}

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 待签名数据
* @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

@ -0,0 +1,31 @@
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.hutool.crypto.SecureUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import java.security.KeyPair;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/1/2 16:59
*/
@Slf4j
@RestController
public class SignatureUtilController implements SignatureUtilApi {
@Override
public ApiResult<KeyPair> generateKeyPair(String algorithm) {
log.info("algorithm = {}", algorithm);
KeyPair keyPair = null;
try {
keyPair = SecureUtil.generateKeyPair(algorithm);
} catch (Exception e) {
throw new ServiceException("密钥对生成失败");
}
return ApiResult.ok(keyPair);
}
}