01

平台概览与部署

理解 DocuSeal 的核心定位与架构,掌握 Docker 一键部署——从零到运行只需一条命令

DocuSeal 是什么?

想象你需要让 50 个合作伙伴签署一份保密协议(NDA)。传统方式是发邮件、打印、签字、扫描、回传——一轮下来至少三天。现在有一个开源工具,能让你上传 PDF、拖拽设置签名区域、一键发送给所有签署人、自动追踪状态、生成审计日志。这就是 DocuSeal

💡
核心理念

DocuSeal 不只是"电子签名"——它是完整的文档处理流水线。从创建模板、分配签署人、发送邀请、追踪状态,到生成审计日志和签名验证,覆盖文档签署的全生命周期。而且它是开源的,你可以部署在自己的服务器上,数据完全由你掌控。

📄

PDF 表单构建器

所见即所得的拖拽式编辑器,支持 12 种字段类型:签名、日期、文件上传、复选框、文本、下拉框等。每个字段可绑定到特定签署人角色。

自动化邮件通知

通过 SMTP 自动发送签署邀请、完成通知、拒绝通知。支持自定义邮件模板和回复地址,每个签署人收到个性化的签署链接。

🔒

电子签名验证

自动在 PDF 中嵌入电子签名,支持签名验证(HexaPDF)、审计日志生成,确保文档的完整性和不可抵赖性。

🌐

开放 API 与 Webhook

RESTful API 支持创建模板、发起签署、查询状态。Webhook 实时推送事件通知,方便与你的业务系统深度集成。

核心架构:四层模型

DocuSeal 基于 Ruby on Rails 构建,采用经典的三层架构加上一个外部集成层。理解这四层模型,就能快速定位任何功能的代码位置。

用户界面层 — 用户看到和操作的部分

🎨
Vue.js 前端组件
📱
移动签署表单
🔌
嵌入式 SDK

业务逻辑层 — 处理核心工作流

📋
模板管理 (Template)
📦
提交流程 (Submission)
异步任务队列

数据持久层 — 存储核心数据

🗃
关系型数据库
📁
文件存储 (Active Storage)

集成层 — 连接外部世界

🔌
REST API
🔗
Webhook 事件
邮件服务 (SMTP)
点击任意组件,了解它在架构中的作用

Docker 一键部署:从零到运行

DocuSeal 最吸引人的地方之一就是部署极简。一条 Docker 命令就能启动完整的签署平台:

CODE
# 最简启动 — 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 三服务架构

生产部署推荐使用 Docker Compose,它包含三个服务:app(DocuSeal 主应用,端口 3000)、postgres(PostgreSQL 18 数据库,带健康检查)、caddy(自动 HTTPS 反向代理)。数据持久化通过 volume 挂载:./docuseal 映射应用数据,./pg_data 映射数据库数据。

代码翻译:Dockerfile 多阶段构建

DocuSeal 的 Dockerfile 使用了三个构建阶段。这是 多阶段构建 的经典实践——最终镜像只包含运行时必需的文件。

CODE
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 标准目录结构。了解目录布局,就能快速定位功能代码:

app/ 应用核心代码
models/ 数据模型:Template、Submission、Submitter、WebhookUrl 等 25+ 个模型
controllers/ 控制器:API 控制器、签署表单控制器、设置控制器等 80+ 个控制器
javascript/ 前端代码:Vue.js 组件、签名画板、模板构建器
jobs/ 异步任务:邮件发送、PDF 生成、Webhook 推送等 20+ 个 Job
mailers/ 邮件器:签署邀请、完成通知、拒绝通知、2FA 验证
views/ ERB 模板:HTML 页面和邮件模板
config/routes.rb 路由定义:Web 路由 + /api/ 命名空间路由
db/migrate/ 数据库迁移:从建表到字段变更的完整历史
docker-compose.yml 生产部署配置:app + PostgreSQL + Caddy
Dockerfile 多阶段 Docker 构建:download -> webpack -> app

签署流程的数据旅程

当用户发起一个签署请求时,数据在系统各个组件之间流转。想象一条流水线——PDF 模板是原材料,经过构建、发送、签署、验证,最终产出一份合法的电子签文档。

📄
模板构建
📦
发起签署
📧
邮件/签署
完成处理
点击"下一步"开始旅程

检验你的理解

DocuSeal 中,哪个模型定义了文档的字段配置和签署人角色?

使用 Docker Compose 生产部署时,app 服务依赖哪个服务的健康检查?

DocuSeal 的 Dockerfile 为什么要使用多阶段构建?

02

模板与表单构建

深入 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 对象,包含丰富的配置。左边是实际代码,右边是逐项解释:

