先说结论
Odoo 的 EDI Proxy 不是“帮你转发 HTTP 请求”的普通中转层。
从 /home/ubuntu/odoo-temp/addons/account_edi_proxy_client/models/account_edi_proxy_auth.py 与 account_edi_proxy_user.py 来看,它真正解决的是四件事:
- 把一个公司在某种 EDI 通道上的身份单独建模成 proxy user。
- 让每次请求都带上可验证的签名,而不是只靠 API key。
- 让代理侧返回的敏感文件不能被中途明文读取。
- 在数据库被复制、恢复备份或 token 失步时,主动阻断“两个库共用同一身份”。
所以很多人以为自己遇到的是“Peppol 接口不稳定”,实际上撞到的往往是代理身份一致性问题。
proxy user 不是账号别名,而是“某公司 + 某 EDI 类型 + 某环境”的独立身份
account_edi_proxy_client.user 上有几个很关键的字段:
id_clientcompany_idedi_identificationprivate_key_idrefresh_tokenproxy_typeedi_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,会:
- 先
_renew_token(); - 立刻
self.env.cr.commit(); - 再重新发一次请求。
这里的 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 支持两种签名:
hmacasymmetric
默认走 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、合规上传里那些“突然失步”的问题,第一反应不要只盯着业务模块。
先问自己这几个问题:
- 这是不是同一个 proxy user 被多个库共享了?
- refresh token 有没有被别处续签?
- 当前失败是普通连接错,还是 identity desync?
- 本地私钥、公钥、远端登记身份是否还是同一条链?
答对这四个问题,很多看似玄学的 EDI 故障,基本都会变得可解释。
DISCUSSION
评论区