平台概览与部署
理解 DocuSeal 的核心定位与架构,掌握 Docker 一键部署——从零到运行只需一条命令
DocuSeal 是什么?
想象你需要让 50 个合作伙伴签署一份保密协议(NDA)。传统方式是发邮件、打印、签字、扫描、回传——一轮下来至少三天。现在有一个开源工具,能让你上传 PDF、拖拽设置签名区域、一键发送给所有签署人、自动追踪状态、生成审计日志。这就是 DocuSeal。
DocuSeal 不只是"电子签名"——它是完整的文档处理流水线。从创建模板、分配签署人、发送邀请、追踪状态,到生成审计日志和签名验证,覆盖文档签署的全生命周期。而且它是开源的,你可以部署在自己的服务器上,数据完全由你掌控。
PDF 表单构建器
所见即所得的拖拽式编辑器,支持 12 种字段类型:签名、日期、文件上传、复选框、文本、下拉框等。每个字段可绑定到特定签署人角色。
自动化邮件通知
通过 SMTP 自动发送签署邀请、完成通知、拒绝通知。支持自定义邮件模板和回复地址,每个签署人收到个性化的签署链接。
电子签名验证
自动在 PDF 中嵌入电子签名,支持签名验证(HexaPDF)、审计日志生成,确保文档的完整性和不可抵赖性。
开放 API 与 Webhook
RESTful API 支持创建模板、发起签署、查询状态。Webhook 实时推送事件通知,方便与你的业务系统深度集成。
核心架构:四层模型
DocuSeal 基于 Ruby on Rails 构建,采用经典的三层架构加上一个外部集成层。理解这四层模型,就能快速定位任何功能的代码位置。
用户界面层 — 用户看到和操作的部分
业务逻辑层 — 处理核心工作流
数据持久层 — 存储核心数据
集成层 — 连接外部世界
Docker 一键部署:从零到运行
DocuSeal 最吸引人的地方之一就是部署极简。一条 Docker 命令就能启动完整的签署平台:
# 最简启动 — SQLite + 本地磁盘存储
docker run --name docuseal \
-p 3000:3000 \
-v .:/data \
docuseal/docuseal
# 生产级启动 — PostgreSQL + 自动 HTTPS
curl -O https://raw.githubusercontent.com/
docusealco/docuseal/master/
docker-compose.yml
# 设置你的域名,Caddy 自动签发 SSL 证书
sudo HOST=sign.yourcompany.com \
docker compose up -d
这是最简启动命令——只需一个容器,端口映射到 3000
-p 3000:3000 把容器内 3000 端口映射到宿主机 3000 端口
-v .:/data 把当前目录挂载为数据目录,SQLite 数据库和上传文件都存在这里
docuseal/docuseal 是官方 Docker 镜像,基于 Ruby 4.0 + Alpine Linux
空行分隔——下面是生产级部署方式
下载官方 docker-compose.yml,包含三个服务:app + PostgreSQL + Caddy
空行
设置 HOST 环境变量为你的域名
Caddy 会自动签发 Let's Encrypt SSL 证书,反向代理到 app:3000
-d 参数让容器在后台运行
生产部署推荐使用 Docker Compose,它包含三个服务:app(DocuSeal 主应用,端口 3000)、postgres(PostgreSQL 18 数据库,带健康检查)、caddy(自动 HTTPS 反向代理)。数据持久化通过 volume 挂载:./docuseal 映射应用数据,./pg_data 映射数据库数据。
代码翻译:Dockerfile 多阶段构建
DocuSeal 的 Dockerfile 使用了三个构建阶段。这是 多阶段构建 的经典实践——最终镜像只包含运行时必需的文件。
FROM ruby:4.0.1-alpine AS download
WORKDIR /fonts
RUN wget GoNotoKurrent-Regular.ttf \
&& wget DancingScript-Regular.otf \
&& wget model_704_int8.onnx \
&& wget pdfium-linux.tgz
FROM ruby:4.0.1-alpine AS webpack
RUN apk add nodejs yarn \
&& yarn install
RUN ./bin/shakapacker
FROM ruby:4.0.1-alpine AS app
RUN apk add libpq vips redis \
onnxruntime
COPY --from=download /fonts/ /fonts/
COPY --from=webpack /app/public/packs ./
CMD ["bundle", "exec", "puma"]
第一阶段 download:下载字体和 AI 模型文件
Noto 字体支持多语言渲染,Dancing Script 用于手写签名样式
model_704_int8.onnx 是 ONNX 格式的字段检测 AI 模型(自动识别 PDF 中的表单字段)
pdfium 是 PDF 渲染引擎(Google 出品),用于 PDF 预览和合并
第二阶段 webpack:编译前端 JavaScript
安装 Node.js + Yarn,下载前端依赖(Vue.js、Tailwind CSS 等)
运行 Shakapacker(Webpack 封装)编译前端资源到 public/packs/
第三阶段 app:最终运行时镜像
安装运行时依赖:libpq(PostgreSQL 客户端)、vips(图像处理)、Redis(缓存)、onnxruntime(AI 推理)
从 download 阶段复制字体和 AI 模型
从 webpack 阶段复制编译好的前端资源
启动 Puma Web 服务器,监听 3000 端口
项目目录结构
DocuSeal 遵循 Ruby on Rails 标准目录结构。了解目录布局,就能快速定位功能代码:
签署流程的数据旅程
当用户发起一个签署请求时,数据在系统各个组件之间流转。想象一条流水线——PDF 模板是原材料,经过构建、发送、签署、验证,最终产出一份合法的电子签文档。
检验你的理解
DocuSeal 中,哪个模型定义了文档的字段配置和签署人角色?
使用 Docker Compose 生产部署时,app 服务依赖哪个服务的健康检查?
DocuSeal 的 Dockerfile 为什么要使用多阶段构建?
模板与表单构建
深入 Template 模型的字段定义、Schema 布局和签署人角色——理解 PDF 表单是如何被描述的
Template 模型:文档的 DNA
在 DocuSeal 中,Template 就像一份文档的 DNA——它编码了"这份文档长什么样、谁需要填什么、字段放在哪里"。理解 Template 的五个核心序列化字段,就掌握了 DocuSeal 的精髓。
fields(字段定义)
JSON 数组,定义所有表单字段。每个字段包含 uuid、name、type(signature/date/text/checkbox/file 等 12 种)、required、readonly 等属性,以及 areas(字段在 PDF 页面上的坐标位置)。
submitters(签署人角色)
JSON 数组,定义签署方角色。每个角色有 name(如"甲方"、"乙方")和 uuid。创建签署时,每个角色对应一个实际的签署人(Submitter)。
schema(文档布局)
JSON 数组,描述文档结构。每个条目对应一个 PDF 附件,包含 attachment_uuid 和页面信息。支持动态文档(dynamic: true),允许在签署时替换文档内容。
preferences(偏好设置)
JSON 对象,存储模板级别的配置:邮件模板(request_email_subject/body)、签署顺序(submitters_order)、完成通知设置等。控制签署流程的行为。
代码翻译:fields 字段定义结构
每个字段是一个 JSON 对象,包含丰富的配置。左边是实际代码,右边是逐项解释:
{
"uuid": "a1b2c3d4",
"submitter_uuid": "sub-001",
"name": "employee_signature",
"type": "signature",
"required": true,
"readonly": false,
"title": "Employee Signature",
"description": "Please sign here",
"default_value": null,
"areas": [{
"uuid": "area-001",
"x": 0.15,
"y": 0.72,
"w": 0.30,
"h": 0.08,
"page": 1,
"attachment_uuid": "doc-001"
}],
"conditions": [],
"validation": {}
}
字段的唯一标识符(UUID 格式),用于关联签署人的填写值
绑定到哪个签署人角色——只有这个角色的签署人能填写此字段
字段名称,API 创建签署时可用来预填值
字段类型:signature(签名)、date(日期)、text(文本)、checkbox(复选框)、file(文件上传)、dropdown(下拉框)、radio(单选)、number(数字)、initials(首字母缩写)、image(图片)、stamp(印章)、cells(表格单元格)——共 12 种
是否必填——提交时会验证
是否只读——只读字段会显示但不允许修改(用于预填值场景)
显示给签署人的字段标题
字段描述/提示文字
默认值——可以为数组(如多选框)
areas 数组:字段在 PDF 页面上的位置区域
x/y 是相对坐标(0~1),w/h 是相对宽高,page 是页码
attachment_uuid 指向对应的 PDF 文档附件
条件字段配置(Pro 功能):满足条件时才显示此字段
验证规则:支持正则(pattern)、范围(min/max)、错误提示(message)
字段定义结束
12 种字段类型一览
DocuSeal 支持 12 种表单字段类型,覆盖了文档签署和表单填写的各种场景。每种字段类型在渲染、验证和存储上有不同的行为:
签名和首字母缩写。使用 Signature Pad(签名画板)组件,支持手写签名绘制。签署后自动嵌入 PDF 的指定位置。这是 DocuSeal 最核心的字段类型。
基础输入字段。text 是通用文本输入,number 限制数字输入(支持 min/max/step 验证),date 显示日期选择器。都支持默认值和只读模式。
选择类字段。checkbox 是复选框(多选),radio 是单选按钮,dropdown 是下拉选择框。每个选项通过 options 数组定义,包含 value 和 uuid。
文件和图像类字段。file 允许上传任意文件,image 限制为图片格式,stamp 是印章字段(类似签名但通常是公司公章或认证章)。
表格单元格字段。用于需要填写表格数据的场景,支持 cell_w(单元格宽度)配置。适合发票、报价单等结构化表格填写。
代码翻译:Template 模型定义
来看 Template 模型的 Ruby 源码。这个模型虽然不到 100 行,但承载了整个系统的核心数据结构:
class Template < ApplicationRecord
belongs_to :author,
class_name: 'User'
belongs_to :account
belongs_to :folder,
class_name: 'TemplateFolder'
serialize :preferences,
coder: JSON
serialize :fields,
coder: JSON
serialize :schema,
coder: JSON
serialize :submitters,
coder: JSON
has_many_attached :documents
has_many :submissions,
dependent: :destroy
has_many :template_versions,
dependent: :destroy
attribute :slug,
default: -> {
SecureRandom.base58(14)
}
end
Template 继承自 ApplicationRecord(Rails 基类),映射到 templates 数据表
模板作者——指向 User 模型,记录是谁创建了这个模板
所属账户——多租户隔离,每个模板属于一个 Account
所属文件夹——模板可以组织在文件夹中,支持多级嵌套
空行
序列化字段:Rails 的 serialize 将 JSON 字符串自动转换为 Ruby 对象
preferences 存储偏好设置(邮件模板、签署顺序等)
fields 存储字段定义数组(上文详解的结构)
schema 存储文档布局(关联的 PDF 附件和页面信息)
submitters 存储签署人角色定义
空行
Active Storage 多文件附件——模板可以关联多个 PDF 文档
一对多关联:一个模板可以有多个签署提交(Submission)
模板版本历史——每次编辑自动创建版本快照
空行
slug 是模板的唯一短链接标识,使用 Base58 编码生成 14 位随机字符串
Base58 比 UUID 更短、更适合 URL(去掉了容易混淆的 0/O、1/I/l 等字符)
多签署人角色:一份合同,多方签署
DocuSeal 的模板支持多个 签署人角色。这是 DocuSeal 的核心设计——每个字段都通过 submitter_uuid 绑定到特定的角色。当发起签署时,每个角色对应一个实际的签署人。
假设一份 NDA 模板定义了两个角色:"Disclosing Party"(披露方)和 "Receiving Party"(接收方)。模板中的"公司名称"字段绑定到披露方,"签字人"字段绑定到接收方。发起签署时,系统分别为两方创建签署链接,各方只能看到和填写自己的字段。
// submitters JSON 结构
[
{
"name": "First Party",
"uuid": "sub-001",
"is_requester": true
},
{
"name": "Second Party",
"uuid": "sub-002",
"invite_by": "email"
}
]
// 字段绑定到角色
{
"name": "party1_signature",
"submitter_uuid": "sub-001"
}
submitters 数组:定义模板中的签署方角色
第一个角色:"First Party"(甲方)
UUID 用于字段绑定——字段通过这个 UUID 关联到此角色
is_requester: true 标记为发起方(通常是创建签署的人)
第二个角色:"Second Party"(乙方)
独立的 UUID,确保字段隔离
invite_by: "email" 指定通过邮件邀请此角色的签署人
角色数组结束
空行
字段通过 submitter_uuid 绑定到特定角色
只有 submitter_uuid="sub-001" 的签署人(甲方)能填写此签名字段
乙方打开签署页面时,看不到甲方的字段
检验你的理解
Template 模型中,哪个字段存储了"字段类型、在 PDF 上的位置、绑定到哪个签署人"这些信息?
签名框在 PDF 第二页的位置信息存储在哪里?
如何实现"甲方只能看到甲方的字段,乙方只能看到乙方的字段"?
提交与签署流程
追踪 Submission 从创建到完成的完整生命周期——状态机、签署表单、邮件通知和异步任务
Submission 生命周期:从创建到完成
Submission 的生命周期就像一个快递包裹——从"已下单"到"已送达",中间经历多个状态转换。DocuSeal 用 scope(查询范围)来定义这些状态。
管理员通过 API 或界面发起签署。系统创建 Submission 记录,从模板复制 fields、schema、submitters 配置。source 字段记录来源:link(链接分享)、api(API 调用)、embed(嵌入式)、invite(邀请)、bulk(批量发送)。
为每个角色创建 Submitter 记录,生成唯一的签署链接 slug。每个 Submitter 有独立的状态:awaiting -> sent -> opened -> completed/declined。metadata 字段可以存储自定义数据(如用户 ID、订单号)。
SendSubmitterInvitationEmailJob 异步发送邀请邮件。邮件包含签署链接(/s/:slug),支持自定义邮件模板和回复地址。sent_at 字段记录发送时间。也可以通过 SMS 发送(Pro 功能)。
签署人打开链接,SubmitFormController#show 渲染签署表单。opened_at 记录打开时间。支持签署顺序控制(enforce_signing_order)和 2FA 验证(邮件验证码/手机验证码)。
签署人提交表单,SubmitFormController#update 调用 Submitters::SubmitValues 处理。ProcessSubmitterCompletionJob 异步生成签署后的 PDF、嵌入电子签名、创建审计日志、触发 Webhook 通知。completed_at 记录完成时间。
Submitter 的五种状态
每个 Submitter 有独立的状态追踪。Submitter 模型的 status 方法是一个简单的状态机:
# app/models/submitter.rb
def status
if declined_at?
'declined'
elsif completed_at?
'completed'
elsif opened_at?
'opened'
elsif sent_at?
'sent'
else
'awaiting'
end
end
status 方法使用时间戳字段判断当前状态
declined_at 不为空 = 签署人已拒绝签署
declined 状态——签署人点击了"拒绝"按钮
completed_at 不为空 = 签署人已完成签署
completed 状态——签署人填写完所有必填字段并提交
opened_at 不为空 = 签署人已打开签署链接
opened 状态——签署人正在查看/填写表单
sent_at 不为空 = 邀请邮件已发送
sent 状态——邀请已发出,等待签署人打开
以上时间戳都为空 = 尚未发送邀请
awaiting 状态——等待管理员发送邀请
状态判断结束
方法结束
DocuSeal 使用时间戳(declined_at、completed_at、opened_at、sent_at)而非枚举字段来实现状态机。这种设计的好处是:状态判断和数据审计合二为一——你不仅能知道当前状态,还能精确知道状态变化的时间。
代码翻译:签署表单控制器
SubmitFormController 是签署人直接交互的入口。当签署人打开链接 /s/:slug 时,show 方法渲染签署表单;当签署人提交表单时,update 方法处理数据。这段代码体现了 DocuSeal 的核心安全设计:
# app/controllers/submit_form_controller.rb
class SubmitFormController
skip_before_action
:authenticate_user!
skip_authorization_check
def show
return render :email_2fa
unless authorized?
return redirect_to :completed
if submitter.completed_at?
return render :awaiting
if enforce_order?
end
def update
return error if completed?
return error if archived?
return error if expired?
return error if declined?
Submitters::SubmitValues.call(
submitter, params, request
)
head :ok
end
end
控制器定义
跳过身份认证——签署人无需登录(签署链接本身就是认证凭证)
跳过权限检查——签署链接已包含授权信息
空行
show 方法:渲染签署表单页面
安全检查 1:如果需要 2FA 验证且未通过,显示验证码页面
安全检查 2:如果已完成签署,重定向到完成页面
安全检查 3:如果启用签署顺序且轮到别人,显示等待页面
show 方法结束
空行
update 方法:处理签署人提交的表单数据
防御性检查:已完成签署 -> 拒绝(防重复提交)
防御性检查:模板或提交已归档 -> 拒绝
防御性检查:签署链接已过期 -> 拒绝(expire_at 检查)
防御性检查:签署人已拒绝 -> 拒绝(不能再修改)
空行
所有检查通过,调用 SubmitValues 服务处理表单数据
传入 submitter、params(表单数据)和 request(用于审计追踪)
返回 200 OK
update 方法结束
控制器结束
异步任务管道:签署完成后的幕后工作
当签署人提交表单后,系统不会同步执行所有操作——那样会让用户等待太久。DocuSeal 使用 Sidekiq 异步任务队列来处理后续工作。
邮件通知系统:四种核心邮件
DocuSeal 的邮件系统(SubmitterMailer)负责签署流程中所有与用户的邮件交互。理解这四种核心邮件,就掌握了签署流程的用户体验设计:
发送给签署人的邀请邮件,包含签署链接 /s/:slug。支持自定义邮件标题和正文(request_email_subject/body),可使用变量替换(如 {{submitter.name}})。这是签署流程的起点。
当签署人完成签署后,发送给模板创建者的通知邮件。附件自动包含签署后的 PDF 文档和审计日志(总计不超过 10MB)。支持自定义邮件模板。
当签署人拒绝签署时,发送给创建者的通知邮件。reply_to 设置为签署人邮箱,方便创建者直接回复了解拒绝原因。
签署完成后,发送给签署人的文档副本邮件。包含签署后的 PDF 和审计日志。确保签署人保留一份完整记录。
检验你的理解
Submitter 模型如何判断签署人的当前状态?
签署人打开签署链接时,需要登录吗?
哪个异步任务负责将签名嵌入到 PDF 文档中?
API 与 Webhook 集成
掌握 DocuSeal 的 RESTful API、认证机制和 Webhook 事件系统——把签署功能嵌入你的业务
API 概览:所有路径都在 /api/ 命名空间下
DocuSeal 的 REST API 路径集中在 /api/ 命名空间下。从 routes.rb 中可以看到,API 分为以下几个核心资源:
GET /api/templates
列出所有模板(支持分页、搜索、文件夹过滤)
GET /api/templates/:id
获取模板详情(包含字段定义、签署人角色、文档 URL)
PATCH /api/templates/:id
更新模板(修改字段、角色、偏好设置)
POST /api/submissions
创建签署提交(核心接口——发起一次签署流程)
GET /api/submissions/:id
获取提交详情(包含签署人状态、签署后文档 URL)
POST /api/submissions/init
创建并初始化提交(返回结构化数据,适合嵌入式场景)
GET /api/submitters/:id
获取签署人详情(包含签署链接、填写值、状态)
POST /api/tools/merge
合并多个 PDF 文件(Base64 编码输入输出)
POST /api/tools/verify
验证 PDF 签名(检查签名有效性和文档完整性)
认证机制:Access Token
API 认证使用 Access Token(访问令牌)。每个用户可以在设置页面生成一个 token,所有 API 请求都需要在 HTTP Header 中携带它:
# 创建签署提交的 API 调用示例
curl -X POST \
https://sign.example.com/api/submissions \
-H "X-Auth-Token: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"template_id": 42,
"send_email": true,
"submitters": [{
"email": "alice@example.com",
"name": "Alice Chen",
"uuid": "sub-001",
"values": {
"company_name": "Acme Corp"
}
}]
}'
使用 curl 命令调用创建签署提交的 API
POST 请求到 /api/submissions 端点
请求地址——你的 DocuSeal 实例域名
认证 Header:X-Auth-Token 携带你的 API 访问令牌
内容类型:JSON 格式的请求体
JSON 请求体开始
template_id:使用哪个模板创建签署——这里用 ID 42 的模板
send_email: true——创建后自动发送邀请邮件给签署人
submitters 数组:定义实际的签署人
签署人邮箱——邀请邮件会发到这个地址
签署人姓名——显示在邮件和文档中
uuid:对应模板中的签署人角色(绑定到此角色的字段)
values:预填值——自动填充 "company_name" 字段
这里预填了公司名称为 "Acme Corp"
预填值结束
签署人数组结束
JSON 请求体结束
DocuSeal 使用 CanCanCan(ability.rb)进行权限控制。API 控制器通过 load_and_authorize_resource 宏自动加载资源并检查权限。AccessToken 模型关联到 User,User 又属于 Account,确保多租户数据隔离。
代码翻译:API 创建签署的核心逻辑
看 Api::SubmissionsController#create 的源码——这个方法是 API 集成的核心入口:
# app/controllers/api/submissions_controller.rb
def create
Params::SubmissionCreateValidator
.call(params)
submissions = if emails?
Submissions.create_from_emails(
template:, user:,
source: :api,
emails:, params:
)
else
Submissions.create_from_submitters(
template:, user:,
source: :api,
submitters_order:,
submissions_attrs:
)
end
WebhookUrls.enqueue_events(
submissions,
'submission.created'
)
end
create 方法:创建签署提交的核心入口
第一步:参数验证——检查必填参数、邮箱格式、字段合法性等
SubmissionCreateValidator 是自定义验证器
空行
根据参数类型选择不同的创建方式
如果传了 emails 参数(简单模式)——一行一个邮箱,自动创建签署人
create_from_emails:为每个邮箱创建 Submission + Submitter
template 是模板对象,user 是当前 API 调用者
source: :api 标记来源为 API 调用(用于统计和审计)
emails 是签署人邮箱列表,params 是额外配置
否则使用详细模式——传入完整的 submitters 配置
create_from_submitters:支持预填值、自定义字段、元数据等
submitters_order:签署顺序(random 随机 / preserved 按顺序)
submissions_attrs:签署人属性数组(邮箱、姓名、预填值、元数据等)
条件分支结束
空行
创建完成后,触发 submission.created Webhook 事件
传入创建的 submissions 数组和事件名称
Webhook 事件会异步推送到所有配置了此事件的 Webhook URL
create 方法结束
Webhook 事件系统:11 种实时通知
Webhook 是 DocuSeal 与外部系统集成的关键桥梁。当特定事件发生时,系统会向配置的 URL 发送 HTTP POST 请求,携带事件数据。
form.viewed
签署人打开了签署链接。触发时机:SubmitFormController#show 被调用。数据包含 submitter 信息和时间戳。
form.started
签署人开始填写表单(第一个字段被修改时触发)。
form.completed
签署人完成签署。最常用的 Webhook 事件——用于触发后续业务流程(如更新订单状态、发送确认邮件等)。
form.declined
签署人拒绝签署。可用于通知销售团队跟进、记录拒绝原因。
submission.created / completed / expired / archived
提交级别事件:创建、全部完成、已过期、已归档。适合整体流程追踪。
template.created / updated / archived
模板级别事件:模板被创建、更新或归档时触发。用于同步模板变更到外部系统。
代码翻译:Webhook HMAC 签名验证
安全是 Webhook 的核心问题——你怎么确认收到的请求真的来自 DocuSeal?DocuSeal 使用 HMAC-SHA256 签名来保证请求的完整性和真实性:
# WebhookUrl 模型中的签名配置
class WebhookUrl < ApplicationRecord
EVENTS = %w[
form.viewed
form.started
form.completed
form.declined
submission.created
submission.completed
submission.expired
submission.archived
template.created
template.updated
template.archived
]
encrypts :url,
:secret,
:hmac_secret
def set_hmac_secret
self.hmac_secret ||=
WebhookUrls::Signatures
.generate_secret
end
end
WebhookUrl 模型——管理 Webhook 端点配置
EVENTS 常量:定义所有支持的 Webhook 事件类型
表单级事件:签署人查看、开始填写、完成签署、拒绝
提交级事件:创建、全部完成、已过期、已归档
模板级事件:创建、更新、归档
总共 11 种事件类型
空行
加密存储:url、secret、hmac_secret 使用 ActiveRecord 加密
这些敏感字段在数据库中是加密存储的,即使数据库泄露也无法直接读取
hmac_secret 是签名密钥,用于生成请求签名
空行
自动生成 HMAC 密钥(如果未设置)
使用 WebhookUrls::Signatures 服务生成安全的随机密钥
方法结束
模型结束
验证 Webhook 请求的步骤:1) 从 HTTP Header 中取出 X-Webhook-Signature;2) 用 hmac_secret 对请求体做 HMAC-SHA256 签名;3) 比较两个签名是否一致。DocuSeal 还支持 Webhook 事件重发(resend)和事件日志查看,方便调试和故障排查。
检验你的理解
调用 DocuSeal API 时,认证 Token 应该放在哪里?
发起一次签署流程,应该调用哪个 API 端点?
Webhook 如何保证请求的真实性和完整性?
安全、签名验证与生产部署
深入 DocuSeal 的安全机制——电子签名原理、PDF 签名验证、审计日志和生产环境最佳实践
电子签名原理:从手写到 PDF 嵌入
DocuSeal 的电子签名流程涉及三个关键步骤:手写签名采集、PDF 签名嵌入和签名验证。理解这个流程,就理解了 DocuSeal 的核心技术价值。
前端使用 Signature Pad(signature_pad JavaScript 库)采集手写签名。签署人在画板上用鼠标或手指绘制签名,数据以 SVG 路径点坐标的形式保存。同时 DocuSeal 支持 Dancing Script 字体生成"手写风格"的文本签名作为备选。
签署完成后,异步任务将签名图片渲染到 PDF 文档的指定位置(基于 fields 中 areas 定义的 x/y/w/h 坐标)。使用 Vips(高性能图像处理库)处理签名图片,确保清晰度和尺寸适配。
生成一份独立的审计日志 PDF,记录每个签署人的签署时间、IP 地址、User-Agent、地理位置等信息。审计日志附加到 Submission 的 audit_trail 附件中,与签署文档一起提供给管理员。
代码翻译:PDF 签名验证 API
DocuSeal 提供 PDF 签名验证 API(POST /api/tools/verify),使用 HexaPDF 库解析和验证 PDF 中的数字签名:
# app/controllers/api/tools_controller.rb
def verify
file = Base64.decode64(
params[:file]
)
pdf = HexaPDF::Document.new(
io: StringIO.new(file)
)
trusted_certs =
Accounts.load_trusted_certs(
current_account
)
is_checksum_found =
CompletedDocument.exists?(
sha256: Digest::SHA256
.digest(file)
.then { |d|
Base64.urlsafe_encode64(d)
}
)
render json: {
checksum_status:
is_checksum_found ?
'verified' :
'not_found',
signatures:
pdf.signatures.map { |sig|
{
signer_name:
sig.signer_name,
signing_time:
sig.signing_time,
signature_type:
sig.signature_type,
verification_result:
sig.verify(
trusted_certs:
)
}
}
}
end
verify 方法:验证 PDF 文件的签名
Base64 解码传入的 PDF 文件内容
params[:file] 是客户端上传的 Base64 编码的 PDF
使用 HexaPDF 打开 PDF 文档进行解析
StringIO 将二进制数据转为文件流对象
空行
加载账户配置的受信证书——用于验证签名的合法性
从账户设置中加载 CA 证书链
空行
双重验证:检查 PDF 的 SHA256 校验和是否在系统中存在
在 CompletedDocument 表中查找匹配的 sha256 值
计算传入文件的 SHA256 哈希
转为 Base64 URL 安全编码格式
空行
返回 JSON 验证结果
checksum_status:如果在数据库中找到匹配 -> 'verified',否则 -> 'not_found'
signatures 数组:PDF 中所有签名的详细信息
遍历 PDF 中的每个签名对象
签名信息对象开始
签名者名称(从签名证书中提取)
签名时间(签名发生的时间戳)
签名类型(如 PAdES、PKCS#7 等)
验证结果——使用受信证书验证签名有效性
传入受信证书列表
验证结果包含消息列表(警告、错误等)
签名信息对象结束
signatures 数组结束
JSON 响应结束
verify 方法结束
DocuSeal 安全机制全景
除了电子签名验证,DocuSeal 还内置了多层安全机制。作为自行部署的开源平台,你可以审计每一行安全相关代码:
用户认证基于 Devise(支持密码登录、邀请制注册)。权限控制使用 CanCanCan,定义了细粒度的 Ability 规则。API 认证使用 Access Token,Webhook 认证使用 HMAC-SHA256 签名。支持 MFA(TOTP 双因素认证)。
敏感字段使用 Rails 内置的 ActiveRecord Encryption 加密存储。包括 WebhookUrl 的 url、secret、hmac_secret,以及 EncryptedConfig 中的 SMTP 密码、SAML 证书等。密钥通过环境变量管理。
签署人打开链接时可要求邮件验证码或短信验证码验证身份。OTP 验证码通过 EmailVerificationCodes 生成,有时效性。在 SubmitFormController 中通过 require_email_2fa / require_phone_2fa 参数启用。
每次签署完成自动生成审计日志 PDF。记录内容:签署人信息、IP 地址、User-Agent、签署时间、文档 SHA256 校验和。审计日志与签署文档一起归档,满足合规要求。
每个签署链接使用 Base58 编码的 14 位随机 slug,不可猜测。支持链接过期设置(expire_at)。链接分享模板还支持额外的 2FA 验证。拒绝(declined)和已完成(completed)的链接不可再修改。
生产部署最佳实践
将 DocuSeal 部署到生产环境时,需要关注以下几个关键配置:
# docker-compose.yml 生产配置
services:
app:
image: docuseal/docuseal:latest
environment:
- DATABASE_URL=
postgresql://user:pass@postgres/db
- FORCE_SSL=${HOST}
volumes:
- ./docuseal:/data/docuseal
postgres:
image: postgres:18
environment:
POSTGRES_USER: docuseal
POSTGRES_PASSWORD:
${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql
caddy:
image: caddy:latest
command:
caddy reverse-proxy
--from $HOST --to app:3000
ports:
- 80:80
- 443:443
docker-compose.yml 生产环境配置
app 服务:DocuSeal 主应用
使用官方最新镜像(生产环境建议锁定版本号)
环境变量配置
DATABASE_URL:必须使用 PostgreSQL(不支持 SQLite 生产环境)
FORCE_SSL:设置为你的域名,强制 HTTPS 重定向
数据持久化:挂载本地目录到容器 /data/docuseal
空行
postgres 服务:PostgreSQL 数据库
使用 PostgreSQL 18 版本
数据库用户名和密码配置
密码从环境变量读取(不要硬编码在配置文件中)
数据库数据持久化挂载
空行
caddy 服务:自动 HTTPS 反向代理
使用 Caddy 最新版
反向代理配置:从你的域名转发到 app:3000
开放 80(HTTP)和 443(HTTPS)端口
Caddy 自动申请和续期 Let's Encrypt SSL 证书
关键环境变量与文件存储配置
DocuSeal 通过环境变量控制关键配置。以下是最重要的配置项:
DATABASE_URL
数据库连接字符串。默认 SQLite(适合开发),生产环境推荐 postgresql://user:pass@host/dbname。支持 PostgreSQL 和 MySQL。
FORCE_SSL
设置为域名后强制 HTTPS。所有 HTTP 请求自动 301 重定向到 HTTPS。
ACTIVE_STORAGE_SERVICE
文件存储后端。默认 local(本地磁盘),可配置 amazon(AWS S3)、google(GCS)、azure(Azure Blob)、或 mirror(多后端冗余)。
SMTP_* / EMAIL_HOST
邮件发送配置。DocuSeal 使用 ActionMailer 发送邮件,配置 SMTP 服务器地址、端口、认证信息。EMAIL_HOST 用于生成邮件中的链接。
RAILS_ENCRYPTOR_KEY
ActiveRecord Encryption 的主密钥。用于加密 Webhook URL、SMTP 密码等敏感字段。丢失此密钥将无法解密已存储的敏感数据。
自行部署 DocuSeal 时,务必妥善保管 RAILS_ENCRYPTOR_KEY 和数据库密码。建议使用密钥管理服务(如 AWS KMS、HashiCorp Vault)存储加密密钥,而非直接写在 .env 文件中。数据库和文件存储(/data/docuseal)都需要定期备份。
嵌入式签署:把签署功能嵌入你的应用
DocuSeal 支持将签署表单嵌入到你的网站或应用中。这对于 SaaS 产品特别有用——用户无需离开你的网站就能完成签署。实现方式是通过 Embedded Signing SDK:
<!-- 嵌入式签署 HTML 示例 -->
<script
src="https://sign.example.com/js/embed.js"
data-token="SUBMITTER_TOKEN"
data-slug="SUBMITTER_SLUG">
</script>
# 后端:创建嵌入式签署会话
POST /api/submissions/init
{
"template_id": 42,
"send_email": false,
"submitters": [{
"email": "user@example.com",
"embedded": true
}]
}
嵌入式签署的前端实现非常简单
引入 DocuSeal 的嵌入脚本(从你的实例域名加载)
data-token:签署人的认证令牌(从 API 获取)
data-slug:签署人的唯一链接标识
脚本会自动在页面中渲染签署表单
空行
后端需要先调用 /api/submissions/init 创建嵌入式签署会话
注意:这里用的是 /init 端点,不是普通的 /submissions
template_id 指定使用的模板
send_email: false——不发送邮件(因为签署人在你的网站内完成)
签署人配置
签署人邮箱
embedded: true 标记为嵌入式签署模式
响应返回 submitters 数组,每个包含 token 和 slug