防篡改: 防止客户端发送的数据包被拦截, 修改数据包中的数据让后再发往服务端

防重放: 客户端发给服务端的数据包被拦截, 然后重新发给服务端, 服务端对这个数据包处理了两次

先说防重放

a. 防守重放可以让数据包唯一, 可以让客户端每次请求都制作一个唯一ID放在请求数据包中, 服务端处理这个请求之后记录这个ID, 再收到相同的请求即判为重放, 这时服务端记录的数据包唯一ID是不断增长的.

b. 再对每个数据包设置有效期, 从客户端发出时携带时间戳, 到服务端验证数据包中的时间戳是否超过有效期(60s), 超出即判为过期的请求而丢掉, 这样的话服务端只需要记录60s内的ID即可.

满足这两步, 数据包被抓后修改为任意随机ID和60s内的时间戳后重放仍会被服务端判为合法请求.

这就需要防篡改

对随机ID和时间戳签名, 这样重放的ID和60s内的时间戳就不会跟签名匹配.

这个唯一ID不能使用时间戳代替, 因为两个合法的请求是可以在两个客户端同时发出的, 也有可能客户端在同一个时间点同时发送两个合法请求;如果仅仅使用随机字符串, 60s后服务端不会再记录使用过的ID, 所以要求在60s内不允许有两个合法请求使用了相同的随机字符串, 否则其中一个判为重放.

使用时间戳配合随机字符串的话, 极端情况要求两个合法请求在统一时刻制作了相同的随机字符串, 这大大降低了碰撞的可能性示例代码:

前端:var pk = "签名密钥:conf/settings.SIGN_KEY";

function genID(){
return Number(Math.random().toString().substr(3,7) + Date.now()).toString(36);
}
function send_req() {
// 制作date和noce
var date = (new Date()).getTime();
var nonce = genID();
// 模拟用户自定义header
var my_header = "test";
// 签名内容. 如果对referer, origin等敏感header属性保护, 和my_header一样, 把对应value拼接在sign_content上
var sign_content = date + nonce + my_header;
// 参与签名的key. 如果对referer, origin等敏感header属性保护, 和my_header一样, 把对应key拼接在sign_key上
// sign_content和sign_key需要按照一定的顺序对应
var sign_key = "signedKey=X-Date,X-Nonce,my-header";
// 计算签名
var hash = CryptoJS.HmacSHA1(sign_content, pk);
var sign = CryptoJS.enc.Base64.stringify(hash);
var sign_val = "sign=" + sign;
// 拼接x-sign header
var x_sign = sign_val + ";" + sign_key;
$.ajax({
async: false,
method: 'POST',
data: {
"request": "params"
},
url: 'http://localhost:8000/user/login/',
dataType: "json",
beforeSend: function(request) {
request.setRequestHeader("X-Date", date);
request.setRequestHeader("X-Nonce", nonce);
request.setRequestHeader("X-Sign", x_sign);
request.setRequestHeader("my-header", my_header);
},
success: function (data) {
if (data.success === true) {
console.log("true", data);
} else {
console.log("false", data);
}
},
error: function (e) {
console.log("error", e);
}
});
}
后端以Django在middleware中拦截为例:class APIReplayMiddleware(MiddlewareMixin):
def process_request(self, request):
meta = request.META
sign = meta.get('HTTP_X_SIGN')
try:
sign_val, sign_key = sign.split(";")
except (ValueError, AttributeError):
return JsonResponse({"code": 10415, "message": "Invalid sign param, date and nonce are required", "data": {}})
else:
sign = sign_val.replace("sign=", "")
keys = sign_key.replace("signedKey=", "").upper().replace("-", "_")
# 解析参与的key和这些key对应的value
sign_content, date, nonce = "", "", ""
for k in keys.split(","):
k = "HTTP_" + k if k not in ("CONTENT_LENGTH", "CONTENT_TYPE") else k
if k == "HTTP_X_DATE":
date = meta.get("HTTP_X_DATE")
if k == "HTTP_X_NONCE":
nonce = meta.get("HTTP_X_NONCE")
if meta.get(k):
sign_content += meta.get(k)
if not all((sign, date, nonce)):
return JsonResponse({"code": 10415, "message": "Invalid request, sign, date and nonce are required", "data": {}})
if not date.isdecimal():
return JsonResponse({"code": 10415, "message": "Invalid sign date", "data": {}})
if int(time.time() * 1000) - int(date) > settings.TIMESTAMP_EXPIRE * 1000:
return JsonResponse({"code": 10415, "message": "Sign expired", "data": {}})
h = hmac.new(settings.SIGN_KEY.encode('utf-8'), sign_content.encode('utf-8'), digestmod="sha1")
resign = base64.b64encode(h.digest()).decode('utf-8')
if resign != sign:
return JsonResponse({"code": 10415, "message": "Invalid Sign", "data": {}})
nonce_key = f'nonce:{date}:{nonce}'
if redis_cli.get(nonce_key):
return JsonResponse({"code": 10415, "message": "Sign has been used before", "data": {}})
redis_cli.set(nonce_key, 1, settings.TIMESTAMP_EXPIRE)
return

请求Header如下X-Sign: sign=6Ey+fHrak3it8S0dg0VNERTGhVw=;signedKey=X-Date,X-Nonce

X-Date: 1605749355945

X-Nonce: ewnk5htzxcw00

X-Sign中包含两对kv, signedKey表示此次签名对数据包中的哪些key的值以什么样的顺序进行进行签名,这里就是对1605749355945ewnk5htzxcw00这个字符串进行签名, sign表示此次签名的结果。

如果想对其它的属性签名例如自定义Header属性my-header, 则请求头如下, 其中sign=dg0VNERTGhVw6Ey+fHrak3it8S0=就是把X-Date,X-Nonce,my-header的值按这个顺序拼接后(即1605749355945ewnk5htzxcw00my-content)再签名得到的X-Sign: sign=dg0VNERTGhVw6Ey+fHrak3it8S0=;signedKey=X-Date,X-Nonce,my-header

X-Date: 1605749355945

X-Nonce: ewnk5htzxcw00

my-header: my-content

所以, 如果整个数据包进行签名, 那么对拦截的数据包作任何修改都会让服务器验证签名失败。