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

相关推荐
Lucky小小吴12 小时前
木马查杀篇—Opcode提取
php·opcode·木马查杀
邪恶的贝利亚15 小时前
《ffplay 读线程与解码线程分析:从初始化到 seek 操作,对比视频与音频解码的差异》
ffmpeg·php·音视频
廖圣平16 小时前
美团核销 第三方接口供应商 (含接口文档)
开发语言·数据库·php
sunsineq16 小时前
[超级简单]讲解如何用PHP实现LINE Pay API!
开发语言·php·linepay
新老农16 小时前
php数据导出pdf,然后pdf转图片,再推送钉钉群
pdf·php·钉钉
上海合宙LuatOS16 小时前
全栈工程师实战手册:LuatOS日志系统开发指南!
java·开发语言·单片机·嵌入式硬件·物联网·php·硬件工程
小诸葛的博客20 小时前
Flannel UDP 模式的优缺点
网络协议·udp·php
桃子酱紫君1 天前
华为配置篇-RSTP/MSTP实验
开发语言·华为·php
JPCstorm1 天前
客服系统重构详细计划
php
智慧地球(AI·Earth)1 天前
OpenAI for Countries:全球AI基础设施的“技术基建革命”
开发语言·人工智能·php