PHP项目电子签章集成方案

wen PHP项目 3

PHP项目电子签章集成方案

技术选型与架构设计

1 常见电子签章平台对比

平台 优点 缺点 适用场景
e签宝 接口完善,PHP SDK支持好 费用较高 企业级应用
法大大 合规性好,本地签章支持 接入门槛较高 合同类业务
契约锁 加密技术强,自定义程度高 文档较少 金融、政务
自建方案 完全可控,无额外费用 开发成本高 内部管理系统
OpenSSL + TCPDF 免费开源 法律效力需评估 非强制场合

2 架构建议

前端(Vue/React) → 后端PHP(Laravel/ThinkPHP) → 电子签章服务(第三方API/自建)
                                                        ↓
                                                  签名服务器/RSA密钥存储

主流第三方平台集成方案

1 e签宝集成(推荐)

步骤1: 安装SDK

PHP项目电子签章集成方案

composer require esign/esign-php-sdk

步骤2: 配置参数

// config/esign.php
return [
    'app_id' => 'your_app_id',
    'secret' => 'your_app_secret',
    'api_url' => 'https://open.esign.cn', // 正式环境
    'notify_url' => 'https://yourdomain.com/api/esign/notify'
];

步骤3: 创建签署流程

use Esign\EsignClient;
class SignService
{
    private $client;
    public function __construct()
    {
        $this->client = new EsignClient(
            config('esign.app_id'),
            config('esign.secret'),
            config('esign.api_url')
        );
    }
    /**
     * 创建合同并发起签署
     */
    public function createContract(array $signers, string $filePath)
    {
        // 1. 上传文件
        $fileId = $this->uploadFile($filePath);
        // 2. 创建签署流程
        $flowId = $this->createSignFlow($fileId, $signers);
        // 3. 设置签署位置
        $this->setSignPositions($flowId, $signers);
        // 4. 发起签署
        return $this->startSign($flowId);
    }
    private function uploadFile(string $path): string
    {
        $response = $this->client->uploadFile([
            'file' => new \CURLFile(realpath($path))
        ]);
        return $response['data']['fileId'];
    }
    private function createSignFlow(string $fileId, array $signers): string
    {
        $data = [
            'name' => '合同签署-' . date('YmdHis'),
            'docs' => [['fileId' => $fileId]],
            'signers' => [],
            'noticeConfig' => [
                'noticeTypes' => ['SMS', 'EMAIL']
            ]
        ];
        foreach ($signers as $index => $signer) {
            $data['signers'][] = [
                'signerAccount' => [
                    'thirdPartyUserId' => $signer['user_id'],
                    'name' => $signer['name'],
                    'idType' => 'CRED_PSN_CH_IDCARD',
                    'idNumber' => $signer['id_card']
                ],
                'signOrder' => $index + 1
            ];
        }
        $response = $this->client->createSignFlow($data);
        return $response['data']['flowId'];
    }
    private function setSignPositions(string $flowId, array $signers)
    {
        foreach ($signers as $index => $signer) {
            $this->client->addSignFieldPosition($flowId, [
                'signerAccountId' => $signer['user_id'],
                'signfields' => [
                    [
                        'type' => 0, // 0-手写签名,1-印章
                        'posPage' => 1,
                        'posX' => $signer['pos_x'] ?? 200,
                        'posY' => $signer['pos_y'] ?? 300
                    ]
                ]
            ]);
        }
    }
    private function startSign(string $flowId)
    {
        return $this->client->startSignFlow($flowId);
    }
    /**
     * 处理回调通知
     */
    public function handleNotify(array $params)
    {
        // 验签
        if (!$this->verifyNotify($params)) {
            throw new \Exception('Invalid signature');
        }
        $flowId = $params['flowId'];
        $status = $params['signStatus']; // 2-签署完成
        if ($status == 2) {
            // 下载已签署文件
            $pdfContent = $this->downloadSignedFile($flowId);
            // 保存到本地
            Storage::put("contracts/{$flowId}.pdf", $pdfContent);
        }
        return response()->json(['code' => 0]);
    }
    private function downloadSignedFile(string $flowId): string
    {
        $response = $this->client->downloadSignedFile($flowId);
        return $response['data']['content']; // base64格式
    }
    private function verifyNotify(array $params): bool
    {
        $signature = $params['signature'] ?? '';
        $data = $params['data'] ?? '';
        return $this->client->verifySignature($data, $signature);
    }
}

2 法大大集成

