潘志宝
2024-11-11 aa1aa68141e3ee33f98cdd785ddc5c244fedc592
提交 | 用户 | 时间
e7c126 1 package com.iailab.framework.signature.core.aop;
H 2
3 import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.map.MapUtil;
5 import cn.hutool.core.util.ObjUtil;
6 import cn.hutool.core.util.StrUtil;
7 import cn.hutool.crypto.digest.DigestUtil;
8 import com.iailab.framework.common.exception.ServiceException;
9 import com.iailab.framework.common.util.servlet.ServletUtils;
10 import com.iailab.framework.signature.core.annotation.ApiSignature;
11 import com.iailab.framework.signature.core.redis.ApiSignatureRedisDAO;
12 import lombok.AllArgsConstructor;
13 import lombok.extern.slf4j.Slf4j;
14 import org.aspectj.lang.JoinPoint;
15 import org.aspectj.lang.annotation.Aspect;
16 import org.aspectj.lang.annotation.Before;
17
18 import javax.servlet.http.HttpServletRequest;
19 import java.util.Map;
20 import java.util.Objects;
21 import java.util.SortedMap;
22 import java.util.TreeMap;
23
24 import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
25
26 /**
27  * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名
28  *
29  * @author Zhougang
30  */
31 @Aspect
32 @Slf4j
33 @AllArgsConstructor
34 public class ApiSignatureAspect {
35
36     private final ApiSignatureRedisDAO signatureRedisDAO;
37
38     @Before("@annotation(signature)")
39     public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
40         // 1. 验证通过,直接结束
41         if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
42             return;
43         }
44
45         // 2. 验证不通过,抛出异常
46         log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
47                 joinPoint.getArgs());
48         throw new ServiceException(BAD_REQUEST.getCode(),
49                 StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg()));
50     }
51
52     public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
53         // 1.1 校验 Header
54         if (!verifyHeaders(signature, request)) {
55             return false;
56         }
57         // 1.2 校验 appId 是否能获取到对应的 appSecret
58         String appId = request.getHeader(signature.appId());
59         String appSecret = signatureRedisDAO.getAppSecret(appId);
60         Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
61
62         // 2. 校验签名【重要!】
63         String clientSignature = request.getHeader(signature.sign()); // 客户端签名
64         String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
65         String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
66         if (ObjUtil.notEqual(clientSignature, serverSignature)) {
67             return false;
68         }
69
70         // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
71         String nonce = request.getHeader(signature.nonce());
72         signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
73         return true;
74     }
75
76     /**
77      * 校验请求头加签参数
78      *
79      * 1. appId 是否为空
80      * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
81      * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
82      * 4. sign 是否为空
83      *
84      * @param signature signature
85      * @param request   request
86      * @return 是否校验 Header 通过
87      */
88     private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
89         // 1. 非空校验
90         String appId = request.getHeader(signature.appId());
91         if (StrUtil.isBlank(appId)) {
92             return false;
93         }
94         String timestamp = request.getHeader(signature.timestamp());
95         if (StrUtil.isBlank(timestamp)) {
96             return false;
97         }
98         String nonce = request.getHeader(signature.nonce());
99         if (StrUtil.length(nonce) < 10) {
100             return false;
101         }
102         String sign = request.getHeader(signature.sign());
103         if (StrUtil.isBlank(sign)) {
104             return false;
105         }
106
107         // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)
108         long expireTime = signature.timeUnit().toMillis(signature.timeout());
109         long requestTimestamp = Long.parseLong(timestamp);
110         long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
111         if (timestampDisparity > expireTime) {
112             return false;
113         }
114
115         // 3. 检查 nonce 是否存在,有且仅能使用一次
116         return signatureRedisDAO.getNonce(nonce) == null;
117     }
118
119     /**
120      * 构建签名字符串
121      *
122      * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
123      *
124      * @param signature signature
125      * @param request   request
126      * @param appSecret appSecret
127      * @return 签名字符串
128      */
129     private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
130         SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
131         SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数
132         String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体
133         return MapUtil.join(parameterMap, "&", "=")
134                 + requestBody
135                 + MapUtil.join(headerMap, "&", "=")
136                 + appSecret;
137     }
138
139     /**
140      * 获取请求头加签参数 Map
141      *
142      * @param request 请求
143      * @param signature 签名注解
144      * @return signature params
145      */
146     private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
147         SortedMap<String, String> sortedMap = new TreeMap<>();
148         sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
149         sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
150         sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
151         return sortedMap;
152     }
153
154     /**
155      * 获取请求参数 Map
156      *
157      * @param request 请求
158      * @return queryParams
159      */
160     private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
161         SortedMap<String, String> sortedMap = new TreeMap<>();
162         for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
163             sortedMap.put(entry.getKey(), entry.getValue()[0]);
164         }
165         return sortedMap;
166     }
167
168 }
169