提交 | 用户 | 时间
|
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 |
|