// 使用法大大PHP SDK
use Fadada\SignClient;
$client = new SignClient([
    'app_id' => 'your_app_id',
    'secret' => 'your_secret',
    'version' => 'v2'
]);
// 1. 实名认证
$authResult = $client->personalAuth([
    'name' => '张三',
    'id_card' => '110101199001011234',
    'mobile' => '13800138000'
]);
// 2. 创建合同
$contractId = $client->createContract([ => '合作协议',
    'template_id' => 'template_001',
    'params' => [
        'party_a' => '公司A',
        'party_b' => '公司B',
        'amount' => 10000
    ]
]);
// 3. 获取签署链接
$signUrl = $client->getSignUrl([
    'contract_id' => $contractId,
    'user_id' => $authResult['user_id'],
    'redirect_url' => 'https://yourdomain.com/sign/success'
]);
// 返回给前端跳转
return redirect($signUrl);

自建电子签章方案

1 基于OpenSSL + TCPDF

技术栈:

  • PHP 7.4+
  • OpenSSL扩展
  • TCPDF库
  • 签章图片(公章、手写签名)

步骤1: 生成CA证书链

# 生成根证书
openssl req -x509 -newkey rsa:2048 -keyout rootCA.key -out rootCA.crt -days 3650
# 生成员工证书
openssl req -new -newkey rsa:2048 -keyout user.key -out user.csr
openssl x509 -req -in user.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out user.crt -days 365

步骤2: PHP签名实现

class CustomSignService
{
    private $certPath;
    private $keyPath;
    private $signImagePath;
    public function __construct()
    {
        $this->certPath = storage_path('certs/user.crt');
        $this->keyPath = storage_path('certs/user.key');
        $this->signImagePath = storage_path('signatures/seal.png');
    }
    /**
     * 给PDF添加数字签名
     */
    public function signPdf(string $inputPdf, string $outputPdf, array $position = [100, 200])
    {
        // 1. 加载PDF
        $pdf = new TCPDF();
        $pdf->setSourceFile($inputPdf);
        // 2. 添加签章图片
        $pdf->Image(
            $this->signImagePath,
            $position[0],
            $position[1],
            50,  // 宽度
            50,  // 高度
            'PNG'
        );
        // 3. 计算文档哈希
        $content = file_get_contents($inputPdf);
        $hash = hash('sha256', $content);
        // 4. 生成数字签名
        $signature = $this->generateSignature($hash);
        // 5. 嵌入签名信息到PDF元数据
        $pdf->setSignature(
            $this->certPath,
            $this->keyPath,
            $signature,
            'Signing by: User1, Date: ' . date('Y-m-d H:i:s'),
            2, // 签名可见度
            [] // 额外选项
        );
        // 6. 输出PDF
        $pdf->Output($outputPdf, 'F');
        return $outputPdf;
    }
    private function generateSignature(string $hash): string
    {
        $privateKey = openssl_pkey_get_private(file_get_contents($this->keyPath));
        openssl_sign($hash, $signature, $privateKey, OPENSSL_ALGO_SHA256);
        return base64_encode($signature);
    }
    /**
     * 验证签名
     */
    public function verifySignature(string $pdfPath): bool
    {
        $pdf = new TCPDF();
        $pdfData = $pdf->parsePdf($pdfPath);
        $storedSignature = $pdfData['signature'] ?? '';
        $storedHash = $pdfData['hash'] ?? '';
        // 重新计算文档哈希
        $content = file_get_contents($pdfPath);
        $computedHash = hash('sha256', $content);
        // 验证签名
        $publicKey = openssl_pkey_get_public(file_get_contents($this->certPath));
        $verified = openssl_verify(
            $storedHash,
            base64_decode($storedSignature),
            $publicKey,
            OPENSSL_ALGO_SHA256
        );
        return $verified && ($storedHash === $computedHash);
    }
}

2 手写签名采集方案

前端实现(HTML5 Canvas):

<canvas id="signaturePad" width="400" height="200"></canvas>
<button onclick="saveSignature()">保存签名</button>
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.0.0/dist/signature_pad.umd.min.js"></script>
<script>
    const canvas = document.getElementById('signaturePad');
    const signaturePad = new SignaturePad(canvas);
    function saveSignature() {
        if (signaturePad.isEmpty()) {
            alert('请先签名');
            return;
        }
        const dataURL = signaturePad.toDataURL('image/png');
        // 发送到后端
        fetch('/api/save-signature', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
            },
            body: JSON.stringify({
                signature: dataURL,
                user_id: 'current_user_id'
            })
        }).then(response => response.json())
          .then(data => {
              alert('签名保存成功');
          });
    }
</script>

后端保存:

