*复盘工作中遇到的问题并整理出知识点。
需求
维护并升级一个历史悠久的PHP项目。该项目要求我在数据安全与通信完整性方面,实施严格的加密与签名机制。具体而言,需要使用RSA加解密与外部进行校验以保障敏感数据的传输安全,同时,为了验证数据的完整性和来源的真实性,还要使用hmacSHA256签名技术,避免在近源攻击上留下隐患。
尽管这些接口在业务逻辑上各有千秋,但它们在处理加密与签名流程时却展现出了惊人的共性:变化的仅仅是请求的uri和消息体内容,而加密与签名的核心逻辑则保持了一致性和可复用性,因此复盘整理出下文中API类来完成对项目接口的调用。
*为了安全考虑,对部分内容省略以及脱敏。这意味着你需要对代码进行适当修改然后才可以跑通
实现
- 开启php.ini中的扩展
extension=curl
- 配置http请求类
/** * 科技 API */ class API{ /** * 客户端id * 用于获取appKey、appSecret、客户端公钥、服务端私钥等 * @var string */ private static $ClientUuid = "1234a567-b89c-12d3-45e6-fa789b0cde1"; /** * 发送请求处理 * 业务 * @param string $host 示例:http://127.0.0.1:3000 * @param string $uri 示例:/api/interface/method * @param mixed $post_data 消息体对象 * @param boolean $debug 测试使用 * @return mixed */ public static function httpRequest($host, $uri, $post_data, $debug = false) { $ch = curl_init(); // 初始化cURL对话 curl_setopt($ch, CURLOPT_URL, $host.$uri); // 设置请求地址 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 获取的信息以文件流的形式返回 $headers = [ // 设置请求头 'key' => Query::getAppKey(self::$ClientUuid), // 这里放你的appKey 'nounce' => Libs::createUuid(), // 为请求头设置一个随机值 'signMethod' => 'HmacSHA256', // 签名方法 'time' => strval(self::timenow()) // 这里是13位数的时间戳字符串 ]; // 对敏感信息进行 RSA 加密 if(isset($post_data['phone'])) { // 如果存在电话 $post_data['phone'] = RSA::publicEncrypt($post_data['phone']); } ksort($post_data); // 对一级键值对进行严格排序 // 设置签名数据 $e = $headers; $e['method'] = "POST"; $e['uri'] = $uri; $e['data'] = $post_data; ksort($e); // 严格排序 // 签名 $temp = self::hmacSHA256($e, $post_data); // 代码省略,此处对数据进行签名 $headers['sign'] = $temp['sign']; // 然后将结果放在请求头签名结果里 if($debug) echo "签名字符串:<br>".$temp['encode']; // 可以人工校验签名串 // 请求头转义,并设置cURL请求头 $key = array_keys($headers); $value = array_values($headers); $r = [ 'Content-Type: application/json' // 这是发送json请求必备的 ]; for($i = 0; $i < count($key); $i++){ array_push($r, $key[$i].":".$value[$i]); } if($debug) echo "请求头:<br>".json_encode($r, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)."<br>"; // 可以人工校验请求头 // 设置请求头 curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_HTTPHEADER, $r); // SSL验证 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // POST请求 curl_setopt($ch, CURLOPT_POST, 1); // 请求消息体 $body = $temp['json']; // 这里可以理解为 请求内容转成json格式 if($debug) echo "请求体:<br>".$body."<br>"; curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // if($debug) return; /* 测试 */ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 发送请求 $file_contents = curl_exec($ch); // 获取状态码 $httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE); // 结束请求 curl_close($ch); $response = json_decode($file_contents, true); // 输出结果 echo "响应体:<br>".json_encode($response, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)."<br>"); // 返回响应体 return $response; } /** * 获取当前时间戳,毫秒 */ public static function timenow() { list($s, $ms) = explode(" ", microtime()); return (float)sprintf("%.0f", (floatval($s) + floatval($ms))*1000); } }
- 开始调用接口
// 获取班级为242001的男同学的个人信息 $res = API::httpRequest('http://8.134.251.42:8889', '/api/v1/getStudentInfo', [ 'class' => '242001', 'gender' => 1 ]);
复盘分析
使用cURL发送json-post请求没有一次走通的原因有:
- 项目原本没有设置
content-type: application/json
,与外部对接会发现签名对不上,在php项目中需要为其加上 - 关于json_encode空对象转为数组的情况
// 如果消息体为空,使用json_encode会导致转换时被当成数组 $data = json_encode([]); // [] // 如果要强制转为对象,可以这样 $data = json_encode([], JSON_FORCE_OBJECT); // {} // 这个时候如果有非空对象里面有一个空的子对象,需要记得进行特殊处理
- 关于json_encode处理斜杠和中文转义的问题,可以这么解决
// 斜杠不进行"\"的转义 JSON_UNESCAPED_SLASHES // 中文不进行"\u3455"的转义 JSON_UNESCAPED_UNICODE $res = json_encode('世界/你好', JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);