# 第二套接口验签方案

以下是第二套接口签名算法的介绍，如果项目不适合使用第一套access_token的验证方式，或者有其他需要可选择第二套接口验签方案。 

## 动态签名方案介绍

access_token的验证方式，是客户端先凭借明文的app_key和app_secret密钥，申请获得access_token的验证方式，然后就可以使用分配的access_token令牌调用其他接口。  

其缺点在于第一次授权时容易泄露app_key和app_secret密钥，并且access_token泄露后容易被第三方使用。此方案不适合客户端使用，因此容易造成被抓包。但好处在于，调用方只需要申请一次令牌，就可以方便调用其他接口。  

而动态签名方案，则是每一次调用接口，都要根据既定的加密方案进行签名的验证。一旦任何一个参数发生变化，那么就需要重新生成接口签名。这意味着，客户端在服务端进行接口通信时安全性相对更高。缺点是：客户端每次都要按算法生成签名，提高了API接入成本，同时相同的接口链接参数是一样的，可以对相同的接口链接重复调用。此时可以在服务端进行业务层的限制，例如每个用户每天只能投5票，或者追加当前时间戳的误差核对。  

小结一下，动态签名方案特点： 
 + 每次都要根据参数生成动态签名
 + 不需要对外暴露app_secret密钥  

## 如何开启第二套接口验签方案

注意，PhalApi Pro 专业版，默认提供的是access_token令牌验签方案。这套方案已经被Platform开放平台和Admin管理后台使用，因此默认的第一套接口验签方案不能被关掉。  

> 注意：需要保留第一套access_token令牌验签方案。  

在保留原来默认第一套接口验签方案的同时，你可以开启第二套接口验签方案。并且，第二套接口验签方案，仅适合用于对外提供的开放接口，包括App命名空间下的全部接口，以及自定义添加的新的接口命名空间，但不包括Admin、Platform。  

下面将来介绍如何开启第二套接口验签方案。  

## 第1步：切换filter服务
打开 ./public/api/app.php这个入口文件，添加以下新代码，目的是为了重新注册fitler服务。  
```php
require_once dirname(__FILE__) . '/../init.php';

// 如果你需要使用第二套加密算法，请开启以下服务
$di->filter = new \App\Common\SignFilter();
```

这样，就可以针对./public/api/app.php这个入口的接口，对应App接口命名空间和自定义的接口命名空间的接口，切换到第二套接口验签方案。  

## 第2步：配置默认接口参数
打开./config/app.php配置文件，将原来的：
```php
    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' => array(
        'accessToken' => array('name' => 'access_token', 'default' => '', 'desc' => '访问令牌，仅当开启签名验证时需要传递，生成令牌可使用App.Auth.ApplyToken接口'),

        /** ----- 如果你需要使用第二套加密算法，请开启以下参数规则 ----- **/

        // 'app_key' => array('name' => 'app_key', 'default' => '', 'desc' => 'app_key，用于区分客户端应用，首次接入需要创建应用并等待管理员审核通过'),
        // 'sign' => array('name' => 'sign', 'desc' => '动态签名，签名算法是：<br/><ul><li>1、全部参数（排除sign），按key进行字典排序</li><li>2、全部参数值，把原始值按字符串进行拼接，并在最后加上app_secret密钥</li><li>3、对第2步结果，拼接密钥后，进行MD5加密</li><li>4、对第3步结果，转成大写，得到sign签名</li></ul>'),
        // 'uid' => array('name' => 'uid', 'type' => 'int', 'default' => 0, 'desc' => ''),
        // 'accessToken' => array('name' => 'access_token', 'default' => '', 'desc' => '访问令牌，保留使用但不需要在文档上展示', 'is_doc_hide' => true),
     ),
```
改成：
```php
    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' => array(
        // 'accessToken' => array('name' => 'access_token', 'default' => '', 'desc' => '访问令牌，仅当开启签名验证时需要传递，生成令牌可使用App.Auth.ApplyToken接口'),

        /** ----- 如果你需要使用第二套加密算法，请开启以下参数规则 ----- **/

        'app_key' => array('name' => 'app_key', 'default' => '', 'desc' => 'app_key，用于区分客户端应用，首次接入需要创建应用并等待管理员审核通过'),
        'sign' => array('name' => 'sign', 'desc' => '动态签名，签名算法是：<br/><ul><li>1、全部参数（排除sign），按key进行字典排序</li><li>2、全部参数值，把原始值按字符串进行拼接，并在最后加上app_secret密钥</li><li>3、对第2步结果，拼接密钥后，进行MD5加密</li><li>4、对第3步结果，转成大写，得到sign签名</li></ul>'),
        'uid' => array('name' => 'uid', 'type' => 'int', 'default' => 0, 'desc' => ''),
        'accessToken' => array('name' => 'access_token', 'default' => '', 'desc' => '访问令牌，保留使用但不需要在文档上展示', 'is_doc_hide' => true),
     ),
```

