对比新文件 |
| | |
| | | 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<String, String> parameterMap = getRequestParameterMap(request); // 请求头 |
| | | SortedMap<String, String> 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<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) { |
| | | SortedMap<String, String> 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<String, String> getRequestParameterMap(HttpServletRequest request) { |
| | | SortedMap<String, String> sortedMap = new TreeMap<>(); |
| | | for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) { |
| | | sortedMap.put(entry.getKey(), entry.getValue()[0]); |
| | | } |
| | | return sortedMap; |
| | | } |
| | | |
| | | } |
| | | |