跳到主要内容

远程访问认证

CountBot 的远程访问认证用于保护暴露在局域网或公网环境下的 HTTP API 与 WebSocket 接口。当前实现的核心代码位于 backend/modules/auth/middleware.pybackend/modules/auth/router.py

核心行为

当前认证逻辑有一个明确分界:

  • 本机直接回环访问:尽量放宽
  • 非本机远程访问:要求认证

中间件只对受保护的 /api/ 路由生效,公开健康检查与认证接口会被单独放行。

本地请求判定

本地访问判定不是只看 IP,还会额外检查代理头。

核心规则可以概括为:

LOCAL_IPS = {"127.0.0.1", "::1", "localhost"}

def is_direct_local_client(client_ip, header_keys):
if client_ip not in LOCAL_IPS:
return False
return not any(h in header_keys for h in {
"x-forwarded-for",
"x-real-ip",
"forwarded",
})

这意味着:

  • 只有真正的本机直连才算本地请求
  • 经过代理转发的请求不会因为源地址看起来像本机就被放行

首次初始化

首次设置管理员用户名和密码,默认只能在本机完成。

如果需要远程初始化,当前实现会依赖一个临时的 setup secret:

  • Header 名:x-setup-secret
  • 默认有效期:30 分钟
  • 最短 10 分钟,最长 120 分钟
  • 成功初始化后会立即清除

相关逻辑位于:

  • ensure_remote_setup_secret()
  • has_valid_remote_setup_secret()
  • clear_remote_setup_secret()

控制台会打印什么

当系统还没有设置管理员密码时,后端启动阶段会在控制台打印一条远程初始化提示。

真实代码位于 backend/app.py

logger.info(
f"远程首次初始化入口:将此路径拼接到上方 Network 地址后访问(有效期 {setup_secret_ttl_minutes} 分钟,初始化成功后立即失效) -> /setup/{setup_secret}"
)

同时,start_app.py 会在启动后打印本机和网络访问地址,例如:

Local:   http://localhost:8000
Network: http://192.168.1.23:8000

因此你真正需要关注的是两部分信息:

  1. Network: http://你的局域网IP:端口
  2. /setup/随机码

把它们拼起来,就是远程首次初始化入口。

远程初始化教程

如果你要从另一台设备完成首次初始化,可以按下面步骤操作。

第 1 步:确认服务对外监听

如果希望其他设备能访问,服务必须监听在 0.0.0.0 或可被局域网访问的地址。

start_app.py 里也会明确提示:

提示: 如需从其他设备访问,请设置 HOST=0.0.0.0

也就是说,远程初始化前至少要满足:

  • 当前机器和目标设备在同一网络
  • 启动时已设置 HOST=0.0.0.0
  • 控制台确实打印出了 Network: 地址

第 2 步:查看控制台随机码

当系统尚未初始化认证时,控制台会打印类似:

远程首次初始化入口:将此路径拼接到上方 Network 地址后访问(有效期 30 分钟,初始化成功后立即失效) -> /setup/AbCdEfGh

这里的 AbCdEfGh 就是随机码。

第 3 步:拼接完整访问地址

假设控制台同时打印了:

Network: http://192.168.1.23:8000

而随机码路径是:

/setup/AbCdEfGh

那么你需要在另一台设备浏览器里访问:

http://192.168.1.23:8000/setup/AbCdEfGh

第 4 步:在页面完成初始化

打开这个地址后,前端会进入首次初始化流程。你需要:

  1. 设置管理员用户名
  2. 设置管理员密码
  3. 提交初始化

初始化成功后:

  • 后端会保存用户名与密码哈希
  • setup secret 会立即失效
  • 浏览器会收到登录态 Cookie

第 5 步:后续访问改为正常登录

完成首次初始化后,后续就不再使用 /setup/随机码,而是直接通过正常登录流程访问系统。

如果再次访问旧的 setup 地址,按当前实现会返回 404 或被判定为无效入口。

几个容易踩坑的点

  • 只看到 Local: http://localhost:8000,看不到 Network::通常说明没有监听到 0.0.0.0
  • 随机码过期:setup secret 超时后会失效,最稳妥的做法是重启后端并重新查看控制台打印的新入口。
  • 初始化成功后旧链接失效:这是正常行为,不是异常。
  • 通过代理转发访问时被判定为非本机:这是当前安全设计的一部分。

认证成功后,后端会设置会话 Cookie:

  • Cookie 名称:CountBot_token
  • HttpOnly=true
  • SameSite=strict
  • 仅在 HTTPS 请求下设置 secure=true

同时,远程访问也支持 Authorization: Bearer <token> 方式传递令牌。

HTTP 中间件和 WebSocket 入口都会优先从以下位置取 token:

  1. Cookie CountBot_token
  2. Authorization: Bearer ... 请求头

认证接口

当前主要接口位于 /api/auth/

  • GET /api/auth/status
  • POST /api/auth/setup
  • POST /api/auth/login
  • POST /api/auth/logout
  • POST /api/auth/change-password

这些接口分别负责:

  • 查询是否已初始化、是否已登录、是否允许 setup
  • 首次设置账号密码
  • 登录并签发会话
  • 注销并删除 Cookie
  • 修改密码并撤销旧会话

限流与锁定

认证模块内置了简单的失败限流逻辑:

  • 统计窗口:15 分钟
  • 最大尝试次数:5 次
  • 锁定时长:15 分钟

该逻辑同时覆盖:

  • setup
  • login
  • change-password

WebSocket 校验

backend/app.py 中的 /ws/chat 在远程访问场景下同样会做认证校验。

当前行为是:

  1. 如果系统还没初始化认证,直接拒绝连接
  2. 优先读取 Cookie 中的 CountBot_token
  3. 没有 Cookie 时再尝试 Bearer Token
  4. token 无效则关闭连接

因此如果你前端页面已经能登录,但 WebSocket 仍然报认证问题,通常应优先检查:

  • Cookie 是否成功写入
  • 反向代理是否透传 Cookie / Authorization
  • 是否误把远程请求当成本地请求来理解

常见误区

  • “监听在 0.0.0.0 就一定能远程直接 setup”:不对,首次初始化默认仍然要求本机或合法 setup secret。
  • “只要是 localhost 来源就算本地”:不对,带代理头的请求不会被视为本机直连。
  • “WebSocket 不需要再认证”:不对,远程 WebSocket 同样走认证校验。

相关文件

文件说明
backend/modules/auth/middleware.py远程访问认证中间件
backend/modules/auth/router.pysetup、login、logout、改密接口
backend/modules/auth/utils.py密码哈希、会话签发与校验
backend/api/auth.py认证路由导出
backend/app.pyHTTP 中间件与 WebSocket 入口挂载