写作缘由
当我们与其他系统进行数据同步时,此时,别人的系统会请求我们的系统。此时,最重要的一个问题肯定就是安全问题了。因为他们直接请求我们的接口,所以我们必须释放我们接口的权限。但是如果我们完全释放,什么都不做约定,我们的接口很容易被非法网路操作者利用,从而获取我们的信息。
我们面临的安全问题有以下三种:
- 请求来源(身份)是否合法?
- 请求参数被篡改?
- 请求的唯一性(不可复制)
公共接口安全设计:
无约定
客户端:传递参数 http://localhost:8000/api/dataSync/informer?beginTime=1623764900000
服务端:接收参数-------进行数据库查询-------返回数据
public Result<Object> getLastInformer(RequestParam long beginTime) {
//直接进行数据库查询
----------------------------------------
getInformers(beginTime);
}
如上,这种方式简单粗暴,通过调用getInformers方法即可获取人员信息了,但是 这样的方式会存在很严重的安全性问题,没有进行任何的验证,大家都可以通过这个方法获取到人员信息,导致人员信息泄露。
那么,如何验证调用者身份呢?如何防止参数被篡改呢?
约定
客户端:
1.给客户端分配 key 和 secret (如:key:keyone secret: 1!@521);
2.客户端请求头上拼接一个时间戳
3.使用MD5加密(secret+请求参数+时间戳) 生成sign,作为参数传递
例如:
Long beginTime(查询时间,必填,13位时间戳 ) : 例如:1624628174000 表示要查询信息的开始时间
Long timeStamp(当前时间,必填,13位时间戳) : 例如:1624628174000 充当时间戳作用
String key (键,必填,值固定(datacenter))
String sign (签名,必填) 值: MD5处理(1!@521+beginTime+timeStamp)注意:1.+号在这里起链接作用,实际传参并没有 2.MD5使用字符编码为utf-8,加密后转换为16进制(小写)。解密后字母分全大写或全小写,我这里使用的是小写。
处理后生成请求路径:
地址+请求参数+key+sign+时间戳
http://localhost:8000/api/dataSync/case?beginTime=1624628174000&key=datacenter&sign=6064679340d45cca00d146a45e62217a&timeStamp=1626183517000
服务端:
public Result<Object> getLastInformer(DataSyncVO dataSyncVO) {
//查询是否有对应的key,有的话返回对应的secret
//判断时间戳是否过期
//检验sign是否正确
//以上均正确:进行查询
----------------------------------------
getInformers(dataSyncVO);
}
时间戳作用:虽然我们在客户端和服务器端做了约定,只要secret不暴漏,正常情况下我们的接口是不会被恶意请求的,但是我们sign还是拼接在url路径上的,所以一旦他们得到了其中一个请求路径,他就可以利用这个路径的sign随意请求了。我们加了时间戳之后,规定好该sign失效时间(比如说5分钟内),那么就算他拿到了我们的sign,使用时间也就只有五分钟,可以减轻信息暴漏的程度。
时间戳可能产生的问题:
- 前后端时区不同,如果客户端和开发人员和你不在同一个时区,那么很可能造成时区不一致而导致你们的时间不一致,前后误差几个小时。(双方确定号时区,使用统一的时区)
- 还有一种小概率可能是服务请求时,请求发送时间过长,服务端配置的该时间戳有效时间果短,导致拒绝访问(延长有效时间)。
- 时间戳虽然重要,但是并不是必须的,如果在信息并不是十分绝密的情况下,不使用时间戳也是完全可以的。
key 和 secret
如果多个客户端都需要我们的数据时,我们可以针对不同的客户,分配不同的key和secret。注意:secret一定要保密!
服务端校验示例
@Data
public class DataSyncVO {
private String key;
private String sign;
private Long beginTime;
private Long timeStamp;
}
public boolean checkPermission(DataSyncVO dataSyncVO) {
if (ObjectUtil.isNotNull(dataSyncVO)) {
if (ObjectUtil.isNull(dataSyncVO.getBeginTime())) {
throw new BadRequestException("查询条件beginTime不能为空");
}
} else {
throw new BadRequestException("参数不能为空");
}
//检验key是否存在
String secret = EncodeEnum.getEncodeEnum(dataSyncVO.getKey());
if (StrUtil.isEmpty(secret)){
throw new BadRequestException("您无权访问");
}
//检验时间戳是否过期
LocalDateTime timeStamp = LocalDateTime.ofEpochSecond(dataSyncVO.getTimeStamp() / 1000, 0, ZoneOffset.ofHours(8));
LocalDateTime timeNow = LocalDateTime.now();
Duration duration = Duration.between(timeStamp, timeNow);
//相差的分钟数
long minutes = duration.toMinutes();
System.out.println(minutes);
if (minutes >= 10) {
throw new BadRequestException("您无权访问");
}
//获取sign
String sign = secret + dataSyncVO.getBeginTime() + dataSyncVO.getTimeStamp();
//服务端MD5加密
byte[] digest = null;
try {
MessageDigest md5 = MessageDigest.getInstance("md5");
digest = md5.digest(sign.getBytes("utf-8"));
} catch (Exception e) {
e.printStackTrace();
}
String md5Str = new BigInteger(1, digest).toString(16);//转为16进制
//sign对比
if (md5Str.equals(dataSyncVO.getSign())) {
return true;
} else {
throw new BadRequestException("您无权访问");
}
}
注意:
加密算法
加密算法分为可逆加密和不可逆加密。
可逆加密又分为对称性加密和非对称性加密。
可逆加密: 能获取加密前数据。
不可逆加密(摘要加密): 获取不到加密前数据,只能将原数据使用相同方式加密,然后进行对比。(用户密码) MD5、SHA-1、SHA-256、HMAC
对称性加密: 加密解密使用同一把钥匙 AES、DES
非对称性加密分为公钥和私钥。对应的公钥解对应的私钥,对应的私钥解对应的公钥 RSA、DSA