public function saveSignature(Request $request)
{
    $signatureData = $request->input('signature');
    // 去除base64前缀 data:image/png;base64,
    $signatureData = preg_replace('/^data:image\/\w+;base64,/', '', $signatureData);
    $signatureData = base64_decode($signatureData);
    $filename = 'signatures/' . $request->user()->id . '_' . time() . '.png';
    Storage::put($filename, $signatureData);
    // 保存到数据库
    UserSignature::updateOrCreate(
        ['user_id' => $request->user()->id],
        ['signature_path' => $filename]
    );
    return response()->json(['success' => true]);
}

数据库表设计

-- 签章记录表
CREATE TABLE `sign_records` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `contract_id` varchar(64) NOT NULL COMMENT '合同编号',
  `signer_id` bigint unsigned NOT NULL COMMENT '签署人',
  `sign_type` tinyint NOT NULL DEFAULT '1' COMMENT '1-平台签 2-自建签',
  `sign_status` tinyint NOT NULL DEFAULT '0' COMMENT '0-待签 1-已签 2-拒签',
  `signed_at` timestamp NULL DEFAULT NULL COMMENT '签署时间',
  `signature_path` varchar(255) DEFAULT NULL COMMENT '签章文件路径',
  `ip_address` varchar(45) DEFAULT NULL COMMENT '签署IP',
  `device_info` varchar(255) DEFAULT NULL COMMENT '设备信息',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_contract_id` (`contract_id`),
  KEY `idx_signer_id` (`signer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 签章配置表
CREATE TABLE `sign_configs` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint unsigned NOT NULL,
  `provider` varchar(32) NOT NULL DEFAULT 'esign' COMMENT '平台类型',
  `account_id` varchar(128) DEFAULT NULL COMMENT '平台账户ID',
  `seal_data` text COMMENT '印章数据(生物特征)',
  `cert_serial` varchar(128) DEFAULT NULL COMMENT '证书序列号',
  `is_active` tinyint NOT NULL DEFAULT '1',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_provider` (`user_id`, `provider`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

安全最佳实践

1 密钥管理

// 使用Laravel的加密服务
use Illuminate\Support\Facades\Crypt;
// 存储私钥前加密
$encryptedKey = Crypt::encryptString($privateKeyContent);
// 使用时解密
$privateKey = Crypt::decryptString($encryptedKey);

2 防篡改机制

class SignVerifyMiddleware
{
    public function handle($request, \Closure $next)
    {
        // 验证请求完整性
        $signature = $request->header('X-Signature');
        $timestamp = $request->header('X-Timestamp');
        $nonce = $request->header('X-Nonce');
        $params = $request->all();
        ksort($params);
        $queryString = http_build_query($params);
        $toSign = "{$timestamp}{$nonce}{$queryString}";
        $expectedSignature = hash_hmac('sha256', $toSign, config('app.api_secret'));
        if (!hash_equals($expectedSignature, $signature)) {
            return response()->json(['error' => 'Invalid signature'], 403);
        }
        return $next($request);
    }
}

3 日志审计

// 记录所有签名操作
\Log::channel('signature')->info('Sign contract', [
    'user_id' => auth()->id(),
    'contract_id' => $contractId,
    'ip' => request()->ip(),
    'user_agent' => request()->userAgent(),
    'action' => 'create_sign_flow',
    'result' => $result
]);

性能优化建议

  1. PDF预处理:使用 Ghostscript 压缩PDF文件
  2. 异步处理:签名完成后通过队列任务下载文件
  3. 缓存方案:缓存公钥信息和证书链
  4. CDN加速:静态签章图片使用CDN
  5. 负载均衡:签名服务独立部署

合规性注意事项

要求 实现方式
实名认证 对接公安/银联实名接口
意愿认证 短信验证码+人脸识别
时间戳服务 对接国家授时中心
证据链保存 记录完整操作日志+IP+设备指纹
加密存储 敏感字段AES-256加密

测试环境搭建

# 使用Docker搭建测试环境
docker run -d --name esign-test \
  -p 8080:80 \
  -e ESIGN_MODE=test \
  esign/esign-server:latest
# PHP单元测试示例
php artisan test tests/Unit/SignServiceTest

选择建议:

  • 初创公司:优先使用e签宝/法大大等成熟平台
  • 中型企业:可以采用混合方案(核心业务用第三方,次要业务自建)
  • 大型企业:自建方案 + 脱敏测试环境

如果需要具体平台的SDK示例或开发过程中遇到问题,可以提供更多细节。

抱歉,评论功能暂时关闭!