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 需根据实际业务调整

相关推荐
用户962377954482 小时前
CTF 伪协议
php
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理6 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082856 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php