「PHP开发APP接口实战008」日常安全防范之签名验证

前言

互联网很危险!站点安全需要在网站设计和使用的各个方面保持警惕。

常见的站点安全威胁有
  • SQL注入
  • 跨站脚本 (XSS)攻击
  • 跨站请求伪造 (CSRF)攻击
  • 恶意访问请求(DDos)攻击

由于时间和篇幅的关系,这里就不对这些攻击手段的理论部分展开来讲了。以下是参考资料,仅供大家参考学习:
站点安全
Web开发安全防范注意
web开发安全防范十二篇

透过现在看本质,我们可以总结出,常见的攻击手段有:
  1. 通过篡改请求参数,伪造SQL或HTML脚本,以达到SQL注入或XSS攻击的目的
  2. 通过拦截或伪造会话(seesion, cookie),欺骗服务器,以达到窃取数据的目的。
  3. 通过大量恶意访问正常请求地址,以达到阻塞网络带宽,消耗网站资源的目的。
解决方案
  1. 防止篡改请求参数方法有:
  • 给请求参数加上签名验证,篡改后的参数将通不过签名验证。
  • 过滤或转义请求参数中非法字符
  • 对请求参数进行严格验证
  • 使用 Phalcon 方法操作数据库,SQL需要的参数通过 bind 方法传递,不直接执行自己写的SQL语句
  1. 防止伪造会话
    介于接口项目性质,我们并没有使用(seesion, cookie)等会话操作,所以,不存在伪造会话类攻击。但需要注意的是,我们需要使用的是 token 进行身份验证,不要直接暴露用户ID,甚至直接使用请求参数中的用户ID来进行身份验证。

  2. 防止恶意访问
    在请求中增加一个 transaction_id 参数。同一个 transaction_id 只能使用一次,用完即失效。这样就很好的防止了攻击者使用正常的请求地址重复访问接口的风险。

实操

接下来我将一一讲解怎么实现上面的解决方案

一. 签名验证
  1. 在配置文件 /app/config/config.php 中, 代码 'debug' => 1, // 是否开启调试 行,下面添加以下签名配置代码:
    'sign' => [
        'enable' => 1, // 开启验签
        'secret_level' => Sign::NORMAL,   // 验签等级:NORMAL 只对POST参数进行验证, HIGHT 对所有请求参数进行验证
        'secret_key' => 'PJ6TmmQE',   // 签名密钥,可随意配置,调用接口时线下约定
        // 验签排除页面
        'exclude' => [
            'test/index',
        ]
    ],

配置参数说明:

  1. enable, 是否开启验签功能: 0 是关闭,1 是开启。开发时,为了方便测试,可将其关闭。
  2. secret_level, 验签等级:
    • Sign::NORMAL 普通签名安全等级,只对POST参数进行签名验证
    • Sign::HIGHT 高级签名安全等级,对所有参数(GET & POST)进行签名验证
  3. secret_key 签名密钥,可随意配置。调用接口时线下约定, 与服务器配置保持一致即可
  4. exclude 无需验签接口。 规则:[ControllerName]/[ActionName]有一些接口不需要签名验证,在此配置即可。
  1. /app/library/ 目录下创建 Sign.php 文件并添加以下代码:
get('sign.secret_key');

        // 生成 URL-encode 之后的请求字符串
        $str = http_build_query($params);

        // 将请求字符串使用MD5加密后,再转换成小写,并返回
        return strtoupper(MD5($str));
    }

    /**
     * 验证签名
     * @param $sign
     * @param $params
     * @return bool
     */
    public function verify($sign, $params)
    {
        return $sign == $this->generate($params);
    }
}

定义签名等级常量:Sign::NORMALSign::MEDIUMSign::HIGHT

签名规则(生成函数 generate()):

  • 第1步,删除参数组中所有等值为FALSE的参数(包括:NULL, 空字符串,0, false)
  • 第2步,按照键名对参数数组进行升序排序
  • 第3步,给参数数组追加密钥,键名为 key, 值为签名配置中配置的 secret_key 的值
  • 第4步,将参数数组转换成 URL-encode 的请求字符串。e.g.: foo=bar&baz=boom&cow=milk&php=hypertext+processor
  • 第5步,将请求字符串使用MD5加密后,再转换成小写
  1. /app/controllers/BaseController.php 中添加签名验证函数 signVerify()
    /**
     * 签名验证
     */
    private function signVerify()
    {
        //未开启签名验证,跳过
        if (!Config::instance()->get('sign.enable')) {
            return true;
        }

        // 过滤无需验证页面
        if (in_array($this->_current_page, Config::instance()->get('sign.exclude'))) {
            return true;
        }

        // 判断验签等级,初始化验签参数
        if (Config::instance()->get('sign.secret_level') > Sign::NORMAL) {
            $params = $this->request->get();
            unset($params['_url']);
        } else {
            $params = $this->request->getPost();
        }

        // 无参数, 跳过验签
        if (count($params) == 0) {
            return true;
        }

        // 验证是否提交签名
        $sign = $this->request->getHeader('sign');
        if (!$sign) {
            Output::instance($this->response)->fail('缺少签名');
        }

        // 验证签名是否有效
        if (!Sign::instance()->verify($sign, $params)) {
            Output::instance($this->response)->fail('无效签名');
        }
    }

需要注意的是,客户端需要在 Header 中提交签名参数 sign

  1. BaseController 中,定义私有变量$_current_page, 用于存储当前接口名。并在 initialize() 函数中添加以下代码:
        // 获取当前接口(页面)名
        $this->_current_page = $this->dispatcher->getControllerName() . '/' . $this->dispatcher->getActionName();

        // 签名验证
        $this->signVerify();
  1. 当前接口(页面)名
  2. 调用签名验证函数

你可能感兴趣的:(「PHP开发APP接口实战008」日常安全防范之签名验证)