构建能够经受审计考验的会计 API
一份实用指南,涵盖金融 API 在获得荷兰企业信任并交托账目之前所需的安全性与审计控制。
一个对支付写入操作返回 200 OK 的会计 API 几乎没有向你透露任何实质信息。它没有确认总账是否记录了该分录、操作主体是否有权进行此操作、金额是否通过了验证,也没有确认是否有任何具授权资格的人员对此进行了批准。对于荷兰的企业而言,税务机关要求记录必须具备可重建性并保留七年,而“请求成功”与“账目正确”之间的这种鸿沟,正是审计出现问题的地方。
审计就绪(Audit-ready)是一项目标为运营设计的目标,而非一张证书。这意味着 API 能够维护一条完整的证据链:经过身份验证的主体到经授权的操作、到经过验证的状态变更、到审批、到总账结果,再到防篡改的审计证据。下文讨论的所有内容,都是为了刻意构建这条证据链,而不是假设身份验证和 TLS 已经覆盖了这些需求。
是什么让会计 API 做到“审计就绪”
审计师在重建交易时,需要回答一连串特定的问题。是谁发起的?他们被允许这样做吗?该变更有通过会计验证吗?谁批准的?总账记录了什么?该记录在事后是否可能被修改而不留下痕迹?
如果你的 API 无法仅凭自身的日志和数据模型回答上述全部六个问题,那么无论代码看起来多么整洁,它都算不上审计就绪。大多数 API 能很好地回答第一个问题,而对后续问题的处理则差强人意。
缩小这一差距的控制措施可以归纳为几大类:明确的会计状态、分层授权、有范围限制的身份验证、幂等写入、服务端验证、职责分离以及防篡改日志。它们相互强化。薄弱的授权会破坏强有力的审计日志,因为你将无法再信任被记录的操作主体确实有权执行该操作。
在设计端点之前先对会计状态建模
最常见的设计错误是将每个财务记录都视为一个带有 create(创建)、read(读取)、update(更新)、delete(删除)功能的自由可编辑对象。会计并非如此运作,如果过账的日记账分录可以被静默覆盖,审计师会立刻察觉。
每个具有财务重要性的资源都需要一个明确的状态机。日记账分录或支付通常会经历以下生命周期:
draft(草稿) → submitted(已提交) → approved(已批准) → posted(已过账) → settled(已结算)
更正操作不会倒拨这一路径。错误的已过账分录会通过关联的冲销(reversal)或抵消调整来处理。原始分录保持原样。
一套切实可行的规则如下:
- 草稿可由经授权的用户进行编辑。
- 已提交的记录仅能由具相应权限的角色进行编辑,或退回为草稿。
- 已批准的记录需要受到控制的修正以及重新批准。
- 已过账的记录无法被覆盖或删除,只能冲销或调整。
- 针对已关闭期间的变更需要提升权限、陈述理由并记录审计事件。
任何试图进行非法转换的行为,例如编辑已过账分录,都应当返回一个稳定的错误并同时写入审计记录。失败的尝试同样也是证据。
这就是不可变的复式记账总账(double-entry ledger)大显身手的地方。Nordlet 的总账在设计上确保每笔交易都作为平衡且永久的分录落库,并在提交时通过数据库触发器强制保持平衡,更正则通过冲销而非重写来进行。这种约束在开发时可能会让人觉得受限,但在审计期间,它会成为赢得会计师信任的关键。
授权每一个请求,而不只是验证身份
身份验证证明了调用者是谁。它根本没有说明该调用者是否有权操作特定的发票、日记账或法律实体。OWASP API 安全项目将越权访问(Broken Object-Level Authorization)列在风险榜首,正是因为 API 暴露了对象标识符,在缺乏检查时极易导致跨租户访问。
对财务 API 的每个请求都应当结合多种因素进行授权,而不仅仅是验证一个令牌:
- 经过身份验证的主体
- 客户端应用程序或服务账户
- 租户
- 法律实体
- 资源 ID
- 请求的操作
- 当前资源状态
- 金额或交易阈值
两条实用的规则比其他规则更为重要。首先,从经过身份验证的身份中推导租户和用户上下文,并拒绝任何与之冲突的客户端提供的租户或角色声明。调用者绝不应该能够自行断言他们代表哪家公司。其次,采用默认拒绝(deny-by-default)原则。如果没有策略明确授予某项操作权限,则该操作失败。
双向测试都必不可少。水平访问(一个租户访问另一个租户的数据)和垂直访问(普通用户访问审批、过账或导出功能)。这两者都很常见,并且在会计数据中都是灾难性的。
Nordlet 的角色模型在同一账户下的多家公司之间支持所有者、管理员、会计师、经理、开发人员和查看者角色,其存在是为了使这种职责分离具有强制性,而不仅仅是停留在愿景层面。拥有创建草稿发票权限的开发人员角色,不应当具备释放支付的能力。
将令牌的范围限定在业务能力,而非 CRUD 动词
对于委派访问,请遵循当前的 OAuth 2.0 安全指南,而不是使用可重复使用的密码或广泛的静态 API 密钥。RFC 9700 是当前的最佳实践(Best Current Practice),并对客户端和授权服务器提出了明确要求:验证重定向 URI、在适用的情况下使用 PKKE、进行发行方和受众检查、缩短访问令牌寿命,以及通过吊销机制实现刷新令牌轮换。
范围(scope)划分的决策最能体现会计领域的专业思考。诸如 read(读取)和 write(写入)之类的通用范围对于职责分离几乎毫无用处。范围应当映射到具体的能力:
journal:create(创建日记账)和journal:approve(批准日记账)是两个不同的范围。- 创建支付指令并不授予释放支付的权限。
- 导出权限与读取权限截然不同,因为批量导出是数据泄露的途径。
明确界定特定产品在该标尺上的位置。Nordlet 的 API 密钥带有诸如 sales:read 和 sales:write 等按模块划分的范围,并在每次操作时进行检查。这区分了不同的模块以及读写操作,但尚未细化到将创建与批准分离;如果今天你需要更细粒度的拆分,请在调用前于你自己的服务中强制执行。
对于高价值操作,值得评估 FAPI 2.0 安全配置文件是否适用。它专为保护敏感财务数据的 API 而设计,比基准 OAuth 提供更强的安全保证。大多数中小企业会计集成不需要完整的配置文件,但支付释放和银行连接流程是需要考虑它的地方。
Nordlet 通过一次性电子邮件链接提供的无密码登录功能,消除了人类用户的可重用密码问题,从而减少了一个需要轮换、泄露或遭受网络钓鱼的凭据。
确保财务写入具备幂等性
网络会在请求进行中途失效。客户端发送支付指令,连接在响应到达前中断,随后客户端进行重试。如果没有保护措施,你现在就会产生两笔相同的支付。
幂等键(Idempotency keys)解决了这个问题。客户端在每次写入时发送一个唯一键;服务器记录该键及其结果。使用相同键的重试将返回原始结果,而不会创建第二个分录。Nordlet 在每个会产生状态变更的调用上都接受 Idempotency-Key 请求头,在以相同有效负载重试时回放存储的响应,并拒绝使用更改后的有效负载但相同键的请求。任何跳过此功能的会计 API 最终都会产生重复的总账分录,需要人工手动去理清。
将此与服务端验证相结合。切勿信任客户端去检查借贷是否相等、增值税税率对于该国家和期间是否有效、或者账户是否存在。验证在服务端运行,发生在状态转换之前,被拒绝的写入会产生稳定的错误和审计记录。
防篡改审计追踪与荷兰数据保留
审计日志是证据层,它与普通的业务数据有着不同的要求。它应当记录授权决策、策略版本、主体、对象、操作、先前状态与新状态、以及原因代码,但绝不能记录密码或完整的支付凭据。
最关键的属性是防篡改性。如果管理员可以悄悄编辑或删除审计记录,那么日志就无法证明任何事情。追加写入存储(append-only storage)、密码学链条(cryptographic chaining)或一次写入多次读取(WORM)保留都可以解决这个问题。核心在于任何修改都必须是可检测的。
数据保留需要有其自身的策略,与业务记录的删除相分离。在荷兰,行政记录的一般保留期限为七年,而不动产相关记录则为十年。你的常规数据删除和 GDPR 擦除工作流绝不能销毁法律保留规则仍涵盖的记录或审计证据。独立定义审计证据的法律保留、归档和删除规则,并将其映射到你运营所在的司法管辖区。
期间锁定(Period locking)在会计层面对此提供了支持。一旦某个月份关闭,其中的分录在没有提升权限和记录在案的理由的情况下就无法更改。Nordlet 强制执行期间锁定,以确保已关闭的月份保持关闭状态,拒绝任何在锁定期间内标明日期的过账(即使通过 API 提交也是如此),这是荷兰审计师首先检查的项目之一。
保护边缘:Webhooks、导出与集成
核心 API 通常是系统中防御最严密的部门,而边缘往往会出现泄露。
Webhooks 存在伪造请求的风险。诸如 sale_invoice.paid 之类的事件应当进行签名,以便接收方验证其确实来自于你;同时,接收方应当将交付视为“至少一次”(at-least-once)并使用事件 ID 进行去重。Nordlet 使用原始主体的 HMAC 签名对每个 Webhook 交付进行签名,并以指数退避算法重试失败的交付,因此接收方必须验证签名并通过事件 ID 去重,而不能假设“刚好一次”(exactly-once)交付。
导出是批量数据泄露的途径。以 XLSX、PDF 或 JSON 格式导出的报表会在一次操作中拉取大量财务数据,因此它们需要自身的权限、速率限制和审计日志。你所使用的第三方集成应当被视为不可信输入,其验证方式应与验证直接客户端请求的方式相同。
荷兰会计 API 的审计就绪检查清单
在声称集成已具备审计就绪之前,请使用以下清单:
- 每个财务资源上都有明确的状态机;已过账的分录无法被覆盖
- 每个请求都具备对象级和法律实体授权,采用默认拒绝原则
- 租户和角色上下文从令牌中推导,而非客户端声明
- 符合 RFC 9700 标准的 OAuth 2.0;基于能力的范围;分离创建与批准
- 在角色中强制执行职责分离,而不仅仅是文档说明
- 所有财务写入均包含幂等键
- 在状态转换之前进行服务端会计与增值税验证
- 附带原因代码的追加写入、防篡改审计日志
- 映射到荷兰期间(七年,不动产为十年)的保留和法律保留规则
- 针对已关闭月份的期间锁定
- 带有去重功能的签名 Webhooks;具备范围限制、速率限制和日志记录的导出
我们会优先构建什么
如果明天我们要将会计功能嵌入到荷兰的某个平台中,我们会在编写任何业务端点之前,从状态模型和对象级授权开始着手。这两项决策制约着后续的一切,而将它们事后补救到一个将总账分录视为可编辑行的系统中,将会极其痛苦。幂等性和审计日志紧随其后,因为在早期添加的成本较低,而在数据开始流动之后再添加则成本高昂。
其余部分(如带范围的令牌、已签名的 Webhooks、期间锁定)在基础稳固后将更容易实现。所有这些都可以在接触生产数据之前,针对沙盒公司(sandbox company)进行演练。
FAQ
审计就绪的 API 是否意味着企业已经合规?
不是,明确这一点非常重要。审计就绪是系统的一项设计属性:总账、控制措施和证据链的构建使得审计能够顺利通过。对特定税务、会计或安全框架的合规性评估是一项独立的工作,它将这些控制措施映射到具名的标准之上。一个构建良好的 API 会使合规工作变得更轻松,但它并不能取代合规本身。
在荷兰,会计记录需要保留多长时间?
一般义务是行政记录保留七年,涉及不动产的记录则延长至十年。建立你的常规删除工作流无法覆盖的保留和法律保留规则。
静态 API 密钥对于金融集成来说足够吗?
对于范围狭窄的服务器到服务器自动化而言,这或许行得通,但可重用的静态密钥很难轮换,且在泄露时极其危险。对于委派访问以及任何面向人类的流程,采用具备短期、基于能力范围令牌的 OAuth 2.0 是更好的选择。
为什么幂等性对会计而言如此重要?
因为在大多数系统中,重复写入只是一个小麻烦,而在会计中,重复写入则是账目上的一个错误数字,必须有人去查找并将其冲销。幂等键使重试变得安全,而不是充满风险。
延伸阅读
- OWASP API 安全项目了解当前的 API 风险类别
- RFC 9700:OAuth 2.0 安全最佳当前实践了解委派访问要求
- 荷兰开发人员的实时欧盟增值税指南了解荷兰税务全貌的实际情况
- API 约定了解范围、幂等性、错误和 Webhook 签名