接口请求参数防篡改检验方案

10月 18, 2018

目的

接口请求的参数如果没做防篡改的参数校验,攻击者可以拦截接口并修改参数,这样后端是不知道参数从前端发起后是否被篡改。比如小游戏游戏部分在前端,完成游戏后提交成绩给后端,攻击者可以抓包请求进行拦截篡改游戏成绩,这点已经被证明是可行的。基于此,打算指定一套前端与后端约定的参加校验规则,提高篡改数据的难度。

真正防范篡改数据很困难,因为加密规则都在前端代码里,有一定技能的人还是可以通过看前端代码获得加密校验算法,所以本文只是提高篡改参数的难度。之前想用对称加密,用一个密钥加密,但密钥在前端代码里,还是能获取到。后来又想动态获取密钥,但这样实现起来复杂很多。所以本文只是简单地做一些参数校验,把大部分恶意者拦截了。

加密思路

前端/客户端请求参数时,增加时间戳字段、校验码字段,也就是需要3个字段,分别是data、timestamp、sign,其中data字段为业务数据参数,timestamp为当前的时间戳,sign为参数校验字段。sign字段由 data+timestamp 进行md5。

###接口请求参数

data: 业务数据,采用json格式,可以为空。

timestamp:时间戳,即从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。

sign:参数校验字段,规则 MD5(data+timestamp),也可以再加一些盐值,不同项目可以做不同的约定,这里就不写了。

具体加密过程

这里附上后端代码,前端可以参考。

  1. 业务数据data用json格式,假设为:

    1
    $data = {"text":"我最棒"}
  2. 时间戳字段timestamp,假设现在时间是2018-10-01 08:00:00,转换成时间戳即为:

    1
    $timestamp = 1538352000
  3. 对上面两个参数进行md5:

    1
    2
    3
    4
    5
    6
    $sign = md5($data.$timestamp); //即 md5('{"text":"我最棒"}1538352000');

    /**
    * 得到md5 加密内容
    * $sign = '920f2aa820cbd374efa63ff5b5c465f4';
    **/

参数校验解密过程

时间校验

客户端已经传了timestamp字段,但客户端的时间可能跟服务器的时间不一样,加上传输过程有延时,所以不能简单地和服务器的时间戳相对进行对比,需要允许一点时间差。

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
/**
* 参数防篡改校验
*/
private function signValidate()
{
$parameters = app()->request->input();

$data = $parameters['data'] ?? '';
$timestamp = $parameters['timestamp'] ?? '';
$sign = $parameters['sign'] ?? '';

if (empty($timestamp) || empty($sign)) {
throw new ApiInternalServerException('缺少校验码', RetCode::ERR_WRONG_SIGN);
}

//校验时间差
if (abs($timestamp - time()) > 30) {
throw new ApiInternalServerException('该请求时间差有错', RetCode::ERR_WRONG_SIGN);
}

//检验具体参数
if ($sign != md5($data . $timestamp)) {
throw new ApiInternalServerException('检验码有误', RetCode::ERR_WRONG_SIGN);
}
}

取消校验

测试调试的时候可以关闭校验,在.env文件设置是否校验的开关。

Postman 设置

在开发的时候需要实时获取时间戳并进行MD5,若人工每次都去计算一下,那实在太蠢了,利用postman的发送请求前可以执行脚本和设置环境变量的功能,我们可以轻松地解决这个问题。在 Pre-request Scripttab页增加以下代码设置环境变量:

1
2
3
4
5
6
7
var timestamp = Math.floor((new Date().getTime())/1000);
var requestData = request.data["data"] || '';
var sign = CryptoJS.MD5(requestData + timestamp);

postman.setEnvironmentVariable("timestamp",timestamp);
postman.setEnvironmentVariable("sign",sign);
postman.setEnvironmentVariable("signtest",requestData + timestamp);

在请求的Body页添加以下参数:

KEY VALUE
timestamp
sign

前端封装好请求接口后,也不需要管这两个参数了。对前端开发的调试没造成多大的困扰。但每个请求接口在postman那都要设置一下这个Pre-request Script,有点麻烦,不知各位有没好的解决方案。