「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 文件并添加以下代码:
<?php

/**
 * 签名认证
 *
 * @author Hu Feng
 */
class Sign
{

    /**
     * 普通签名安全等级,只对POST参数进行签名验证
     */
    const NORMAL = 1;

    /**
     * 高级签名安全等级,对所有参数(GET & POST)进行签名验证
     */
    const HIGHT = 2;


    public static $instance;

    private function __construct()
    {
    }

    public static function instance()
    {
        if (!self::$instance) self::$instance = new self();
        return self::$instance;
    }

    /**
     * 生成签名
     * @param array $params
     * @return string
     */
    public function generate(Array $params)
    {
        // 将删除参数组中所有等值为FALSE的参数(包括:NULL, 空字符串,0, false)
        $params = array_filter($params);

        // 按照键名对参数数组进行升序排序
        ksort($params);

        // 给参数数组追加密钥,键名为 key, 值为签名配置中配置的 secret_key 的值
        $params['key'] = Config::instance()->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. 调用签名验证函数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容