前言
最近有给写一个开发接口给第三方用。也趁机学习了一下有关如果提供一个接口的验签的过程
也有参考到这篇文章:https://mp.weixin.qq.com/s/VJX6h2Fl_a5N3iCWe777nA
如何接口安全问题
需要解决3个问题:
请求身份是否合法?
请求参数是否被篡改?
请求是否唯一?
请求身份
为开发者分配accessKey(开发者标识,确保唯一)和secretKey(用于接口加密,确保不易被穷举,生成算法不易被猜测)。
防止篡改
按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA;
在stringA最后拼接上Secretkey得到字符串stringSignTemp;
对stringSignTemp进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值。
请求携带参数accessKey和sign,只有拥有合法的身份accessKey和正确的签名sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到secretKey(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。
重放攻击
虽然解决了请求参数被篡改的隐患,但是还存在着重复使用请求参数伪造二次请求的隐患。
使用
timestamp+nonce方案
nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
假设允许客户端和服务端最多能存在15分钟的时间差,同时追踪记录在服务端的nonce集合。当有新的请求进入时,首先检查携带的timestamp是否在15分钟内,如超出时间范围,则拒绝,然后查询携带的nonce,如存在已有集合,则拒绝。否则,记录该nonce,并删除集合内时间戳大于15分钟的nonce。
代码实现
基本请求类
public class OpenRequestDTO {
private String accessKey;
private String sign;
private LocalDateTime timestamp;
private String nonce;
//以下为get/set方法
继承后的一个实现类
public class TestOpenRequestDTO extends OpenRequestDTO{
private String paramA;
private String paramB;
//以下为get/set方法
具体的参数校验方法类
import com.chen.service.requestDTO.OpenRequestDTO;
import com.chen.service.requestDTO.TestOpenRequestDTO;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.Gson;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.BeanUtils;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class CheckSign {
private static String secretKey = "123456";
private static String accessKey = "testUser";
// 通过CacheBuilder构建一个缓存实例
private static Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(10000) // 设置缓存的最大容量
.expireAfterWrite(15, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效
.concurrencyLevel(10) // 设置并发级别为10
.recordStats() // 开启缓存统计
.build();
public static void main(String[] args) {
TestOpenRequestDTO openRequestDTO = new TestOpenRequestDTO();
openRequestDTO.setAccessKey(accessKey);
openRequestDTO.setNonce("qwe");
//先执行一次获得对应的sign
openRequestDTO.setSign("F36A9A4436D5C8B42A410C6A061FE333");
openRequestDTO.setParamA("a");
openRequestDTO.setParamB("b");
//注意设置时间与当前时间
LocalDateTime dateTime = LocalDateTime.of(2020, 11, 20, 11, 40, 0);
openRequestDTO.setTimestamp(dateTime);
OpenRequestDTO openRequestDTONext = new OpenRequestDTO();
BeanUtils.copyProperties(openRequestDTO,openRequestDTONext);
Gson gson = new Gson();
String a = gson.toJson(openRequestDTO);
System.out.println("请求json:"+a);
System.out.println(CheckSign.check(openRequestDTO));
System.out.println(CheckSign.check(openRequestDTONext));
}
public static boolean check(OpenRequestDTO openRequestDTO) {
if (!checkParams(openRequestDTO)) {
return false;
}
if (!checkTime(openRequestDTO)) {
return false;
}
if (!checkNonce(openRequestDTO)) {
return false;
}
String sign = openRequestDTO.getSign();
openRequestDTO.setSign(null);
Gson gson = new Gson();
String a = gson.toJson(openRequestDTO);
Map<String, Object> map = gson.fromJson(a, Map.class);
String result = CheckSign.sign(map, secretKey);
System.out.println("result:" + result);
if (result.equals(sign)) {
return true;
}
return false;
}
/**
* 参数校验
* @param openRequestDTO
* @return
*/
public static boolean checkParams(OpenRequestDTO openRequestDTO) {
if (openRequestDTO.getAccessKey() == null || openRequestDTO.getNonce() == null || openRequestDTO.getSign() == null || openRequestDTO.getTimestamp() == null) {
System.out.println("校验失败,有参数为空");
return false;
}
return true;
}
/**
* 时间校验
* @param openRequestDTO
* @return
*/
public static boolean checkTime(OpenRequestDTO openRequestDTO) {
Duration duration = Duration.between(openRequestDTO.getTimestamp(), LocalDateTime.now());
if (duration.toMinutes() > 15 || duration.toMinutes() < 0) {
System.out.println("校验失败,时间超时");
return false;
}
return true;
}
/**
* 随机数校验
* @param openRequestDTO
* @return
*/
public static boolean checkNonce(OpenRequestDTO openRequestDTO) {
String key = openRequestDTO.getNonce();
String value = null;
value = cache.getIfPresent(key);
if (value != null) {
System.out.println("校验失败,重复请求");
return false;
}
cache.put(openRequestDTO.getNonce(), openRequestDTO.getTimestamp().toString());
return true;
}
/**
* 签名校验
* @param attrs
* @param signKey
* @return
*/
public static String sign(Map<String, Object> attrs, String signKey) {
StringBuilder sb = new StringBuilder();
Set<String> strings = attrs.keySet();
ArrayList<String> keys = new ArrayList<>(strings);
Collections.sort(keys);
for (String key : keys) {
Object value = attrs.get(key);
buildField(sb, key, value);
}
sb.append(signKey);
System.out.println("最终参数为sb:" + sb);
return DigestUtils.md5Hex(sb.toString().getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
/**
* 参数构建
* @param sb
* @param key
* @param value
*/
private static void buildField(StringBuilder sb, String key, Object value) {
if (null != value) {
//第二次
if (sb.length() > 0) {
sb.append("&");
}
sb.append(key);
sb.append("=");
sb.append(value);
}
}
}
结果
请求json:{"paramA":"a","paramB":"b","accessKey":"testUser","sign":"F36A9A4436D5C8B42A410C6A061FE333","timestamp":{"date":{"year":2020,"month":11,"day":20},"time":{"hour":11,"minute":40,"second":0,"nano":0}},"nonce":"qwe"}
最终参数为sb:accessKey=testUser&nonce=qwe¶mA=a¶mB=b×tamp={date={year=2020.0, month=11.0, day=20.0}, time={hour=11.0, minute=40.0, second=0.0, nano=0.0}}123456
result:F36A9A4436D5C8B42A410C6A061FE333
true
校验失败,重复请求
false
说明
对应的jar包
spring工具
google的工具包
jdk1.8
google的gsong工具
参数设置
secretKey恒定为123456
accessKey恒定为testUser
这部分在服务器上可以用配置文件或者存放在数据库中
设置了一个google的缓存,用于存储请求的随机唯一值,在线上服务器可以用redis代替
代码使用
先localdatetime设置时间不能和现在时间超出15分钟。
直接启动main方法获得sign值,这个时候会提示校验出错,
获得产生的sign值进行修改
然后校验第一次通过,第二次由于随机数相同校验失败