Реалізація підписання документів через браузер на веб-сайті
Підписання документів через браузер — це процес, при якому користувач переглядає документ, підтверджує свою згоду та залишає підпис без встановлення додаткового ПО. Охоплює широкий спектр: від простого електронного підпису (клік "Погоджуюсь"), намальованого підпису до простого КЕП через SMS.
Рівні електронного підпису
Простий ЕП — підтвердження особистості через логін/пароль або SMS-код. Юридично слабка, підходить для внутрішніх документів та згод.
Посилений неквалістичний ЕП — створюється з допомогою ключів, але без сертифікату акредитованого УЦ. Використовується в b2b за наявності угоди сторін про ЕДО.
Посилений кваліфікований ЕП (КЕП) — повна юридична значимість. Потребує сертифікованого СКЗІ.
Більшість сценаріїв "підписання в браузері" — це простий або посилений неквалітичний ЕП.
Процес підписання
Користувач відкриває документ
↓
Перегляд PDF/HTML документу
↓
Введення підпису (рисування, текст або SMS)
↓
Хеш документа + метаданні → сервер
↓
Сервер створює підписану версію
↓
Сповіщення + збереження
Перегляд PDF у браузері
Перед підписанням користувач повинен прочитати документ. Вбудовуємо переглядач PDF:
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('Невірний код');
}
await redis.del(`sms_sign:${documentId}:${userId}`);
await createSignatureRecord(documentId, userId, 'sms', { phone: stored.phone });
}
Мультиподписання
Документи часто потребують підписів від кількох сторін (контракт між клієнтом та виконавцем). Процес:
- Сторона А підписує → документ отримує статус
partially_signed - Сповіщення стороні Б + посилання на підписання
- Сторона Б підписує → статус
fully_signed - Обидві сторони отримують фіналь документ по email
Терміни
Перегляд PDF + намальований підпис + вбудовування в PDF + аудит-лог — 4–5 днів. SMS-підтвердження — 1–2 дні. Мультиподписання з процесом — 3–4 дні.







