Реализация электронного подписания документов
Электронная подпись в веб-приложении решает юридически значимые задачи: подписание договоров, согласие с офертой, подтверждение транзакций. Различают несколько типов: простая ЭП (ПЭП — логин/пароль/SMS-код), усиленная неквалифицированная (НЭП — криптография, токены), квалифицированная (КЭП — только через удостоверяющий центр, имеет высшую юрсилу).
Простая ЭП: SMS-подписание договора
Наиболее распространённый подход для B2C: пользователь получает код по SMS, вводит его, PDF-договор фиксируется с отметкой времени, IP, fingerprint. Юридически значима как простая ЭП при наличии соглашения об использовании ЭП.
// Модель документа с аудитом подписания
class Document extends Model
{
protected $casts = [
'signing_metadata' => 'array',
'signed_at' => 'datetime',
];
}
// Сервис подписания
class DocumentSigningService
{
public function initiateSignin(Document $document, User $user): void
{
// Генерация и отправка кода
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
Cache::put("signing_code:{$document->id}:{$user->id}", bcrypt($code), now()->addMinutes(15));
$user->notify(new DocumentSigningCodeNotification($code, $document));
}
public function confirmSigning(Document $document, User $user, string $code, Request $request): void
{
$cached = Cache::get("signing_code:{$document->id}:{$user->id}");
if (!$cached || !Hash::check($code, $cached)) {
throw new InvalidSigningCodeException('Неверный или истёкший код подтверждения');
}
// Создать хэш текущей версии документа
$documentHash = hash('sha256', Storage::disk('s3')->get($document->path));
$document->update([
'status' => 'signed',
'signed_at' => now(),
'signed_by' => $user->id,
'document_hash' => $documentHash,
'signing_metadata' => [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'fingerprint' => $request->header('X-Client-Fingerprint'),
'method' => 'sms_code',
'phone_last4' => substr($user->phone, -4),
'code_sent_at' => Cache::get("signing_code_sent_at:{$document->id}:{$user->id}"),
'signed_at_iso' => now()->toIso8601String(),
'timezone' => $request->header('X-Timezone', 'UTC'),
],
]);
// Зафиксировать подпись в аудит-логе
AuditLog::create([
'action' => 'document.signed',
'user_id' => $user->id,
'document_id' => $document->id,
'metadata' => $document->signing_metadata,
]);
Cache::forget("signing_code:{$document->id}:{$user->id}");
// Отправить подписанную копию на email
$user->notify(new DocumentSignedNotification($document));
}
}
Визуальная подпись: canvas + API
import SignatureCanvas from 'react-signature-canvas';
import { useRef, useState } from 'react';
function DocumentSigner({ documentId }: { documentId: number }) {
const sigCanvas = useRef<SignatureCanvas>(null);
const [step, setStep] = useState<'draw' | 'confirm' | 'sms'>('draw');
const [smsCode, setSmsCode] = useState('');
const handleDrawComplete = async () => {
if (sigCanvas.current?.isEmpty()) return;
const signatureData = sigCanvas.current!.toDataURL('image/png');
// Сохранить изображение подписи, перейти к SMS-подтверждению
await api.post(`/documents/${documentId}/initiate`, { signature_image: signatureData });
setStep('sms');
};
const handleSmsConfirm = async () => {
await api.post(`/documents/${documentId}/confirm`, { code: smsCode });
setStep('confirm');
};
return (
<div>
{step === 'draw' && (
<>
<p>Нарисуйте вашу подпись:</p>
<div style={{ border: '1px solid #e5e7eb', borderRadius: 8 }}>
<SignatureCanvas
ref={sigCanvas}
penColor="#1a1a1a"
canvasProps={{ width: 500, height: 200, className: 'signature-canvas' }}
/>
</div>
<button onClick={() => sigCanvas.current?.clear()}>Очистить</button>
<button onClick={handleDrawComplete}>Далее</button>
</>
)}
{step === 'sms' && (
<>
<p>Введите код из SMS для подтверждения подписи:</p>
<input
type="text" inputMode="numeric"
maxLength={6} value={smsCode}
onChange={e => setSmsCode(e.target.value)}
/>
<button onClick={handleSmsConfirm}>Подписать</button>
</>
)}
{step === 'confirm' && (
<p>Документ успешно подписан. Копия отправлена на ваш email.</p>
)}
</div>
);
}
Встраивание подписи в PDF
use Smalot\PdfParser\Parser;
use setasign\Fpdi\Fpdi;
class SignedPdfService
{
public function addSignatureStamp(string $pdfPath, array $signing): string
{
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($pdfPath);
for ($i = 1; $i <= $pageCount; $i++) {
$templateId = $pdf->importPage($i);
$size = $pdf->getTemplateSize($templateId);
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($templateId);
// На последней странице добавить штамп подписи
if ($i === $pageCount) {
$pdf->SetFont('DejaVu', '', 8);
$pdf->SetTextColor(0, 100, 0);
$stamp = implode("\n", [
"Документ подписан электронной подписью",
"Подписант: {$signing['user_name']}",
"Email: {$signing['user_email']}",
"Дата: {$signing['signed_at']}",
"SHA-256: " . substr($signing['document_hash'], 0, 16) . '...',
"IP: {$signing['ip']}",
]);
$pdf->MultiCell(0, 4, $stamp, 'D', 'L');
}
}
$signedPath = str_replace('.pdf', '_signed.pdf', $pdfPath);
$pdf->Output('F', $signedPath);
return $signedPath;
}
}
КЭП через СБИС / КриптоПро
Для юридически значимых договоров с максимальной силой (B2B, госзаказ) используется квалифицированная ЭП через удостоверяющий центр:
// Интеграция со СБИС API (подписание на стороне УЦ)
class SbisSigningService
{
public function sign(string $documentBase64, int $signatoryId): string
{
$response = Http::withToken($this->getToken())
->post('https://online.sbis.ru/service/sbis.Signature.Sign', [
'jsonrpc' => '2.0',
'method' => 'СБИС.ПодписатьДокумент',
'params' => [
'Документ' => $documentBase64,
'Подписывающий' => $signatoryId,
],
]);
return $response->json('result.Подпись');
}
}
Хранение и проверка
// Верификация подписи — проверить, что документ не изменён после подписания
public function verify(Document $document): bool
{
$currentHash = hash('sha256', Storage::disk('s3')->get($document->path));
return hash_equals($document->document_hash, $currentHash);
}
Подписанные документы хранят с неизменяемыми правами (S3 Object Lock) для предотвращения фальсификации.
Срок реализации
| Задача | Срок |
|---|---|
| Простая ЭП (SMS-подтверждение + аудит) | 3–4 дня |
| Canvas подпись + встраивание в PDF | +2–3 дня |
| Интеграция СБИС или КриптоПро КЭП | 5–7 дней |
| Полная система с хранилищем и верификацией | 7–10 дней |







