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

相关推荐
黑客Ash2 小时前
【D01】网络安全概论
网络·安全·web安全·php
->yjy2 小时前
计算机网络(第一章)
网络·计算机网络·php
阳光帅气男孩4 小时前
PhpSpreadsheet导出图片
php
周全全4 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
Mr.Pascal5 小时前
刚学php序列化/反序列化遇到的坑(攻防世界:Web_php_unserialize)
开发语言·安全·web安全·php
建群新人小猿6 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
黑客Ela9 小时前
网络安全问题概述
安全·web安全·php
Wh1teR0se9 小时前
详解php://filter--理论
web安全·php
李钢蛋14 小时前
PHP函数---function_exists()详解
开发语言·php