package com.iailab.framework.signature.core.aop; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import com.iailab.framework.common.exception.ServiceException; import com.iailab.framework.common.util.servlet.ServletUtils; import com.iailab.framework.signature.core.annotation.ApiSignature; import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import javax.servlet.http.HttpServletRequest; import java.util.Map; import java.util.Objects; import java.util.SortedMap; import java.util.TreeMap; import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; /** * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 * * @author Zhougang */ @Aspect @Slf4j @AllArgsConstructor public class ApiSignatureAspect { private final ApiSignatureRedisDAO signatureRedisDAO; @Before("@annotation(signature)") public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { // 1. 验证通过,直接结束 if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { return; } // 2. 验证不通过,抛出异常 log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), joinPoint.getArgs()); throw new ServiceException(BAD_REQUEST.getCode(), StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg())); } public boolean verifySignature(ApiSignature signature, HttpServletRequest request) { // 1.1 校验 Header if (!verifyHeaders(signature, request)) { return false; } // 1.2 校验 appId 是否能获取到对应的 appSecret String appId = request.getHeader(signature.appId()); String appSecret = signatureRedisDAO.getAppSecret(appId); Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); // 2. 校验签名【重要!】 String clientSignature = request.getHeader(signature.sign()); // 客户端签名 String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串 String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名 if (ObjUtil.notEqual(clientSignature, serverSignature)) { return false; } // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) String nonce = request.getHeader(signature.nonce()); signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit()); return true; } /** * 校验请求头加签参数 * * 1. appId 是否为空 * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟 * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了 * 4. sign 是否为空 * * @param signature signature * @param request request * @return 是否校验 Header 通过 */ private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { // 1. 非空校验 String appId = request.getHeader(signature.appId()); if (StrUtil.isBlank(appId)) { return false; } String timestamp = request.getHeader(signature.timestamp()); if (StrUtil.isBlank(timestamp)) { return false; } String nonce = request.getHeader(signature.nonce()); if (StrUtil.length(nonce) < 10) { return false; } String sign = request.getHeader(signature.sign()); if (StrUtil.isBlank(sign)) { return false; } // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) long expireTime = signature.timeUnit().toMillis(signature.timeout()); long requestTimestamp = Long.parseLong(timestamp); long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); if (timestampDisparity > expireTime) { return false; } // 3. 检查 nonce 是否存在,有且仅能使用一次 return signatureRedisDAO.getNonce(nonce) == null; } /** * 构建签名字符串 * * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥 * * @param signature signature * @param request request * @param appSecret appSecret * @return 签名字符串 */ private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) { SortedMap parameterMap = getRequestParameterMap(request); // 请求头 SortedMap headerMap = getRequestHeaderMap(signature, request); // 请求参数 String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体 return MapUtil.join(parameterMap, "&", "=") + requestBody + MapUtil.join(headerMap, "&", "=") + appSecret; } /** * 获取请求头加签参数 Map * * @param request 请求 * @param signature 签名注解 * @return signature params */ private static SortedMap getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) { SortedMap sortedMap = new TreeMap<>(); sortedMap.put(signature.appId(), request.getHeader(signature.appId())); sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); sortedMap.put(signature.nonce(), request.getHeader(signature.nonce())); return sortedMap; } /** * 获取请求参数 Map * * @param request 请求 * @return queryParams */ private static SortedMap getRequestParameterMap(HttpServletRequest request) { SortedMap sortedMap = new TreeMap<>(); for (Map.Entry entry : request.getParameterMap().entrySet()) { sortedMap.put(entry.getKey(), entry.getValue()[0]); } return sortedMap; } }