一种简单的接口验签规则

Published On December 28, 2016

category project | tags 签名 md5


背景介绍

当要限制http接口只能被特定的客户端调用的时候,我们就需要验证接口调用者的身份。可以通过http提供的Authorization头传递用户名密码的方式来完成身份验证,虽然用户名和密码经过base64编码后人眼看不出来,但很容易被解析出来。 现在普遍的做法是使用签名来验证身份,也就是通讯的双方约定好一个密钥,调用方将请求的数据使用该密钥生成一个签名并作为参数传递,服务方用同样的密钥对请求数据加密,然后和调用方传递过来的签名进行比较。 几乎每个公司内部都会使用一套自己的验签规则,不同系统应该使用一套统一的验签规则。

签名算法介绍

这里介绍一种极其简单但安全有效的签名算法,它有以下特点:

  • 验签规则是语言无关的
  • 对中间人攻击免疫,也即使窃听到http请求的内容也无法伪造客户端重新发起请求
    • 请求参数包含时间戳,服务端会和当前的时间戳比较,如果相差超过一定时长就认为是无效的
    • 签名使用md5单向hash散列生成
    • 签名的生成依赖key,但是key只有客户端和服务端才知道并不会通过参数传递,攻击者无法获取key,就不可能伪造出正确的签名
  • 不同的调用方使用不同的appid和appkey组合访问,使用这种组合的好处是:
    • 区分不同的调用者,方便统计和追踪问题,appid用于区别不同的调用者
    • 当要禁掉某个调用者的时候直接删掉对应的appid和appkey,如果只用一个统一的key则只能让所有其他的调用者都跟着修改key

具体的签名规则如下:

  1. 在参数中增加当前的时间戳(ts)和调用方id(appid)
  2. 生成签名
    1. 在query_string后面追加&appkey=得到临时的temp_query_string,appkey最好是纯ascii字符
    2. 通过md5(temp_query_string>)计算出签名sign
  3. 在query_string后面添加&sign=作为最终的query string

使用php实现的简单实例如下

<?php
/**
 * client.php
 *
 * Created by PhpStorm.
 * User: yxr
 * Date: 16/12/28
 * Time: 下午4:30
 */

$url = 'http://localhost/server.php';
$params = [
    'name'  => 'yanxr',
    'age'   => 25
];
$appid  = 'client1';
$appkey = '1234567890';
echo request($url, $params, $appid, $appkey);

// below are common functions which should be placed in a base class
function request($url, $params, $appid, $appkey, $method='GET')
{
    $qry_str = sign($params, $appid, $appkey);
    // echo $qry_str . '<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_TIMEOUT, '3');
    if ($method == 'GET') {
        curl_setopt($ch, CURLOPT_URL, $url . '?' . $qry_str);
    } elseif ($method == 'POST') {
        curl_setopt($ch, CURLOPT_URL, $url);
        // Set request method to POST
        curl_setopt($ch, CURLOPT_POST, 1);
        // Set query data here with CURLOPT_POSTFIELDS
        curl_setopt($ch, CURLOPT_POSTFIELDS, $qry_str);
    } else {
        throw new Exception('wrong method parameter');
    }
    $ret = trim(curl_exec($ch));
    curl_close($ch);
    return $ret;
}

function sign($params, $appid, $appkey)
{

    $params['ts'] = time();
    $params['appid'] = $appid;
    $qry_str = http_build_query($params);// http_build_query默认使用x-www-form-urlencoded编码
    $sign = md5($qry_str . '&appkey=' . $appkey);
    return $qry_str . '&sign=' . $sign;
}

<?php

/**
 * server.php
 *
 * Created by PhpStorm.
 * User: yxr
 * Date: 16/12/28
 * Time: 下午4:57
 */

if (!checkSignature($_REQUEST)) {
    die('you sucks');
}
$my_age = 25;
if (isset($_REQUEST['name'])) {
    echo "hello {$_REQUEST['name']} <br>";
    if (isset($_REQUEST['age'])) {
        $not = $_REQUEST['age'] == $my_age? '' : 'not ';
        echo "I am {$not}as old as you";
    }
} else {
    echo 'hello world';
}

function checkSignature($params)
{
    $max_interval = 3; // max time interval not more than 3 seconds
    // appid => appkey
    $app = [
        'client1' => '1234567890',
        'client2' => '1029384756'
    ];
    if (!isset($params['appid']) || !isset($app[$params['appid']])) {
        error_log("wrong appid\n", 3, "/tmp/my-errors.log");
        return false;
    }
    if (!isset($params['ts']) || abs($params['ts'] - $_SERVER['REQUEST_TIME']) > $max_interval) {
        error_log("wrong ts\n", 3, "/tmp/my-errors.log");
        return false;
    }
    if (!isset($params['sign'])) {
        error_log("sign not set\n", 3, "/tmp/my-errors.log");
        return false;
    }
    $origin_sign = $params['sign'];
    unset($params['sign']);
    $qry_str = http_build_query($params);
    $sign = md5($qry_str . '&appkey=' . $app[$params['appid']]);
    if ($origin_sign != $sign) {
        error_log("wrong signature: 1.check your appkey 2.check your sign generation method\n", 3, "/tmp/my-errors.log");
    }
    return $origin_sign == $sign;
}

