Реализация подписания документов через браузер на сайте
Подписание документов через браузер — это процесс, при котором пользователь просматривает документ, подтверждает своё согласие и оставляет подпись без установки дополнительного ПО. Охватывает широкий спектр: от простой электронной подписи (клик «Согласен»), рисованной подписи до простой КЭП по SMS.
Уровни электронной подписи
Простая ЭП — подтверждение личности через логин/пароль или SMS-код. Юридически слабая, подходит для внутренних документов и согласий.
Усиленная неквалифицированная ЭП — создаётся с помощью ключей, но без сертификата аккредитованного УЦ. Используется в b2b при наличии соглашения сторон об ЭДО.
Усиленная квалифицированная ЭП (КЭП) — полный правовой эффект. Требует сертифицированного СКЗИ.
Большинство сценариев «подписания в браузере» — это простая или усиленная неквалифицированная ЭП.
Workflow подписания
Пользователь открывает документ
↓
Просмотр PDF/HTML документа
↓
Ввод подписи (рисование, текст или SMS)
↓
Хэш документа + метаданные → сервер
↓
Сервер создаёт подписанную версию
↓
Уведомление + сохранение в хранилище
Просмотр PDF в браузере
Перед подписанием пользователь должен прочитать документ. Встраиваем PDF viewer:
import { Viewer, Worker } from '@react-pdf-viewer/core';
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
import '@react-pdf-viewer/core/lib/styles/index.css';
function DocumentViewer({ pdfUrl, onDocumentRead }) {
const [pagesRead, setPagesRead] = useState(new Set<number>());
const totalPagesRef = useRef(0);
const handlePageChange = ({ currentPage }: { currentPage: number }) => {
setPagesRead(prev => {
const updated = new Set(prev).add(currentPage);
if (updated.size >= totalPagesRef.current) {
onDocumentRead(); // Разблокируем кнопку подписи
}
return updated;
});
};
return (
<Worker workerUrl="/pdf.worker.min.js">
<Viewer
fileUrl={pdfUrl}
plugins={[defaultLayoutPlugin()]}
onPageChange={handlePageChange}
onDocumentLoad={({ doc }) => { totalPagesRef.current = doc.numPages; }}
/>
</Worker>
);
}
Применение подписи к PDF
После получения подписи пользователя встраиваем её в PDF и создаём подписанную версию:
// Backend: обработка подписанного документа
async function processDocumentSignature(documentId, userId, signatureDataUrl, metadata) {
const document = await db.documents.findByPk(documentId);
// 1. Загружаем оригинальный PDF
const originalPdf = await s3.getObject({ Bucket: BUCKET, Key: document.s3Key }).promise();
// 2. Вычисляем хэш оригинала для аудит-лога
const originalHash = crypto.createHash('sha256').update(originalPdf.Body).digest('hex');
// 3. Встраиваем подпись
const { PDFDocument, rgb } = require('pdf-lib');
const pdfDoc = await PDFDocument.load(originalPdf.Body);
const lastPage = pdfDoc.getPages().at(-1);
// Добавляем изображение подписи
const signatureImage = await pdfDoc.embedPng(
Buffer.from(signatureDataUrl.replace(/^data:image\/png;base64,/, ''), 'base64')
);
lastPage.drawImage(signatureImage, { x: 60, y: 50, width: 150, height: 50 });
// Добавляем текстовый штамп
const font = await pdfDoc.embedFont('Helvetica');
lastPage.drawText(
`Подписано: ${metadata.signerName}\n${metadata.signedAt.toISOString()}\nIP: ${metadata.ip}`,
{ x: 60, y: 30, size: 8, font, color: rgb(0.4, 0.4, 0.4) }
);
const signedPdfBytes = await pdfDoc.save();
// 4. Сохраняем подписанную версию
const signedKey = `signed/${documentId}/${userId}.pdf`;
await s3.putObject({ Bucket: BUCKET, Key: signedKey, Body: signedPdfBytes }).promise();
// 5. Фиксируем в БД
await db.documentSignatures.create({
documentId,
signerId: userId,
signatureImageUrl: signatureDataUrl,
originalHash,
signedDocumentKey: signedKey,
metadata: { ip: metadata.ip, userAgent: metadata.userAgent, signedAt: new Date() },
});
return signedKey;
}
SMS-подтверждение как простая ЭП
Для юридически значимых согласий — подтверждение номером телефона:
// Генерация и отправка кода
async function initiateSmsSigning(documentId, userId, phone) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
const codeHash = bcrypt.hashSync(code, 10);
await redis.setex(
`sms_sign:${documentId}:${userId}`,
300, // 5 минут
JSON.stringify({ codeHash, phone, attempts: 0 })
);
await smsService.send(phone, `Код подписания документа: ${code}. Действует 5 минут.`);
}
// Проверка кода и фиксация подписи
async function confirmSmsSigning(documentId, userId, code) {
const stored = JSON.parse(await redis.get(`sms_sign:${documentId}:${userId}`));
if (!stored || !bcrypt.compareSync(code, stored.codeHash)) {
throw new Error('Invalid code');
}
await redis.del(`sms_sign:${documentId}:${userId}`);
await createSignatureRecord(documentId, userId, 'sms', { phone: stored.phone });
}
Мультиподписание
Документы часто требуют подписей от нескольких сторон (договор между клиентом и исполнителем). Workflow:
- Сторона А подписывает → документ получает статус
partially_signed - Уведомление стороне Б + ссылка на подписание
- Сторона Б подписывает → статус
fully_signed - Обе стороны получают финальный документ по email
Сроки
Просмотр PDF + рисованная подпись + встройка в PDF + аудит-лог — 4–5 дней. SMS-подтверждение — 1–2 дня. Мультиподписание с workflow — 3–4 дня.