CODE
{
  "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 / initials

签名和首字母缩写。使用 Signature Pad(签名画板)组件,支持手写签名绘制。签署后自动嵌入 PDF 的指定位置。这是 DocuSeal 最核心的字段类型。

📄
text / number / date

基础输入字段。text 是通用文本输入,number 限制数字输入(支持 min/max/step 验证),date 显示日期选择器。都支持默认值和只读模式。

checkbox / radio / dropdown

选择类字段。checkbox 是复选框(多选),radio 是单选按钮,dropdown 是下拉选择框。每个选项通过 options 数组定义,包含 value 和 uuid。

📎
file / image / stamp

文件和图像类字段。file 允许上传任意文件,image 限制为图片格式,stamp 是印章字段(类似签名但通常是公司公章或认证章)。

📊
cells

表格单元格字段。用于需要填写表格数据的场景,支持 cell_w(单元格宽度)配置。适合发票、报价单等结构化表格填写。

代码翻译:Template 模型定义

来看 Template 模型的 Ruby 源码。这个模型虽然不到 100 行,但承载了整个系统的核心数据结构:

CODE
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"(接收方)。模板中的"公司名称"字段绑定到披露方,"签字人"字段绑定到接收方。发起签署时,系统分别为两方创建签署链接,各方只能看到和填写自己的字段。

CODE
// 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 第二页的位置信息存储在哪里?

如何实现"甲方只能看到甲方的字段,乙方只能看到乙方的字段"?

03

提交与签署流程

追踪 Submission 从创建到完成的完整生命周期——状态机、签署表单、邮件通知和异步任务

Submission 生命周期:从创建到完成

Submission 的生命周期就像一个快递包裹——从"已下单"到"已送达",中间经历多个状态转换。DocuSeal 用 scope(查询范围)来定义这些状态。

1
创建(submission.created)

管理员通过 API 或界面发起签署。系统创建 Submission 记录,从模板复制 fields、schema、submitters 配置。source 字段记录来源:link(链接分享)、api(API 调用)、embed(嵌入式)、invite(邀请)、bulk(批量发送)。

2
分配签署人(Submitter 创建)

为每个角色创建 Submitter 记录,生成唯一的签署链接 slug。每个 Submitter 有独立的状态:awaiting -> sent -> opened -> completed/declined。metadata 字段可以存储自定义数据(如用户 ID、订单号)。

3
发送邀请(send_email)

SendSubmitterInvitationEmailJob 异步发送邀请邮件。邮件包含签署链接(/s/:slug),支持自定义邮件模板和回复地址。sent_at 字段记录发送时间。也可以通过 SMS 发送(Pro 功能)。

4
签署人填写(form.viewed -> form.started)

签署人打开链接,SubmitFormController#show 渲染签署表单。opened_at 记录打开时间。支持签署顺序控制(enforce_signing_order)和 2FA 验证(邮件验证码/手机验证码)。

5
完成签署(form.completed)

签署人提交表单,SubmitFormController#update 调用 Submitters::SubmitValues 处理。ProcessSubmitterCompletionJob 异步生成签署后的 PDF、嵌入电子签名、创建审计日志、触发 Webhook 通知。completed_at 记录完成时间。

Submitter 的五种状态

每个 Submitter 有独立的状态追踪。Submitter 模型的 status 方法是一个简单的状态机:

CODE
# 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 的核心安全设计:

CODE
# 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 异步任务队列来处理后续工作。

表单提交
📄
PDF 生成
🔗
通知推送
状态更新
点击"下一步"开始旅程

邮件通知系统:四种核心邮件

DocuSeal 的邮件系统(SubmitterMailer)负责签署流程中所有与用户的邮件交互。理解这四种核心邮件,就掌握了签署流程的用户体验设计:

📩
invitation_email(签署邀请)

发送给签署人的邀请邮件,包含签署链接 /s/:slug。支持自定义邮件标题和正文(request_email_subject/body),可使用变量替换(如 {{submitter.name}})。这是签署流程的起点。

completed_email(完成通知)

当签署人完成签署后,发送给模板创建者的通知邮件。附件自动包含签署后的 PDF 文档和审计日志(总计不超过 10MB)。支持自定义邮件模板。

declined_email(拒绝通知)

当签署人拒绝签署时,发送给创建者的通知邮件。reply_to 设置为签署人邮箱,方便创建者直接回复了解拒绝原因。

📦
documents_copy_email(文档副本)

签署完成后,发送给签署人的文档副本邮件。包含签署后的 PDF 和审计日志。确保签署人保留一份完整记录。

检验你的理解

Submitter 模型如何判断签署人的当前状态?

签署人打开签署链接时,需要登录吗?

哪个异步任务负责将签名嵌入到 PDF 文档中?

04

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 中携带它:

CODE
# 创建签署提交的 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 请求体结束

📚
API 权限控制

DocuSeal 使用 CanCanCan(ability.rb)进行权限控制。API 控制器通过 load_and_authorize_resource 宏自动加载资源并检查权限。AccessToken 模型关联到 User,User 又属于 Account,确保多租户数据隔离。

代码翻译:API 创建签署的核心逻辑

看 Api::SubmissionsController#create 的源码——这个方法是 API 集成的核心入口:

CODE
# 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 签名来保证请求的完整性和真实性:

CODE
# 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 安全最佳实践

验证 Webhook 请求的步骤:1) 从 HTTP Header 中取出 X-Webhook-Signature;2) 用 hmac_secret 对请求体做 HMAC-SHA256 签名;3) 比较两个签名是否一致。DocuSeal 还支持 Webhook 事件重发(resend)和事件日志查看,方便调试和故障排查。