注意，原来的access_token参数还是保留，但进行了隐藏，避免在线文档上显示出来，误导开发者。  

保存后，刷新在线接口文档，可以看到以下3个新增加的公共接口参数。  

![](http://cdn7.okayapi.com/yesyesapi_20200429235914_ebeca5a4bc364381e94c0c28386c0442.png)  

> 温馨提示：熟悉后，你可根据项目的需要调整公共参数的规则，例如是否必须。  

## 第3步：测试调用接口

完成上面两步，就可以实现第二套接口验签方案的开启了。  

接下来，你可以进行接口的测试，验证新的加密方案是否已生效。  

例如，故意填写错误的签名sign参数。  
![](http://cdn7.okayapi.com/yesyesapi_20200429234458_4043caf12098932748e5f278d5f8ec8f.png)  

会如期得到“签名错误”的返回结果。  

```
{
    "ret": 400,
    "data": {},
    "msg": "非法请求：签名错误"
}
```

## 如何查看正确的sign签名？
你可以通过查看日志来查看正确的签名是什么。  

查看当天的运行日记，例如： 
```bash
$ tailf ./runtime/log/202004/20200429.log 

2020-04-29 23:44:29');|{"request":{"service":"App.HelloWorld.HiRights","need_sign":"dMYpWZkvC6U40FbnIM6eGr","sign":"XXXX","uid":"0"}}
```
从上面日志可以看到需要的正确签名应该是：```dMYpWZkvC6U40FbnIM6eGr```，但客户端实际提供的签名是：```XXXX```。  

## 如何临时去掉签名验证？  

在调试模式下，可以去掉签名验证，并且可以通过app_key和uid参数手动设置当前的应用和顾客ID。  

## 动态签名算法及示例

以下是动态签名的算法，也是一种很主流很流行的签名方式。  

动态签名，签名算法是：

 + 1、全部参数（排除sign），按key进行字典排序
 + 2、全部参数值，把原始值按字符串进行拼接，并在最后加上app_secret密钥
 + 3、对第2步结果，拼接密钥后，进行MD5加密
 + 4、对第3步结果，转成大写，得到sign签名

以下是一个示例。  

假设app_key是dMYpWZkvC6U40FbnIM6eGr，同时app_secret密钥是Eb8LgJGSA2juKjmND6R3XuHdqe3n5xEEjPx。  

待请求的API接口链接是：  
```
http://你的域名/api/app.php?service=App.HelloWorld.HiApp&app_key=dMYpWZkvC6U40FbnIM6eGr&uid=1
```

那么：  


 + 1、全部参数（排除sign），按key进行字典排序
 得到：
```
app_key=dMYpWZkvC6U40FbnIM6eGr
service=App.HelloWorld.HiApp
uid=1
```
 + 2、全部参数值，把原始值按字符串进行拼接
 得到：
```
dMYpWZkvC6U40FbnIM6eGrApp.HelloWorld.HiApp1Eb8LgJGSA2juKjmND6R3XuHdqe3n5xEEjPx
```
 + 3、对第2步结果，拼接密钥后，进行MD5加密
 得到：
```
175113721BD679F455D15F96CB2CDCA0
```

 + 4、对第3步结果，转成大写，得到sign签名（32位大写）
 得到：  
```
175113721BD679F455D15F96CB2CDCA0
```

最后，得到sign=175113721BD679F455D15F96CB2CDCA0，从而得到完整的接口请求链接是：  
```
http://你的域名/api/app.php?service=App.HelloWorld.HiApp&app_key=dMYpWZkvC6U40FbnIM6eGr&uid=1&sign=175113721BD679F455D15F96CB2CDCA0
```

请求后，得到结果（接口签名通过）：  
```
{
    "ret": 200,
    "data": {
        "content": "Hello app: dMYpWZkvC6U40FbnIM6eGr"
    },
    "msg": ""
}
```
