PHP项目电子签章集成方案
技术选型与架构设计
1 常见电子签章平台对比
| 平台 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| e签宝 | 接口完善,PHP SDK支持好 | 费用较高 | 企业级应用 |
| 法大大 | 合规性好,本地签章支持 | 接入门槛较高 | 合同类业务 |
| 契约锁 | 加密技术强,自定义程度高 | 文档较少 | 金融、政务 |
| 自建方案 | 完全可控,无额外费用 | 开发成本高 | 内部管理系统 |
| OpenSSL + TCPDF | 免费开源 | 法律效力需评估 | 非强制场合 |
2 架构建议
前端(Vue/React) → 后端PHP(Laravel/ThinkPHP) → 电子签章服务(第三方API/自建)
↓
签名服务器/RSA密钥存储
主流第三方平台集成方案
1 e签宝集成(推荐)
步骤1: 安装SDK

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
]);
性能优化建议
- PDF预处理:使用
Ghostscript压缩PDF文件 - 异步处理:签名完成后通过队列任务下载文件
- 缓存方案:缓存公钥信息和证书链
- CDN加速:静态签章图片使用CDN
- 负载均衡:签名服务独立部署
合规性注意事项
| 要求 | 实现方式 |
|---|---|
| 实名认证 | 对接公安/银联实名接口 |
| 意愿认证 | 短信验证码+人脸识别 |
| 时间戳服务 | 对接国家授时中心 |
| 证据链保存 | 记录完整操作日志+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示例或开发过程中遇到问题,可以提供更多细节。