# 登录鉴权扩展接口文档(auth_login) ## 账号、注册与单点总开关(2026-10-05) 管理员设置 `GET/PUT /api/system-settings/authentication`,请求和响应是完整对象: ```json {"password_login_enabled":false,"registration_enabled":false,"sso_enabled":true,"single_session_enabled":false} ``` 密码登录页面、自助注册与同账号单处登录默认关闭,外部系统免登默认开启,已有管理员保存的设置不变。至少保留一种登录方式;自助注册要求密码登录开启。无个人密码的存量用户使用原共享口令,未配置则使用 `123ewq`,因此关闭外部系统免登无需先给管理员设个人密码。未登录页面通过 `GET /api/v1/auth/options` 读取同四字段和 `needs_setup`(当前没有管理员),响应禁止缓存。这里只公开开关,不公开共享密码、个人密码或上游密钥。 | 接口 | 请求 | 成功响应 | | --- | --- | --- | | `POST /api/v1/auth/login/local` | form-urlencoded `username`、`password`;用户名或邮箱 | `UsernameLoginResponse` + HttpOnly Cookie | | `POST /api/v1/auth/register` | JSON `{username,password}`;兼容 `{email,password}`,两种账号字段只传一项 | `201` + `UsernameLoginResponse`,自动登录普通用户 | | `POST /api/v1/auth/initialize` | JSON `{email,password}`;只允许系统没有管理员时 | `201` + `UsernameLoginResponse`,创建首任管理员 | | `GET /api/v1/auth/me` | 当前会话 | `id,email,system_role,needs_setup,has_password,requires_current_password,uses_default_password` | | `POST /api/v1/auth/change-password` | JSON `{current_password,new_password}` | `message,access_token,token_type,expires_in` + 更新 Cookie | | `PUT /api/v1/auth/users/{id}/login-password` | 管理员 JSON `{password}`;也可用 `/reset-password` 别名 | 用户列表项;重置自己时附 `access_token,token_type,expires_in` | `UsernameLoginResponse` 为 `access_token,token_type,expires_in,user_id,email,system_role,needs_setup,created,username`。前端收到改密/本人重置成功响应后必须同步替换本地 Bearer,否则会被旧版本 token 拒绝。新增 `auth_usernames` 表用数据库唯一键保护并发新建账号的用户名;新旧域名注册同名也只能成功一次。ORM 自动创建该表,历史用户在新建前按邮箱 local-part 检查。用户名单点在初次查询后发生并发注册/改密时,会再次校验最终账号凭据后才签发会话。 新密码为 8–255 字符,拒绝常见弱密码和首尾空格;所有用户都必须提供 `current_password`,无个人凭据时用原共享口令(未配置则 `123ewq`)首次设置,不能留空。用户名接受字母、数字、`.`、`_`、`-`,首尾须为字母或数字,也接受有效邮箱。用户名不区分大小写;已存在的相同邮箱 local-part 不得通过换域名重复注册或覆盖。重复返回 `409`,校验失败 `422`,关闭方式或越权返回 `403`,错误密码 `401`(改密码的错误当前密码为 `400`),登录限速 `429`。新注册沿用现有黑名单语义默认放行;已有封禁账号仍被拦截。 新密码与「登录口令」统一在 `password_hash`、`login_password` 保存同一版本化散列;两种登录入口接受同一密码。历史明文个人口令优先于旧 `password_hash`,可直接用于密码登录、URL 口令登录或验证改密,修改后即改存散列。管理员列表仅对历史明文保留旧兼容,任何散列均不返回,新增 `has_login_password` 表示是否已设置。管理员设置/重置不回显密码;已有密码不得清空。复制新登录链接时使用本次输入,之后需重新输入或重置。修改/重置原子递增 `token_version`,此前全部会话与旧密码失效;并发过期修改返回 `409`,普通审批不能覆写凭据或复活旧会话。 `sso_enabled=false` 后,用户名直登、上游 token、OAuth 及 `/api/parallel-agents/auth/token` 都拒绝登录。并行面板兼容入口现复用标准用户名登录的密码、限速和封禁校验,可附 `?password=`。关闭自助注册不影响已启用的单点对外部身份自动建普通用户。用户名含 `admin` 不再授予或提升管理员权限;历史已经是管理员的账号不变。首任管理员初始化使用独立单例 claim 与用户插入的同一数据库事务,跨工作进程并发也最多成功一次。部署无需人工增加列,ORM 启动自动创建 `auth_initialization` 表;管理员恢复仍使用 `python -m app.gateway.auth.reset_admin`,同样同步两套凭据。 启用 `password_login_enabled` 后,用户名/并行面板登录也必须验证个人密码或原共享口令(未配置 `auth_login.username_login.password` 时为 `123ewq`);个人凭据存在时共享口令不能替代。JWT 的 `auth_method=password/passphrase/sso` 仅由实际完成校验的登录路径显式写入。Gateway 与 LangGraph 同时拒绝缺少标记的历史/免密会话,要求重新登录;已验证的上游 SSO 仍受独立 `sso_enabled` 控制。开发环境 `DEER_FLOW_AUTH_DISABLED` 也不能绕过已开启的密码登录或单处登录限制。关闭密码登录时保留旧用户名单点及共享口令门行为;`create_access_token` 不会自动给未经校验的调用添加可信标记。 「单点登录(同账号仅一处在线)」由独立 `single_session_enabled` 控制,默认关闭;`sso_enabled` 仅表示既有外部单点/免登入口,两个功能分开。开启单处登录后,每次成功的账号密码、用户名口令、上游 token、并行面板、注册或初始化登录,都在数据库内原子递增该账号 `token_version` 后签新 JWT,旧会话立即返回 401,前端轮询 `/me` 后退出旧页面。失败登录不会踢人,关闭时允许多处会话并存;修改/重置密码仍始终撤销旧会话。并发登录通过同一行的原子递增保证最终仅最高版本会话有效;事务还比较已验证的密码/口令与权限快照,避免并发重置密码后旧凭据借新登录版本恢复。 在原有登录之上新增两种自定义登录模式,均由 `config.yaml` 的 `auth_login` 段控制。 ## 配置 ```yaml auth_login: # 功能一:用户名直登口令门 username_login: require_password: false # true 时 /login/username 必须带 ?password= password: "" # 共享口令;留空默认 123ewq # 功能二:token 换登录 token_login: enabled: false # 总开关 token_info_url: "https://ch1.b.uat.4.cn/consumer/login/getTokenInfo" service_authorization: "Bearer admin" # 调上游接口的鉴权头 timeout_seconds: 10 verify_ssl: false # 上游 HTTPS 证书校验;UAT/内网自签证书需置 false,生产置 true ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并强制校验 ``` > **HTTPS 注意**:`token_info_url` 是 HTTPS。若上游用自签名 / 私有 CA 证书(UAT、内网常见),`verify_ssl: true` 时 httpx 的 TLS 握手会失败,导致 token 登录**恒定 401**。此时应: > - 临时/内网:`verify_ssl: false`(跳过证书校验); > - 正规做法:把私有 CA 证书导出为 PEM,配 `ca_cert_path: /path/to/ca.pem`(优先于 `verify_ssl`,仍做校验); > - 生产对外域名(有效证书):保持 `verify_ssl: true`。 > > 握手失败时后端日志会打印具体异常类型(如 `SSLError`)与当前 `verify` 取值,便于排查。 配置模型:`packages/harness/deerflow/config/login_config.py`。修改 `config.yaml` 后无需重启(`get_app_config()` 按 mtime 自动重载)。 --- ## 功能一:用户名直登 + 口令门 此前提为系统设置的 `sso_enabled=true`。账号已设置个人密码/口令时,始终校验该凭据;未设置个人凭据时,开启账号密码登录或 `require_password` 任一开关都会要求共享口令(未配置则为 `123ewq`)。 ### 请求 ``` POST /api/v1/auth/login/username[?password=<共享口令>] Content-Type: application/json { "username": "alice" } ``` - `?password=`:账号已有个人密码/口令时必须匹配个人凭据;否则仅当 `require_password=true` 时需要匹配共享口令。 - 共享口令仅适用于没有个人凭据的账号,不影响目标账号选择。 - 校验失败按 IP 计入登录限速(5 次/5 分钟锁定),与本地密码登录共用同一限速器。 ### 响应 成功 `200`: ```json { "access_token": "", "token_type": "bearer", "expires_in": 604800, "user_id": "", "email": "alice@cm.com", "system_role": "user", "needs_setup": false, "created": false } ``` 同时种下 `access_token` HttpOnly Cookie。 失败: | 状态码 | 场景 | |--------|------| | `401` | `require_password=true` 但口令缺失或不匹配 | | `429` | 触发登录限速 | ### 前端入口 访问 `/login/<用户名>?password=<口令>`。[LoginPage.tsx](../../../frontend-web/src/pages/LoginPage.tsx) 从 `?password=` 读取并透传给 [`loginByUsername`](../../../frontend-web/src/core/auth/api.ts)。 --- ## 功能二:token 换登录 前端用 URL 把上游 token 传进来,后端调上游接口换出用户名再登录。 ### 请求 ``` POST /api/v1/auth/login/token?token=<上游access_token> ``` 后端据此向 `token_info_url` 发起(与下方 curl 对齐):**前端传入的 token 直接拼成 `Authorization: Bearer ` 头**,上游从该头读取令牌(不再用 cookie,也不再用静态 `service_authorization`)。 ```bash curl 'https://ch1.b.uat.4.cn/consumer/login/getTokenInfo' \ -X POST \ -H 'authorization: Bearer <上游access_token>' \ -H 'content-type: application/x-www-form-urlencoded' ``` 上游返回示例: ```json { "state": "200", "msg": "操作成功!", "data": { "username": "b1admin", "...": "..." } } ``` 后端取 `data.username` → 经 `_username_to_email` 映射 → 库内查找(含跨域名 local-part 兜底)→ **查不到则自动注册** → 发本系统 JWT 并种 Cookie。 ### 响应 成功 `200`:结构同功能一的 `UsernameLoginResponse`(首次自动注册时 `created=true`)。 失败: | 状态码 | 场景 | |--------|------| | `403` | `token_login.enabled=false` | | `401` | 上游调用失败 / HTTP 非 200 / `state != "200"` / 未取到 username | | `429` | 触发登录限速 | | `500` | `token_info_url` 未配置 | ### 前端入口 访问 `/login?authToken=<上游token>`(用 `authToken` 而非 `token`,以避开既有的 `tools_token` 参数 `?token=`)。LoginPage 命中 `authToken` 时走 [`loginByToken`](../../../frontend-web/src/core/auth/api.ts),不再调用户名直登。 --- ## 安全说明 - 两个端点都在 `auth_middleware._PUBLIC_EXACT_PATHS` 白名单内(免会话鉴权,自身完成鉴权)。 - 口令门是**最低限度**的共享密钥防护,适合内网/受控环境;非每用户独立密码。 - token 换登录信任上游 `getTokenInfo` 的鉴权结论,务必保证 `token_info_url` 配置正确且走内网/HTTPS。前端传入的 token 会直接作为 `Authorization: Bearer ` 发给上游,因此该 token 必须来自可信入口。(`service_authorization` 配置项已不再参与请求,保留仅为兼容,后续可移除。) ## 相关代码 - 后端端点 / 助手:`app/gateway/routers/auth.py`(`login_by_username`、`login_by_token`、`_get_or_create_user_by_username`、`_fetch_username_from_token`) - 白名单:`app/gateway/auth_middleware.py` - 配置模型:`packages/harness/deerflow/config/login_config.py`(挂载于 `AppConfig.auth_login`) - 前端:`frontend-web/src/core/auth/api.ts`、`frontend-web/src/pages/LoginPage.tsx` - 测试:`tests/test_login_config.py`、`tests/test_auth_login_modes.py`