之前做的一个对外网关项目,安全需要所以得对参数进行加密校验
参数
接口交互约定了三个参数:
- timestamp(请求时间戳)
- data(实际请求数据,经过加密得到的字符串)
- sign(以HMAC-SHA1算法对参数的签名)
请求方式
接口统一使用json来进行交互,请求的Content-Type统一为application/json
方案
1.需引入依赖 commons-codec
1
2
3
4
5
|
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
|
2.根据需要先提供一个工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@UtilityClass
@CommonsLog
public class EncryptUtils {
public static String HMAC_KEY = "abcd1234abcd";
/**
* 以HMAC-SHA1算法对字符串toEnc进行签名
*/
public static String hmacSha1(String toEnc, String key) {
HmacUtils hmacUtils = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, key);
return hmacUtils.hmacHex(toEnc);
}
public static String urlEncode(String str) {
return urlEncode(str, StandardCharsets.UTF_8);
}
public static String urlEncode(String str, Charset charset) {
try {
return URLEncoder.encode(str, charset.toString());
} catch (UnsupportedEncodingException e) {
log.info("URL encode 失败", e);
return null;
}
}
}
|
3.声明一个空接口,在下面的参数处理器中,仅处理实现了Encryptable接口的参数类对象
1
2
|
public interface Encryptable {
}
|
声明加密后的参数对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class EncryptModel {
/**
* 时间戳(毫秒)
*/
private Long timestamp;
/**
* json加密后的字符串
*/
private String data;
/**
* 参数签名
*/
private String signature;
}
|
4.通过自定义 RequestBodyAdviceAdapter
来实现对参数的自动解密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
@RequiredArgsConstructor
@ControllerAdvice(assignableTypes = {OpenApiController.class})
@CommonsLog
public class RequestDecryptAdvice extends RequestBodyAdviceAdapter {
// 请求时间戳检验的时间限制
private static Integer TIME_LIMIT = 5;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
Class<?> clazz = (Class) targetType;
return Encryptable.class.isAssignableFrom(clazz);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
Class<?> clazz = (Class) targetType;
if (Encryptable.class.isAssignableFrom(clazz)) {
log.info("校验OpenApi请求");
ByteArrayInputStream inputStream = convert(inputMessage.getBody());
log.info("OpenApi请求校验成功");
return new MappingJacksonInputMessage(inputStream, inputMessage.getHeaders());
} else {
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}
/**
* 校验签名
*
* @param body 请求体
* @return 转换后的请求参数体
*/
private ByteArrayInputStream convert(InputStream body) throws IOException {
String content = StreamUtils.copyToString(body, StandardCharsets.UTF_8);
EncryptModel encryptModel = JsonUtil.readValue(content, EncryptModel.class);
log.info("OpenApi请求校验, 校验参数: " + JsonUtil.toJson(encryptModel));
checkEncryptModel(encryptModel);
Long timestamp = encryptModel.getTimestamp();
log.info("OpenApi请求校验, 校验时间戳: " + timestamp);
checkTimestamp(timestamp);
String dataFrom64 = encryptModel.getData();
byte[] data = Base64.decodeBase64(dataFrom64);
encryptModel.setData(new String(data));
String rawSign = getRowSign(encryptModel);
log.info("构建参数, 待签名参数:" + rawSign);
String sign = EncryptUtils.hmacSha1(rawSign, HMAC_KEY);
log.info("OpenApi请求校验, 校验签名: " + sign);
if (StringKit.ne(sign, encryptModel.getSignature())) {
throw new OpenApiException("签名校验失败");
}
return new ByteArrayInputStream(data);
}
/**
* 拼接参数,生成待签名的字符串
* @param encryptModel 请求参数
* @return 待签名字符串
*/
private String getRowSign(EncryptModel encryptModel) {
String data = encryptModel.getData();
JsonNode dataNode = JsonUtil.readValue(data, JsonNode.class);
List<String> paramList = new ArrayList<>();
paramList.add("timestamp=" + EncryptUtils.urlEncode(encryptModel.getTimestamp().toString()));
dataNode.fieldNames().forEachRemaining(key -> {
String value = dataNode.get(key).asText(null);
if (StringUtils.isBlank(value)) {
return;
}
String encode = EncryptUtils.urlEncode(value);
paramList.add(key + "=" + encode);
});
paramList.sort(String::compareTo);
return String.join("&", paramList);
}
/**
* 校验加密请求参数
* @param encryptModel 参数
*/
private void checkEncryptModel(EncryptModel encryptModel) {
if (encryptModel == null) {
throw new OpenApiException("参数不能为空");
}
if (encryptModel.getTimestamp() == null) {
throw new OpenApiException("缺少参数: timestamp");
}
if (encryptModel.getData() == null) {
throw new OpenApiException("缺少参数: data");
}
if (encryptModel.getSignature() == null) {
throw new OpenApiException("缺少参数: signature");
}
}
/**
* 校验请求时间戳
* @param timestamp 时间戳
*/
private void checkTimestamp(Long timestamp) {
Instant now = Instant.now().atZone(ZoneId.systemDefault()).toInstant();
Instant time = Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toInstant();
if (now.isBefore(time) || now.plus(TIME_LIMIT, ChronoUnit.MINUTES).isBefore(time)) {
throw new OpenApiException("无权访问");
}
}
}
|
测试
对接口测试可以通过对controller写单元测试,这是另一部分下次再说。
小结
如果只是对参数进行参数校验,防止参数被篡改,那其实只需要在收到请求后,对收到的请求参数以同样的方式签名,最终判断签名是否一致,即可防止请求被劫持导致的安全问题。
而Spring对于请求的处理机制,提供了很方便的方式去统一处理请求参数,通过自定义RequestBodyAdvice即可对参数进行统一的解析并校验签名。同样,如果需要对返回值进行统一的加密处理,也可以通过自定义的ResponseBodyAdvice来实现。