# 共享出口匿名投票防刷修改与测试报告 - **时间**:2026-08-26 - **目标**:解决 Cloudflare + `trust_proxy=true` 场景下,局域网用户共享公网 IP 导致同一投票只能一人提交的问题。 - **策略**:签名 `vote_fp` 负责设备级一人一票;IP 仅用于高阈值突发风控,不再承担同一投票的一票封顶。 ## 修改内容 ### 1. 删除匿名投票的 IP 一票封顶 `route/vote/vote.go`: - 移除提交处理器内 `CountByIP(... 24h)` 且 `count >= 1` 即拒绝的逻辑。 - 匿名用户继续通过有效 HMAC `vote_fp` 与数据库唯一索引防止同一浏览器/设备重复投票。 - 无效或缺失 Cookie 仍会被重新签发,不会成为可信身份。 ### 2. 新增投票提交专用的身份优先限流 `internal/route/ratelimit.go`: 新增 `voteSubmitRateLimitMiddleware`,投票提交同时经过两层限制: - 登录用户:按 `user:` 独立限流,额度为常规参数的 3 倍。 - 匿名用户:按有效 `token:` 独立限流。 - 所有匿名请求:同时经过 `ip:` 高阈值兜底限流,默认是传入基础额度的 10 倍。 - 无效/缺失 Cookie 不会获得独立 token 限流身份,只能使用 IP 兜底。 - 触发限制时返回 HTTP 429,不进入实际投票提交处理器。 `internal/route/route.go` 已将 `/vote/{token}/submit` 从普通 IP 限流切换到该专用中间件。 ### 3. 校准 Cloudflare 客户端 IP 解析 `internal/shared/ip.go`: - `TrustProxy=true` 时优先读取 Cloudflare 官方 `CF-Connecting-IP`。 - 若该头不存在或非法,再回退到 `X-Real-IP`、`X-Forwarded-For`,最后使用 `RemoteAddr`。 - `TrustProxy=false` 时仍完全忽略这些请求头,只使用 `RemoteAddr`。 - 新增 Cloudflare 头优先级和不信任代理时忽略头部的测试。 ### 4. 更新设计文档 `anonymous-vote-design.md` 已同步新的防刷矩阵、token 流程和共享出口说明。 ## 验证结果 通过: ```text gofmt -d internal/route/ratelimit.go internal/route/route.go \ internal/shared/ip.go route/vote/vote.go # 无输出 go test -count=1 ./internal/shared ./route/vote ./internal/template \ ./templates ./internal/i18n ./internal/cache ./internal/security/autoban # 全部 PASS go vet ./route/vote ./internal/shared ./internal/template ./templates ./internal/i18n # 无输出 ``` 补充测试覆盖: - 同一共享 IP 下多个不同 token 彼此独立; - 同一 token 仍受独立限流; - IP 兜底允许多次正常请求,但能阻断异常突发; - Cloudflare `CF-Connecting-IP` 优先于其他代理头; - 未启用可信代理时伪造的代理头不会生效。 `go test ./internal/route` 当前仍无法编译完成,原因是项目既有的 `github.com/chai2010/webp@v1.4.0` 在本机缺少 CGO/WebP 编译符号,并非本次限流逻辑测试失败。 ## Cloudflare 部署注意事项 1. 源站防火墙应只允许 Cloudflare 官方 IP 段访问源站端口,否则攻击者可能直接访问源站并伪造 `CF-Connecting-IP`。 2. 保持 `trust_proxy=true` 的前提是源站确实位于可信反向代理之后。 3. 重要投票建议开启 Turnstile 或要求登录;匿名签名 Cookie 只能保证设备级防重,无法阻止清 Cookie、换浏览器或自动化客户端。 4. 当前共享 IP 兜底阈值为 200 次/分钟(投票路由传入基础值 20),可根据真实并发量调整。