Skip to content

Commit e57948f

Browse files
authored
Merge pull request #3 from slipper4j/master
feat: 新增admin4j-signature模块
2 parents 1df28bd + 8f66cd9 commit e57948f

20 files changed

Lines changed: 794 additions & 0 deletions

File tree

admin4j-signature/pom.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.admin4j</groupId>
8+
<artifactId>framework</artifactId>
9+
<version>${revision}</version>
10+
</parent>
11+
12+
<groupId>com.admin4j.signature</groupId>
13+
<artifactId>admin4j-signature</artifactId>
14+
<version>${admin4j-signature.version}</version>
15+
<packaging>pom</packaging>
16+
17+
<properties>
18+
<admin4j-signature.version>0.8.0</admin4j-signature.version>
19+
<maven.compiler.source>8</maven.compiler.source>
20+
<maven.compiler.target>8</maven.compiler.target>
21+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
22+
</properties>
23+
24+
<modules>
25+
<module>signature-core</module>
26+
<module>signature-spring-boot-starter</module>
27+
</modules>
28+
29+
</project>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.admin4j.signature</groupId>
8+
<artifactId>admin4j-signature</artifactId>
9+
<version>${admin4j-signature.version}</version>
10+
</parent>
11+
12+
<artifactId>signature-core</artifactId>
13+
14+
<properties>
15+
<maven.compiler.source>8</maven.compiler.source>
16+
<maven.compiler.target>8</maven.compiler.target>
17+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>com.admin4j.common</groupId>
23+
<artifactId>admin4j-common</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.springframework</groupId>
27+
<artifactId>spring-webmvc</artifactId>
28+
<scope>provided</scope>
29+
</dependency>
30+
<dependency>
31+
<groupId>javax.servlet</groupId>
32+
<artifactId>javax.servlet-api</artifactId>
33+
<scope>provided</scope>
34+
</dependency>
35+
<dependency>
36+
<groupId>com.admin4j.common</groupId>
37+
<artifactId>admin4j-common-spring-web</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-starter-data-redis</artifactId>
42+
</dependency>
43+
</dependencies>
44+
45+
</project>
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package com.admin4j.framework.signature;
2+
3+
import com.admin4j.framework.signature.annotation.Signature;
4+
import com.admin4j.framework.signature.properties.SignatureProperties;
5+
import com.alibaba.fastjson2.JSONObject;
6+
import org.apache.commons.lang3.StringUtils;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.data.redis.core.StringRedisTemplate;
10+
import org.springframework.util.CollectionUtils;
11+
import org.springframework.util.DigestUtils;
12+
13+
import javax.servlet.http.HttpServletRequest;
14+
import java.io.IOException;
15+
import java.util.Map;
16+
import java.util.SortedMap;
17+
import java.util.TreeMap;
18+
import java.util.concurrent.TimeUnit;
19+
20+
/**
21+
* @author zhougang
22+
* @since 2023/11/10 10:43
23+
*/
24+
public abstract class AbstractSignature implements SignatureService {
25+
26+
private static final Logger log = LoggerFactory.getLogger(AbstractSignature.class);
27+
28+
private final StringRedisTemplate stringRedisTemplate;
29+
30+
private final SignatureProperties signatureProperties;
31+
32+
private static final String SIGNATURE_NONCE_REDIS_KEY = "signature:nonce:";
33+
34+
public AbstractSignature(StringRedisTemplate stringRedisTemplate, SignatureProperties signatureProperties) {
35+
this.stringRedisTemplate = stringRedisTemplate;
36+
this.signatureProperties = signatureProperties;
37+
}
38+
39+
/**
40+
* 判断请求是否签名通过
41+
*
42+
* @param request HttpServletRequest
43+
* @return 是否通过
44+
*/
45+
@Override
46+
public boolean verify(Signature signature, HttpServletRequest request) throws IOException {
47+
// 根据appId获取appSecret
48+
String appId = request.getHeader(signature.appId().filedName());
49+
String appSecret;
50+
if (StringUtils.isBlank(appId) ||
51+
StringUtils.isBlank(appSecret = getAppSecret(request.getHeader(signature.appId().filedName())))) {
52+
return false;
53+
}
54+
// 根据request 中 header值生成SignatureHeaders实体
55+
if (!verifyHeaders(signature, request)) {
56+
return false;
57+
}
58+
// 获取全部参数(包括URL和Body上的)
59+
SortedMap<String, String> allParams = getAllParams(signature, request);
60+
// 生成服务端签名
61+
String plainText = paramsSplicing(allParams, appSecret);
62+
// 将digest 转换成UTF-8 的 byte[] 后 使用MD5算法加密,最后将生成的md5字符串
63+
String serverSign = digestEncoder(plainText);
64+
// 客户端签名
65+
String clientSign = request.getHeader(signature.sign().filedName());
66+
if (!StringUtils.equals(clientSign, serverSign)) {
67+
return false;
68+
}
69+
String nonce = allParams.get(signature.nonce().filedName());
70+
// 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
71+
stringRedisTemplate.opsForValue().set(SIGNATURE_NONCE_REDIS_KEY + nonce, nonce, signatureProperties.getExpireTime() * 2, TimeUnit.MILLISECONDS);
72+
return true;
73+
}
74+
75+
/*
76+
private SignatureHeaders getSignatureHeaders(Signature signature, HttpServletRequest request) {
77+
SignatureHeaders signatureHeaders = new SignatureHeaders();
78+
signatureHeaders.setAppId(request.getHeader(signature.appId().filedName()));
79+
signatureHeaders.setAppId(request.getHeader(signature.timestamp().filedName()));
80+
signatureHeaders.setAppId(request.getHeader(signature.nonce().filedName()));
81+
signatureHeaders.setAppId(request.getHeader(signature.sign().filedName()));
82+
return signatureHeaders;
83+
}
84+
*/
85+
86+
/**
87+
* 1.appId是否合法,appId是否有对应的appSecret。
88+
* 2.请求是否已经超时,默认10分钟。
89+
* 3.随机串是否合法,是否在指定时间内已经访问过了。
90+
* 4.sign是否合法。
91+
*/
92+
private boolean verifyHeaders(Signature signature, HttpServletRequest request) {
93+
94+
String timestamp = request.getHeader(signature.timestamp().filedName());
95+
//Assert.notNull(timestamp, "timestamp cannot be empty");
96+
if (StringUtils.isBlank(timestamp)) {
97+
return false;
98+
}
99+
100+
Long expireTime = signatureProperties.getExpireTime();
101+
//其他合法性校验
102+
long requestTimestamp = Long.parseLong(timestamp);
103+
// 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)
104+
long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
105+
//Assert.isTrue(!(timestampDisparity > expireTime), "Request time exceeds the specified limit");
106+
if (timestampDisparity > expireTime) {
107+
return false;
108+
}
109+
110+
String nonce = request.getHeader(signature.nonce().filedName());
111+
//Assert.notNull(nonce, "Random strings cannot be empty");
112+
if (StringUtils.isBlank(nonce)) {
113+
return false;
114+
}
115+
//Assert.isTrue(!(nonce.length() < 10), "The random string nonce length is at least 10 bits");
116+
if (nonce.length() < 10) {
117+
return false;
118+
}
119+
String cacheNonce = stringRedisTemplate.opsForValue().get(SIGNATURE_NONCE_REDIS_KEY + nonce);
120+
//Assert.isNull(cacheNonce, "This nonce has already been used and the request is invalid");
121+
if (StringUtils.isNotBlank(cacheNonce)) {
122+
return false;
123+
}
124+
125+
String sign = request.getHeader(signature.sign().filedName());
126+
//Assert.notNull(sign, "sign cannot be empty");
127+
return StringUtils.isNotBlank(sign);
128+
}
129+
130+
/**
131+
* 获取全部参数(包括URL和Body上的)
132+
*
133+
* @param request request
134+
* @return
135+
*/
136+
protected SortedMap<String, String> getAllParams(Signature signature, HttpServletRequest request) throws IOException {
137+
138+
SortedMap<String, String> sortedMap = new TreeMap<>();
139+
140+
sortedMap.put(signature.appId().filedName(), request.getHeader(signature.appId().filedName()));
141+
sortedMap.put(signature.timestamp().filedName(), request.getHeader(signature.timestamp().filedName()));
142+
sortedMap.put(signature.nonce().filedName(), request.getHeader(signature.nonce().filedName()));
143+
// 有url带动态参数的情况, 所以加上url, 客户端对应也要拼接
144+
sortedMap.put("url", request.getServletPath());
145+
146+
// 获取parameters(对应@RequestParam)
147+
if (!CollectionUtils.isEmpty(request.getParameterMap())) {
148+
Map<String, String[]> requestParams = request.getParameterMap();
149+
//获取GET请求参数,以键值对形式保存
150+
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
151+
sortedMap.put(entry.getKey(), entry.getValue()[0]);
152+
}
153+
}
154+
155+
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
156+
// 分别获取了request input stream中的body信息、parameter信息
157+
JSONObject data = JSONObject.parseObject(requestWrapper.getBody());
158+
// 获取POST请求的JSON参数,以键值对形式保存
159+
for (Map.Entry<String, Object> entry : data.entrySet()) {
160+
sortedMap.put(entry.getKey(), entry.getValue().toString());
161+
}
162+
163+
return sortedMap;
164+
}
165+
166+
/**
167+
* 所有的参数与应用密钥appSecret 进行排序加密后生成签名
168+
*
169+
* @param sortedMap 根据key升序排序的后所有请求参数
170+
* @param appSecret 应用id对应的应用密钥
171+
* @return 生成接口签名
172+
*/
173+
protected String paramsSplicing(SortedMap<String, String> sortedMap, String appSecret) {
174+
// 进行key value拼接
175+
StringBuilder plainText = new StringBuilder();
176+
for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
177+
plainText.append(entry.getKey()).append(entry.getValue());
178+
}
179+
180+
// 结尾拼接应用密钥 appSecret
181+
plainText.append(appSecret);
182+
183+
// 摘要
184+
return plainText.toString();
185+
}
186+
187+
/**
188+
* 获取appId对应的secret,假数据
189+
*
190+
* @param appId 应用id
191+
* @return
192+
*/
193+
protected String getAppSecret(String appId) {
194+
return "";
195+
}
196+
197+
/**
198+
* 摘要加密
199+
* @param plainText
200+
* @return
201+
*/
202+
protected String digestEncoder(String plainText) throws IOException {
203+
return DigestUtils.md5DigestAsHex(StringUtils.getBytes(plainText, "UTF-8"));
204+
}
205+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.admin4j.framework.signature;
2+
3+
import javax.servlet.ReadListener;
4+
import javax.servlet.ServletInputStream;
5+
import javax.servlet.http.HttpServletRequest;
6+
import javax.servlet.http.HttpServletRequestWrapper;
7+
import java.io.*;
8+
9+
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
10+
11+
private final String body;
12+
13+
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
14+
super(request);
15+
StringBuilder stringBuilder = new StringBuilder();
16+
BufferedReader bufferedReader = null;
17+
try {
18+
InputStream inputStream = request.getInputStream();
19+
if (inputStream != null) {
20+
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
21+
char[] charBuffer = new char[128];
22+
int bytesRead;
23+
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
24+
stringBuilder.append(charBuffer, 0, bytesRead);
25+
}
26+
}
27+
} finally {
28+
if (bufferedReader != null) {
29+
bufferedReader.close();
30+
}
31+
}
32+
body = stringBuilder.toString();
33+
}
34+
35+
@Override
36+
public ServletInputStream getInputStream() {
37+
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
38+
return new ServletInputStream() {
39+
@Override
40+
public boolean isFinished() {
41+
return false;
42+
}
43+
44+
@Override
45+
public boolean isReady() {
46+
return false;
47+
}
48+
49+
@Override
50+
public void setReadListener(ReadListener readListener) {
51+
}
52+
53+
@Override
54+
public int read() {
55+
return byteArrayInputStream.read();
56+
}
57+
};
58+
}
59+
60+
@Override
61+
public BufferedReader getReader() {
62+
return new BufferedReader(new InputStreamReader(this.getInputStream()));
63+
}
64+
65+
public String getBody() {
66+
return this.body;
67+
}
68+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.admin4j.framework.signature;
2+
3+
import com.admin4j.framework.signature.properties.SignatureProperties;
4+
import org.springframework.data.redis.core.StringRedisTemplate;
5+
6+
/**
7+
* 签名的默认实现
8+
*
9+
* @author zhougang
10+
* @since 2023/10/11 13:39
11+
*/
12+
public class DefaultSignature extends AbstractSignature {
13+
14+
public DefaultSignature(StringRedisTemplate stringRedisTemplate, SignatureProperties signatureProperties) {
15+
super(stringRedisTemplate, signatureProperties);
16+
}
17+
}

0 commit comments

Comments
 (0)