PHP 调用 e 签宝接口签名指南

前言

在 401 问题上卡了 一段时间,参考官网文档和鉴权签名计算测试也试了很久,签名确定是没错的,但是一直提示 INVALID_SIGNATURE

其实问题在于我忽略了 公共请求头格式Content-MD5 部分的一句话:

GET 和 DELETE 请求且 Body 体无数据时,此参数可为 ""(空字符串)或不传此参数。

因为参数必选部分他写了 ,我就只关注这个了...害

鉴权签名计算:https://open.esign.cn/tools/signature

下面就快速列出代码了

代码部分

获取 Content-MD5

php 复制代码
/**
 * @param string $body
 *
 * @return string 请求体字符串,如果是文件则需要 md5_file 方法并传入文件名(带路径)
 */
public function getContentMd5(string $body): string {
    return base64_encode(md5($body, true));
}

获取签名

这里我将 App Secret 直接作为形参传入了。因为请求头和 Date 可忽略,这里也直接不作处理。

php 复制代码
/**
 * @param string $method
 * @param string $content_md5
 * @param string $content_type
 * @param string $uri
 * @param string $app_secret
 *
 * @return string
 */
public function getSignature(string $method,
                             string $content_md5, string $content_type, string $uri, string $app_secret): string {
    $string = "$method\n*/*\n$content_md5\n$content_type\n\n$uri";

    return base64_encode(hash_hmac('sha256', $string, $app_secret, true));
}

构建请求头

需注意 X-Tsign-Open-Ca-Timestamp 请求头 必需 传入毫秒级时间戳,也就是 13 位长 ,用 time() 方法获取的是秒级,同样会得到 401 INVALID_TIMESTAMP 的响应

php 复制代码
/**
 * @param string $app_id
 * @param string $app_secret
 * @param string $method
 * @param string $body          这里以 JSON 作为请求体示例,如果涉及到其它类型请求,自行修改一下
 * @param string $content_type
 * @param string $uri
 *
 * @return array
 */
public function buildSignedHeaders(string $app_id,
                                   string $app_secret,
                                   string $method, string $body, string $content_type, string $uri): array {
    $contentMd5 = '';

    if (in_array($method, ['GET', 'DELETE'])) {
        $content_type = '';
    } else {
        $contentMd5 = $this->getContentMd5($body);
    }

    return [
        'Accept' => '*/*',
        'Content-MD5' => $contentMd5,
        'Content-Type' => $content_type,
        'X-Tsign-Open-App-Id' => $app_id,
        'X-Tsign-Open-Auth-Mode' => 'Signature',
        'X-Tsign-Open-Ca-Signature' => $this->getSignature($method, $contentMd5, $content_type, $uri, $app_secret),
        'X-Tsign-Open-Ca-Timestamp' => Carbon::now()->getTimestampMs()
    ];
}

如果没有 Carbon 库,可以用官方 Demo 的写法:

php 复制代码
/**
 * @return float 返回值是个 double
 */
public function getMillisecond(): float {
    [$t1, $t2] = explode(' ', microtime());

    return (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000);
}

发送请求

php 复制代码
/**
 * @param string      $method
 * @param string      $uri
 * @param array       $data
 * @param string|null $content_type
 * @param bool        $sandbox
 *
 * @return array|null
 */
public function request(string  $method, string $uri, array $data = [],
                        ?string $content_type = 'application/json', bool $sandbox = false): ?array {
    $method = strtoupper($method); // 统一转换为大写

    $body = '';

    if ($method === 'POST') {
        if (str_starts_with($content_type, 'application/json')) {
            $body = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        } else {
            // TODO: 其它类型的接口自行处理
        }
    }

    // 此处通过 .env 文件取配置
    $host = env('ESIGN_HOST'); // https://openapi.esign.cn
    $appId = env('ESIGN_APPID');
    $appSecret = env('ESIGN_APPSECRET');

    // 可以根据参数进入沙盒环境方便调试
    if ($sandbox) {
        $host = env('ESIGN_SANDBOX_HOST'); // https://smlopenapi.esign.cn
        $appId = env('ESIGN_SANDBOX_APPID');
        $appSecret = env('ESIGN_SANDBOX_APPSECRET');
    }

    $headers = $this->buildSignedHeaders($appId, $appSecret, $method, $body, $content_type, $uri);

    // 这里使用了 Laravel/Lumen 9+ 的内置 Http Facade,实际也是调用 GuzzleHttp 客户端,如果直接使用 Guzzle 的 Client,send 换成 request 即可
    // 可能有人会问为什么不直接用定义好的 asJson 和 post 方法,因为...他带上了 UA,得重新处理签名部分,我懒
    $response = Http::send($method, $host . $uri, [
        'headers' => $headers,
        'body' => $body // 示例仅针对 JSON 请求,其它接口需调整
    ])->body();

    $response = json_decode($response, true);

    if (json_last_error() === JSON_ERROR_NONE) {
        return $response;
    }

    return null;
}

调用

假设类名为 ESignService

php 复制代码
// 所以 Carbon 这个库真的方便 ;)
$from= Carbon::createFromDate(2023, 12, 1)->startOfDay()->getTimestampMs();
$to = Carbon::createFromDate(2023, 12, 31)->endOfDay()->getTimestampMs();

// 此处为查询集成方企业流程列表接口
var_dump((new ESignService)->request('POST', '/v3/organizations/sign-flow-list', [
    'pageNum' => 1,
    'pageSize' => 10,
    'signFlowStartTimeFrom' => $from, // 对于这个接口,signFlowStartTimeFrom 和 signFlowStartTimeTo 是必传的,文档上必选为否又误导了
    'signFlowStartTimeTo' => $to
]));

注意: 部分接口形式带有资源路由参数,如 /v3/sign-flow/{signFlowId}/preview-file-download-url,上方代码仅供参考,中间的 signFlowId 需根据实际业务调整

相关推荐
两个人的幸福2 小时前
Windows 桌面应用自研 PHP 队列(下):完整代码与六大工程化优化
php
BingoGo2 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack2 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户3074596982073 天前
PHP 扩展——从入门到理解
php
鹏仔先生4 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
云水一下4 天前
从零开始学 PHP 系列(一):PHP 的前世今生与开发环境搭建
开发语言·php
xingpanvip4 天前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
酉鬼女又兒4 天前
零基础入门计算机网络运输层:端到端通信核心作用、端口号分类规则、复用分用工作机制及UDP与TCP协议全方位对比详解
网络·网络协议·tcp/ip·计算机网络·考研·udp·php
dog2504 天前
不要再继续优化 TCP
网络协议·tcp/ip·php
Channing Lewis4 天前
PHP 解析 Excel 的那些坑:一次“行号错位”引发的数据丢失
开发语言·php·excel