Actual 26.5.1/26.5.2 补丁发布解析:认证限流、自签名证书与 UUID 生成兼容性修复
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
本篇基于 Actual 仓库官方发布公告 2026-05-08-release-26-5-1.md 编写。v26.5.1 是一个聚焦的补丁版本,针对自托管场景下的三个真实痛点——认证接口限流误伤、桌面端连接自签名证书服务器失败、非 HTTPS 环境下 UUID 生成报错——给出了修复方案。阅读本文后,你将理解这三次修复的底层原因与源码实现位置,并掌握对应的升级方式与配置要点。
一、版本概览:26.5.1 与 26.5.2 功能完全相同
发布公告明确说明:v26.5.1 与 v26.5.2 在功能上是完全一致的(functionally identical)。额外发布 26.5.2 的唯一目的是解决 Windows Store 应用商店发布环节的问题,不包含任何代码层面的差异。
- Docker Tag:
26.5.1/26.5.2 - 发布时间:2026-05-08
- 类型:补丁版本(patch release),仅包含 bugfix,不引入新功能
本版本共合入三个修复项,全部围绕自托管(self-hosted)部署体验展开:
| PR | 修复内容 | 主要贡献者 |
|---|---|---|
| #7707 | 认证限流只统计失败的登录尝试 | @danielhopkins |
| #7713 | 修复桌面端自签名证书功能 | @MikesGlitch |
| #7734 | UUID 生成回退使用uuid库而非crypto.randomUUID() | @MatissJanis |
完整的版本历史与变更记录维护在 packages/docs/docs/releases.md 中,可对照查阅该版本前后的功能演进。
二、修复一:认证限流只统计失败的登录尝试(#7707)
2.1 问题背景
Actual 的同步服务器(sync-server)对认证接口做了速率限制(rate limiting),用于抵御暴力破解。此前的实现会对窗口期内的所有登录请求计数,包括密码正确、登录成功的请求。这带来的副作用是:在正常的多次登录、或客户端重试场景下,合法用户也可能被限流器拦截(返回 429),造成"被锁死"的体验。
#7707 的修复思路很直接:只把失败的登录尝试计入限流计数,成功的登录不再消耗配额。
2.2 源码实现
限流器定义在 packages/sync-server/src/app-account.js,基于express-rate-limit中间件:
const authRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window legacyHeaders: false, standardHeaders: true, skipSuccessfulRequests: true, message: { status: 'error', reason: 'too-many-requests' }, });关键参数说明:
skipSuccessfulRequests: true:本次修复的核心。开启后,响应状态码为 2xx 的请求(即登录成功的请求)不会计入限流计数,只有失败请求(如密码错误返回 4xx)才会累积;windowMs: 15 * 60 * 1000:时间窗口为 15 分钟;max: 5:每个窗口内允许的失败尝试上限为 5 次;standardHeaders: true/legacyHeaders: false:通过标准化的RateLimit-*响应头(而非废弃的X-RateLimit-*)把限流状态暴露给客户端;message:触发限流时返回的 JSON 响应体,reason固定为too-many-requests。
该限流器被挂载到两个未认证端点(见 app-account.js):
app.post('/bootstrap', authRateLimiter, async (req, res) => { ... }); app.post('/login', authRateLimiter, async (req, res) => { ... });/bootstrap用于首次初始化实例(仅可调用一次),/login用于密码登录。两者共享同一个限流器实例,因此限流计数在/login与/bootstrap之间是联动的。
2.3 测试用例验证
对应的行为测试位于 packages/sync-server/src/app-account.test.js,共覆盖三个场景:
- 连续失败触发 429:连续 5 次用错误密码调用
/login后,第 6 次请求返回429,响应体为{ status: 'error', reason: 'too-many-requests' }; - 跨端点共享限流:连续 5 次失败的
/login之后,再调用/bootstrap同样被限流器拦截(返回 429),验证了"同一限流器应用于多个端点"的设计; - 非认证端点不受影响:即使
/login已触发限流,GET /needs-bootstrap这类非认证端点仍返回 200。
此外测试的beforeEach中通过authRateLimiter.resetKey('127.0.0.1')重置限流计数,保证测试之间相互独立。
2.4 对使用者的影响
- 该修复对 API 行为的影响是透明的:限流窗口(15 分钟)、失败次数上限(5 次)、429 响应格式均未改变;
- 行为变化体现在:成功登录不再消耗配额,多设备、多客户端频繁登录时更不容易被误锁;
- 触发限流后,客户端应依据
RateLimit-*响应头中的Retry-After类信息等待窗口重置,而不是立即重试。
三、修复二:桌面端自签名证书支持(#7713)
3.1 问题背景
Actual 桌面应用(Electron)在连接使用**自签名证书(self-signed certificate)**的自托管服务器时,其底层fetch调用会因无法信任该证书而校验失败,导致同步、登录等请求被拒。此前该功能存在回归,本版本将其修复。
3.2 源码实现
修复逻辑位于 packages/desktop-electron/index.ts 的createBackgroundProcess函数中。桌面端启动后台服务器进程时,会先从global-store.json读取全局偏好:
async function loadGlobalPrefs() { let state: GlobalPrefsJson = {}; try { state = JSON.parse( fs.readFileSync( path.join(process.env.ACTUAL_DATA_DIR!, 'global-store.json'), 'utf8', ), ); } catch { logMessage('info', 'Could not load global state - using defaults'); state = {}; } return state; }随后,如果全局偏好中存在server-self-signed-cert,则将其注入后台进程的环境变量:
if (globalPrefs['server-self-signed-cert']) { envVariables = { ...envVariables, NODE_EXTRA_CA_CERTS: globalPrefs['server-self-signed-cert'], // add self signed cert to env - fetch can pick it up }; }机制说明:
server-self-signed-cert偏好值指向自签名证书的路径;- 通过设置
NODE_EXTRA_CA_CERTS环境变量,Node.js 的fetch/https层会将该证书追加到系统 CA 信任链中,从而能够正常验证自签名证书; - 该环境变量在 fork 后台 server 进程(
utilityProcess.fork(__dirname + '/server.js', ...))时一并传入,因此只影响桌面端启动的后台服务器进程,不影响系统其他进程; - 注释明确说明了这一设计意图:"add self signed cert to env - fetch can pick it up"。
3.3 使用与验证方式
- 在桌面端设置(或直接编辑
global-store.json)中配置server-self-signed-cert,指向你的自签名证书文件路径; - 重启桌面应用,使
createBackgroundProcess重新读取全局偏好并重建后台进程环境; - 连接使用该证书的自托管服务器,登录与同步请求即可正常完成。
从代码结构看,该修复同时保证了重启后依然生效:createBackgroundProcess每次都会重新调用loadGlobalPrefs(),确保最新配置被加载。
四、修复三:UUID 生成回退到uuid库(#7734)
4.1 问题背景
在更早的版本中,项目部分位置改用 Web 平台原生的crypto.randomUUID()生成 UUID。该 API 的可用性依赖安全上下文(secure context)——即仅在 HTTPS 或 localhost 环境下可用。对于通过纯 HTTP(非 HTTPS)地址访问的自部署实例,调用crypto.randomUUID()会直接抛出异常,导致会话标识等关键数据无法生成,功能不可用。
#7734 的处理方式是回退:将相关调用重新改为使用成熟的uuid库(uuidv4()),它不依赖安全上下文,在任何环境下都能稳定生成 UUID v4。
4.2 源码实现
以 packages/loot-core/src/platform/client/connection/index.ts 为例:
import { v4 as uuidv4 } from 'uuid';会话请求的标识符生成(index.ts)使用:
const id = uuidv4();uuid库在整个仓库中被广泛使用,包括:
- 平台层连接模块 packages/loot-core/src/platform/client/connection/index.electron.ts、packages/loot-core/src/platform/client/undo/index.ts、packages/loot-core/src/platform/server/sqlite/index.electron.ts;
- 服务端账户与同步逻辑,如 packages/loot-core/src/server/accounts/app.ts、packages/loot-core/src/server/accounts/sync.ts、packages/loot-core/src/server/cloud-storage.ts;
- 数据库迁移脚本,如 packages/loot-core/migrations/1722804019000_create_dashboard_table.js、packages/loot-core/migrations/1765518577215_multiple_dashboards.js;
- 测试与 mock 数据,如 packages/loot-core/src/mocks/budget.ts、packages/loot-core/src/mocks/index.ts。
由此可见,项目整体统一采用uuid库生成标识符,这既保证了跨平台(浏览器/Electron/Node)的一致性,也规避了安全上下文差异带来的兼容性问题。
4.3 对使用者的影响
- 修复后,通过 HTTP 访问的自部署实例(未启用 HTTPS 反代的环境)不会再因为
crypto.randomUUID不可用而出现会话/请求标识生成失败; - 生成结果仍是标准 UUID v4,对外接口与数据格式无任何变化,无需迁移既有数据。
五、升级指引
5.1 Docker 部署
自托管用户可直接拉取新标签进行升级:
docker pull actualbudget/actual-server:26.5.1 # 或等价标签 docker pull actualbudget/actual-server:26.5.2由于两个标签功能一致,选用任意一个即可;升级后建议通过浏览器访问管理页验证登录与同步正常。
5.2 版本兼容性说明
- 本次补丁不包含数据库迁移(migrations),因此升级过程不涉及数据结构变更,回退到 26.5.0 也是安全的(前提是期间没有写入依赖新格式的数据);
- 限流行为(15 分钟窗口 / 5 次失败上限)保持不变,仅计数口径发生变化,无需调整客户端重试逻辑;
- 若你此前因自签名证书问题在桌面端使用过
server-self-signed-cert偏好,升级后该偏好会继续按预期生效。
六、小结
v26.5.1/26.5.2 虽然是一个小补丁版本,但三个修复都精准地指向了自托管部署链路中的真实痛点:
- 认证限流(app-account.js)通过
skipSuccessfulRequests让成功登录不再消耗限流配额,配合 app-account.test.js 的三组用例验证了限流边界; - 自签名证书(desktop-electron/index.ts)通过将证书路径注入
NODE_EXTRA_CA_CERTS环境变量,恢复了桌面端连接私有服务器的能力; - UUID 生成(connection/index.ts)回退到
uuid库,彻底消除了 HTTP 非安全上下文下的运行时异常。
对于运行自托管 Actual 的用户,这是值得及时跟进的一个版本;对于二次开发或审计需求的读者,上述源码路径可以作为理解这三块逻辑的起点。
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考