Rota 安全加固实战:如何用 JWT + bcrypt + 双限流构建四层登录防线(完整指南)
Rota 安全加固实战:如何用 JWT + bcrypt + 双限流构建四层登录防线(完整指南)
Rota 是一个自托管的高性能代理轮换平台,除了代理引擎,它还自带一个带实时图表的管理面板(Dashboard)。面板默认就是暴露在公网的,那么问题来了:登录接口如何防暴力破解?密码如何存储?会话如何校验?本文带你从源码视角拆解 Rota 安全加固的完整方案,覆盖 JWT 认证、bcrypt 密码哈希、登录暴力破解双重限流、WebSocket 来源校验 四大核心机制。
一、先看全景:Rota 的部署与信任边界
如上图所示,Rota 的所有管理流量(API 与 WebSocket)都收敛到同一入口,由 Caddy 反向代理统一转发。理解这个信任边界很重要,因为它直接决定了后面的限流与来源校验该怎么配置:
| 边界 | 安全机制 |
|---|---|
| 浏览器 → 面板 | JWT 会话校验 + CORS 白名单 |
| 公网 → 登录接口 | 双重限流(按 IP + 全局) |
| 数据库 → 凭证存储 | bcrypt 单向哈希 |
| WebSocket 升级 | Origin 来源校验(防 CSWSH) |
二、第一层:bcrypt 密码哈希——数据库里永远没有明文密码
Rota 的密码存储遵循最稳妥的原则:只存哈希,不存明文。
- 管理员账号在首次启动时通过
bcrypt.GenerateFromPassword哈希后写入数据库,逻辑见 admin_repository.go 的Seed方法; - 登录时用
bcrypt.CompareHashAndPassword做比对(admin_repository.go),数据库里查不到明文,即使库被拖走也无法直接还原密码; - 修改密码时强制校验旧密码、要求新密码至少 6 位,见 ChangePassword。
💡 bcrypt 的一个优点:自带随机盐且计算缓慢,天然抵抗彩虹表与暴力枚举。
更细致的优化出现在代理用户认证链路中:user_auth.go 实现了 TTL 60 秒的认证缓存——首次认证跑一次昂贵的 bcrypt 比对后,把已验证的哈希与链信息缓存起来,缓存过期才重新比对。用户改密码后哈希变化会导致缓存自动失效,安全与性能兼得。
另外,如果你没有显式设置 ROTA_ADMIN_PASSWORD,Rota 会随机生成一个强密码并只在首次启动日志中打印一次(config.go),而不是回退到众所周知的默认口令——这是很多自托管项目翻车的重灾区。
三、第二层:JWT 无状态认证——每个受保护接口都要过这道门
登录成功后,Rota 会签发一个 HS256 签名的 JWT,有效期 24 小时,逻辑在 auth_handler.go 的 generateToken。之后除登录外的所有 /api/v1 路由都必须携带 token(Authorization: Bearer <token>),由 middleware.go 中的 JWTMiddleware 统一拦截校验。
源码里有两处值得学习的防御细节:
- 限定签名算法,防 alg-confusion 攻击:解析 token 时强制要求
jwt.SigningMethodHMAC(middleware.go),拒绝攻击者把签名算法改成none或混淆算法伪造 token。 - 密钥持久化到数据库:JWT 签名密钥默认由 secret_repository.go 生成 256 位随机密钥并存入
system_secrets表。这样服务重启(发版、崩溃、docker restart)不会让所有管理员会话失效;如果你希望自行掌控密钥轮转,设置JWT_SECRET环境变量即可覆盖,见 server.go。
四、第三层:登录暴力破解双重限流——本项目最硬核的部分
这是标题里"双重限流"的主角,实现在 auth_ratelimit.go。Rota 在登录接口上同时挂了两套相互独立的限流机制:
1️⃣ 按 IP 限流:防单机字典爆破
- 在滑动窗口(默认 10 分钟)内,同一 IP 失败 10 次即触发封锁,封锁 30 分钟;
- 封锁期间任何登录请求直接返回
429 Too Many Requests,并附带Retry-After头。
2️⃣ 全局限流:防分布式撞库
- 统计所有 IP 在过去 60 秒内的全部登录尝试(成功失败都算);
- 超过阈值(默认 1000 次/分钟)时,登录接口对所有人锁定 1 分钟。
为什么要两套?单机限流挡不住"僵尸网络每 IP 试几次"的分布式撞库;全局锁则把整体尝试速率压到可控水平。两层叠加,攻击成本直线上升。
限流计数器保存在内存中,并有后台协程每 5 分钟清理过期条目(cleanup),防止长期运行内存泄漏。
一个关键细节:如何获取客户端真实 IP? 很多项目直接信任 X-Forwarded-For 头,攻击者伪造一个头就换"一个新 IP",按 IP 限流瞬间失效。Rota 的处理是(clientIP):
- 仅当显式设置
TRUST_PROXY_HEADERS=true(即确实在可信反向代理之后)才读取X-Forwarded-For/X-Real-IP; - 直暴露场景下默认只信
RemoteAddr,从根上堵住头伪造绕过。
限流参数一览(全部可用环境变量调整)
| 环境变量 | 默认值 | 含义 |
|---|---|---|
AUTH_IP_MAX_ATTEMPTS |
10 | 窗口内失败多少次触发 IP 封锁 |
AUTH_IP_WINDOW_MINUTES |
10 | 按 IP 计数的滑动窗口(分钟) |
AUTH_IP_BLOCK_MINUTES |
30 | 单个 IP 的封锁时长(分钟) |
AUTH_GLOBAL_MAX_PER_MINUTE |
1000 | 全局限速阈值(次/分钟) |
AUTH_GLOBAL_LOCKOUT_MINUTES |
1 | 触发全局限速后的锁定时长(分钟) |
TRUST_PROXY_HEADERS |
false | 是否信任代理头中的客户端 IP |
配置读取逻辑见 config.go。限流中间件挂载在登录路由上,见 server.go:r.With(s.authRL.Middleware()).Post("/api/v1/auth/login", ...)。
五、第四层:WebSocket 来源校验——挡住 CSWSH 攻击
Rota 面板的实时图表与日志流走的是 WebSocket(/ws/dashboard、/ws/logs),连接升级前除了校验 JWT,还要过一道 Origin 检查,实现在 websocket_handler.go 的 checkOrigin:
- ✅ 无
Origin头(CLI 等非浏览器客户端)→ 放行; - ✅ 同源请求(Origin 的 host 等于请求 Host)→ 放行;
- ✅ Origin 命中
CORS_ALLOWED_ORIGINS白名单(或*)→ 放行; - ❌ 其他跨域浏览器请求 → 拒绝升级。
这一步防的是 CSWSH(跨站 WebSocket 劫持):攻击者在自己的恶意网页里用受害者浏览器已登录的会话去连 WebSocket,从而窥探实时数据。默认 CORS 白名单是 *(config.go),生产环境建议显式收紧,例如 CORS_ALLOWED_ORIGINS=https://dashboard.example.com。
上面这个实时仪表盘的数据正是通过受 JWT + Origin 双重保护的 WebSocket 通道推送的——安全机制保的正是这块"实时大屏"。
六、安全加固部署清单:5 项必查
结合 config.go 与 Caddyfile 的默认部署方式,生产上线前建议逐项确认:
- 设置
ROTA_ADMIN_PASSWORD为强密码,并在面板中定期修改(Settings → Admin Account); - 设置
JWT_SECRET为 32 字节以上的随机值,便于自主轮转签名密钥; - 收紧
CORS_ALLOWED_ORIGINS,把默认的*换成你的面板域名; - 反向代理后设置
TRUST_PROXY_HEADERS=true,直暴露则保持默认false; - 按需调整
AUTH_*限流参数:公网暴露且账号敏感时,可把AUTH_IP_MAX_ATTEMPTS调低到 5 左右。
总结
Rota 的安全设计给出了一套可以直接抄作业的自托管面板加固范式:
- 存储层:bcrypt 单向哈希 + 随机盐,杜绝明文入库;
- 会话层:HS256 JWT + 算法白名单,密钥持久化防重启掉线;
- 接口层:按 IP + 全局双限流,且客户端 IP 不可伪造;
- 通道层:WebSocket Origin 校验,封死 CSWSH 侧路。
四层防线环环相扣,普通用户只需要管好上面清单里的 5 个环境变量,就能让 Rota 面板稳稳地暴露在公网而不用担心被"撞库"。
核心源码位置速查:auth_handler.go · auth_ratelimit.go · middleware.go · websocket_handler.go · config.go

