数据签名的意义
数据签名就是我们在发送 HTTP 请求的时候,增加一个无法伪造的字符串,用来保证数据在传输的过程中不被篡改。
数据签名使用比较多的算法是 MD5 算法,这个算法是将要提交的数据,通过某种方式组合成一个字符串,然后通过 MD5 算法生成一个签名。
如果觉得单纯的 md5 不够安全的话,可以在 md5 加密的时候加盐或者加hash或者加接口密钥,进一步降低请求被劫持后存在模拟的风险。
md5介绍
md5 消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16个字符(BYTES))的散列值(hash value),用于确保信息传输完整一致。
- 不管明文多长,散列后的密文定长
- 明文不一样,散列后结果一定不一样
- 散列后的密文不可逆
- 一般用于校验数据完整性、签名 sign
- 由于密文不可逆,所以后台无法还原,也就是说他要验证,会在后台以跟前台一样的方式去重新签名一遍。也就是说他会把源数据和签名后的值一起提交到后台。所以我们要保证在签名时候的数据和提交上去的源数据一致。这种算法特喜欢在内部加入时间戳。但是避免一个问题:在签名的时候取一次时间戳,然后在提交源数据的时候,又取一次时间戳,这样会导致后台验证失败。
使用md5进行数据签名
一个简单的数据签名例子
签名公式: md5(参数列表)
- 请求参数:
src: name = test & password = 123 - 直接对请求参数增加签名,只需要调用 md5 算法:
md5(src) - 并将得到的签名添加给原先请求参数的sign属性:
src.sign = md5(src) - 最终提交给后端的json数据如下:
{
"name": "test",
"password": "123",
"sign": "012f6e456621d373afed4e8326271234"
}
服务端生成的签名与客户端生成的签名进行比对,判断是否一致,如果一致,则说明客户端传过来的参数没有被修改;如果不一致,则证明客户端传过来的参数已经被修改。
function genSign(data) {
const keySortArr = Object.keys(data)
.sort() // 将请求参数对象的属性数组Object.keys(data)成员按照首字母ASCII码进行排序
.filter(v => {
return data[v] !== undefined && `${data[v]}`.length !== 0; // 将数组中 值为undefined 和 空字符串 的参数成员 筛选掉,得到 有意义的参数 用于生成签名
});
const sign = `${keySortArr
.map(k => `${k}=${data[k]}`)
.join('&')}`; // 将排序删选好的请求参数数组拼接name=test&age=123 格式的字符串形式
return md5(sign); // 利用处理好的请求参数字符串name=test&age=123 进行生成签名, 如012f6e456621d373afed4e8326271234
}
完善上面的数据签名例子
针对上述情况,存在安全隐患:假设请求被中间人劫持,中间人获取到请求参数并做了篡改,并用修改了的请求参数生成了一个新的签名,添加到请求参数的sign字段,服务端收到被修改的数据,也根据修改的数据生成一个签名,与请求时发来的签名sign对比,两个签名一致,服务器端根本无法察觉请求数据被修改过。
为了解决上述的问题,我们可以在 请求参数 被 md5 加密之前,在请求参数末尾拼接上一个只有服务端和客户端知道的密钥,src: name=test & password=123 & key={ 密钥 }
之后再进行 md5 加密,并将获得的数据签名赋值给请求参数的sign字段,src.sign = md5(src)
签名公式:md5(参数列表 & key={ 接口密钥 })
最终提交给后端的json数据就会是下面这个样子:
{
"name": "test",
"password": "123",
"sign": "098f6bcd4621d373cade4e832627b4f6"
}
假设请求数据被中途截获并被篡改,签名也被修改,注意此处的签名生成过程是无法加入那个只有客户端和服务端知道的密钥key={ 接口密钥 }的,服务器端在接收到该请求后,将请求数据(除去签名sign字段)拼上只有客户端和服务端知道的密钥key={ 接口密钥 },进行一次md5算法加密,生成签名,并与请求中的sign字段的签名对比,立马能发现异常。
封装一个生成数据签名的函数
function genSign(data) {
const keySortArr = Object.keys(data)
.sort() // 将 请求参数对象的属性数组 **Object.keys(data) 成员按照首字母ASCII码进行排序
.filter(v => {
return data[v] !== undefined && `${data[v]}`.length !== 0; // 将数组中值为undefined 和空字符串的参数成员筛选掉,得到有意义的参数用于生成签名
});
const sign = `${keySortArr
.map(k => `${k}=${data[k]}`)
.join('&')}&key=${feedbackSignSecretKey}`; // 将排序删选好的请求参数数组 拼接成 name=test&age=123的字符串形式,并拼接上一个只有服务端和客户端知道的这个接口的密钥
return md5(sign); // 利用处理好的请求参数字符串 name=test&age=123 进行生成签名, 如098f6bcd4621d373cade4e832627b4f6
}
更安全的方法:客户端生成md5签名+时间戳
签名公式:**md5(参数列表 & key={ 接口密钥 }) + 时间戳
客户端生成md5加密签名串,和当前时间戳跟随请求的参数一起发送到服务器端,服务器端获取获取签名进行验证,并保存到redis中并设置一个失效时长(60s),则60s内不能使用相同的sign发送请求,然后获取系统当前时间戳和前端发送过来的时间戳做比较,如果两者相差60s,则认定为非法操作。验证时间戳可以防止重复提交,推荐使用。
{
"name": "testing",
"base": {
"from": "android", //来源
"userId": 123, //登录用户id
"signature": "9070D6BBE067283F2A25BE9ACBE0211E", //客户端生成的签名
"timestamp": 1570518677803 //客户端生成的时间戳
}
}