改进

将第2步中生成签名的方式稍加变动即可得到另一种签名规则

  1. 将所有请求参数按参数名排序后的值用|串联
  2. 在后面追加&
  3. 使用md5计算出签名

使用php实现的简单实例如下

<?php
/**
 * client.php
 *
 * Created by PhpStorm.
 * User: yxr
 * Date: 16/12/28
 * Time: 下午4:30
 */

$url = 'http://localhost/server.php';
$params = [
    'name'  => 'yanxr',
    'age'   => 25
];
$appid  = 'client1';
$appkey = '1234567890';
echo request($url, $params, $appid, $appkey, 'POST');

// below are common functions which should be placed in a base class
function request($url, $params, $appid, $appkey, $method='GET')
{
    $params['_appid'] = $appid;
    $params['_ts'] = time();
    $params['_sign'] = sign($params, $appkey);
    $qry_str = http_build_query($params);
    // echo $qry_str . '<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_TIMEOUT, '3');
    if ($method == 'GET') {
        curl_setopt($ch, CURLOPT_URL, $url . '?' . $qry_str);
    } elseif ($method == 'POST') {
        curl_setopt($ch, CURLOPT_URL, $url);
        // Set request method to POST
        curl_setopt($ch, CURLOPT_POST, 1);
        // Set query data here with CURLOPT_POSTFIELDS
        curl_setopt($ch, CURLOPT_POSTFIELDS, $qry_str);
    } else {
        throw new Exception('wrong method parameter');
    }
    $ret = trim(curl_exec($ch));
    curl_close($ch);
    return $ret;
}

function sign($params, $appkey)
{
    ksort($params);
    $str = implode('|', $params);
    return md5($str . '|' . $appkey);
}

<?php

/**
 * server.php
 *
 * Created by PhpStorm.
 * User: yxr
 * Date: 16/12/28
 * Time: 下午4:57
 */

if (!checkSignature($_REQUEST)) {
    die('you sucks');
}
$my_age = 25;
if (isset($_REQUEST['name'])) {
    echo "hello {$_REQUEST['name']} <br>";
    if (isset($_REQUEST['age'])) {
        $not = $_REQUEST['age'] == $my_age? '' : 'not ';
        echo "I am {$not}as old as you";
    }
} else {
    echo 'hello world';
}

function checkSignature($params)
{
    $max_interval = 3; // max time interval not more than 3 seconds
    // appid => appkey
    $app = [
        'client1' => '1234567890',
        'client2' => '1029384756'
    ];
    if (!isset($params['_appid']) || !isset($app[$params['_appid']])) {
        error_log("wrong appid\n", 3, "/tmp/my-errors.log");
        return false;
    }
    if (!isset($params['_ts']) || abs($params['_ts'] - $_SERVER['REQUEST_TIME']) > $max_interval) {
        error_log("wrong ts\n", 3, "/tmp/my-errors.log");
        return false;
    }
    if (!isset($params['_sign'])) {
        error_log("sign not set\n", 3, "/tmp/my-errors.log");
        return false;
    }
    $origin_sign = $params['_sign'];
    unset($params['_sign']);
    ksort($params);
    $str = implode('|', $params);
    $sign = md5($str . '|' .  $app[$params['_appid']]);
    if ($origin_sign != $sign) {
        error_log("wrong signature: 1.check your appkey 2.check your sign generation method\n", 3, "/tmp/my-errors.log");
    }
    return $origin_sign == $sign;
}

哪种方式更合适

推荐第二种方式,因为第一种方式会有坑:

  • 签名基于query string生成,query string有不同的urlencode方式,服务端在生成签名的时候一般不是基于原始的query string,而是去掉sign参数后重新构建新的query string,这就需要保证参数的顺序和urlencode方式与原始的请求一致。上面的实现中$_REQUEST数组的顺序与请求参数一致,并且请求参数是通过application/x-www-form-urlencoded编码,这是form表单的默认方式,也是curl以及php中http_build_query默认用的编码
  • 另一方面,客户端一般使用http类库,不会手动调用http_build_query创建query string,这就需要在生成签名的时候单独调用一次http_build_query,该函数具有urlencode的功能,并不是简单的把所有参数用&拼起来。如果使用的语言没有类似的函数,就只能手动编码了。

qq email facebook github
© 2018 - Xurui Yan. All rights reserved
Built using pelican