框架深潜

Odoo EDI Proxy 为什么不是“后面有个中转服务”而已:refresh token、RSA 加密与数据库克隆边界讲透

很多团队在调 Peppol 或其他电子发票接入时,只看到前台按钮和后端报错,却没看清 Odoo 还有一层 account_edi_proxy_client 基础设施。真正容易出问题的,不是“代理服务挂了”,而是 refresh token 24 小时轮换、HMAC 与私钥双签名回退、数据库克隆后的 invalid_signature、以及代理端文件为何必须走公钥加密。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的 EDI Proxy 不是“帮你转发 HTTP 请求”的普通中转层。

/home/ubuntu/odoo-temp/addons/account_edi_proxy_client/models/account_edi_proxy_auth.pyaccount_edi_proxy_user.py 来看,它真正解决的是四件事:

  1. 把一个公司在某种 EDI 通道上的身份单独建模成 proxy user。
  2. 让每次请求都带上可验证的签名,而不是只靠 API key。
  3. 让代理侧返回的敏感文件不能被中途明文读取。
  4. 在数据库被复制、恢复备份或 token 失步时,主动阻断“两个库共用同一身份”。

所以很多人以为自己遇到的是“Peppol 接口不稳定”,实际上撞到的往往是代理身份一致性问题

proxy user 不是账号别名,而是“某公司 + 某 EDI 类型 + 某环境”的独立身份

account_edi_proxy_client.user 上有几个很关键的字段:

  • id_client
  • company_id
  • edi_identification
  • private_key_id
  • refresh_token
  • proxy_type
  • edi_mode

而且它有两个硬约束:

  • id_client 全局唯一;
  • 同一公司、同一 proxy_type、同一 edi_mode 只能有一个 active 用户。

这意味着 Odoo 没把代理访问设计成“公司配置里填一串密钥就完事”。 它把代理连接当成了可生命周期管理的远端身份

例如同一家公司:

  • Peppol 生产环境是一套身份;
  • Peppol 测试环境是另一套身份;
  • 未来别的 EDI proxy type 还会再分开。

这种建模方式的好处是:安全边界清晰,失步后也能单独失活,不会把所有接入混成一锅。

注册时先生成 RSA 私钥,不是为了“看起来高级”,而是为了后续文件解密

_register_proxy_user() 做的第一件事,不是发请求,而是先调用:

_generate_rsa_private_key(...)

然后把公钥放进 _get_iap_params() 发到代理端。

这背后的设计非常值得注意:

  • 代理端保存的是客户端公钥
  • Odoo 本地保存的是私钥
  • 后续代理返回的文件,不直接明文回传;
  • 而是用对称密钥加密文件,再把这个对称密钥用公钥加密。

最后 Odoo 侧在 _decrypt_data() 里先用私钥解开 symmetric key,再走 Fernet 解密正文。

也就是说,Proxy 的默认假设不是“链路里没人会看内容”,而是:

就算中转层存在,敏感载荷也应当只能被目标数据库解开。

这比很多团队想象得更“平台层”。

refresh token 不是长期凭据,而是 24 小时的短周期一致性锁

源码注释写得很直白:

refresh token expire after 24h to avoid that multiple database use the same credentials

这句话几乎就是整个模块的灵魂。

为什么不发一个长期 token,然后大家都一直用?

因为 Odoo 明确要防的是:

  • 生产库被完整克隆到测试;
  • 旧备份恢复成新实例;
  • 两个数据库同时认为自己拥有同一代理身份。

如果 token 永不过期,那么两个库就都能向代理侧继续收发数据,结果一定会乱。

所以 _make_request() 一旦收到 refresh_token_expired,会:

  1. _renew_token()
  2. 立刻 self.env.cr.commit()
  3. 再重新发一次请求。

这里的 commit 很关键。 它说明 token 轮换不是普通字段更新,而是不能随业务事务一起回滚的安全状态

为什么数据库克隆后最危险的不是 401,而是 invalid_signature

如果代理返回 invalid_signature,源码给出的提示非常直接:

  • 可能是另一个数据库也连着同一个 Odoo Access Point;
  • 常见原因就是数据库复制或恢复;
  • 建议联系支持处理。

这里最容易误解的一点是:

很多人会觉得“签名错了 = 密钥配错了”。 但在这个模块里,invalid_signature 往往代表的是身份主权冲突

因为请求签名不是单纯拿一个静态 secret 算出来,而是和:

  • id_client
  • 当前 refresh token
  • 请求 URL path
  • query 参数
  • JSON body
  • timestamp

一起拼出 payload 后再签名。

一旦另外一个数据库把 token 刷新走了,或者代理端认为当前签名链不再可信,本地这边就会突然“从昨天还能用,今天全部失效”。

这不是偶发 bug,而是系统主动保护你不要双写远端状态。

HMAC 是常态,非对称签名是兜底恢复通道

OdooEdiProxyAuth 支持两种签名:

  • hmac
  • asymmetric

默认走 HMAC,也就是用 refresh token 派生签名; 但注释里也写了:

fallback to resync the token in case of multiple database desynchronization problem

这意味着私钥不只是用来解密文件,它还承担了一个恢复职责:

  • 正常请求:靠 refresh token 快速签名;
  • 异常失步:可以切换到 private key 方案做重新同步。

这套设计很聪明,因为它把“高频认证”和“高可信恢复”拆开了:

  • 高频场景用 HMAC,成本低;
  • 失步场景用非对称签名,证明我是原始身份持有者。

no_such_user 为什么会直接把本地用户停用

_make_request() 里还有个很狠的处理:

如果代理返回 no_such_user,本地这条 proxy user 会被 active = False

这说明 Odoo 的思路不是“继续重试到成功”,而是:

远端都说这个身份不存在了,本地就不该装作它还活着。

甚至源码注释还特地提到一种情况:

  • 本地一直没交换数据;
  • 别的实例已经把相同 edi_identification 认领走。

这时强行保留本地 active,只会让用户继续在错误身份上操作。

demo 模式为什么被最后一道闸门硬拦

_make_request() 里在真正发请求前还有一层:

if self.edi_mode == 'demo':
    raise AccountEdiProxyError("block_demo_mode", "Can't access the proxy in demo mode")

这说明 demo 不是一个“低风险生产环境”,而是明确不允许直连真实代理。

也就是说,调用方如果没提前处理 demo mode,底层模型也会兜底拦住。 这类“最后一道闸门”在合规接口里很重要,因为演示环境最怕的就是误把测试动作打到真实通道。

真正该怎么理解这层代理

如果只从业务页面看,EDI Proxy 很像是远程 API 的一个包装器。

但从源码看,它更像下面这套组合:

  • 身份层:一个公司在某条 EDI 通道上的唯一远端用户;
  • 安全层:refresh token + HMAC + RSA 私钥;
  • 密文传输层:代理即使中转文件,也不应明文掌握最终内容;
  • 一致性层:防数据库克隆、防多实例共用同一凭据;
  • 失效层:远端否认身份时,本地主动停用,不假装还能继续用。

所以你以后再看到 Peppol、税务 EDI、合规上传里那些“突然失步”的问题,第一反应不要只盯着业务模块。

先问自己这几个问题:

  1. 这是不是同一个 proxy user 被多个库共享了?
  2. refresh token 有没有被别处续签?
  3. 当前失败是普通连接错,还是 identity desync?
  4. 本地私钥、公钥、远端登记身份是否还是同一条链?

答对这四个问题,很多看似玄学的 EDI 故障,基本都会变得可解释。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。