检验你的理解

调用 DocuSeal API 时,认证 Token 应该放在哪里?

发起一次签署流程,应该调用哪个 API 端点?

Webhook 如何保证请求的真实性和完整性?

05

安全、签名验证与生产部署

深入 DocuSeal 的安全机制——电子签名原理、PDF 签名验证、审计日志和生产环境最佳实践

电子签名原理:从手写到 PDF 嵌入

DocuSeal 的电子签名流程涉及三个关键步骤:手写签名采集、PDF 签名嵌入和签名验证。理解这个流程,就理解了 DocuSeal 的核心技术价值。

1
签名采集(Signature Pad)

前端使用 Signature Pad(signature_pad JavaScript 库)采集手写签名。签署人在画板上用鼠标或手指绘制签名,数据以 SVG 路径点坐标的形式保存。同时 DocuSeal 支持 Dancing Script 字体生成"手写风格"的文本签名作为备选。

2
PDF 签名嵌入(ProcessSubmitterCompletionJob)

签署完成后,异步任务将签名图片渲染到 PDF 文档的指定位置(基于 fields 中 areas 定义的 x/y/w/h 坐标)。使用 Vips(高性能图像处理库)处理签名图片,确保清晰度和尺寸适配。

3
审计日志生成(EnsureAuditGenerated)

生成一份独立的审计日志 PDF,记录每个签署人的签署时间、IP 地址、User-Agent、地理位置等信息。审计日志附加到 Submission 的 audit_trail 附件中,与签署文档一起提供给管理员。

代码翻译:PDF 签名验证 API

DocuSeal 提供 PDF 签名验证 API(POST /api/tools/verify),使用 HexaPDF 库解析和验证 PDF 中的数字签名:

CODE
# 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)

用户认证基于 Devise(支持密码登录、邀请制注册)。权限控制使用 CanCanCan,定义了细粒度的 Ability 规则。API 认证使用 Access Token,Webhook 认证使用 HMAC-SHA256 签名。支持 MFA(TOTP 双因素认证)。

🔒
数据加密(ActiveRecord Encryption)

敏感字段使用 Rails 内置的 ActiveRecord Encryption 加密存储。包括 WebhookUrl 的 url、secret、hmac_secret,以及 EncryptedConfig 中的 SMTP 密码、SAML 证书等。密钥通过环境变量管理。

📩
2FA 签署验证(Email / SMS)

签署人打开链接时可要求邮件验证码或短信验证码验证身份。OTP 验证码通过 EmailVerificationCodes 生成,有时效性。在 SubmitFormController 中通过 require_email_2fa / require_phone_2fa 参数启用。

📄
审计追踪(Audit Trail)

每次签署完成自动生成审计日志 PDF。记录内容:签署人信息、IP 地址、User-Agent、签署时间、文档 SHA256 校验和。审计日志与签署文档一起归档,满足合规要求。

🛡
签署链接安全

每个签署链接使用 Base58 编码的 14 位随机 slug,不可猜测。支持链接过期设置(expire_at)。链接分享模板还支持额外的 2FA 验证。拒绝(declined)和已完成(completed)的链接不可再修改。

生产部署最佳实践

将 DocuSeal 部署到生产环境时,需要关注以下几个关键配置:

CODE
# 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:

CODE
<!-- 嵌入式签署 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

检验你的理解

POST /api/tools/verify 接口做了哪两层验证?

WebhookUrl 模型中的 url、secret、hmac_secret 字段在数据库中如何存储?

如何实现嵌入式签署(让用户在你的网站内完成签署)?