增加用户名
This commit is contained in:
@@ -20,9 +20,10 @@ HUB_DISABLED_IDS=1
|
||||
# true=允许 RFC1918 私网访问中控页面;false=仅 127.0.0.1
|
||||
HUB_TRUST_LAN=true
|
||||
|
||||
# 中控 Web 登录密码(非空即启用;反代到公网时务必设置)
|
||||
# 中控 Web 登录(密码非空即启用;反代到公网时务必设置用户名+密码)
|
||||
# HUB_USERNAME=admin
|
||||
# HUB_PASSWORD=your-strong-password-here
|
||||
# 会话签名密钥(建议单独随机串;未设则回退为 HUB_PASSWORD)
|
||||
# 会话签名密钥(建议单独随机串;未设则用用户名+密码拼接)
|
||||
# HUB_SESSION_SECRET=another-long-random-string
|
||||
# HTTPS 反代时设为 true,Cookie 仅通过加密连接传输
|
||||
# HUB_COOKIE_SECURE=true
|
||||
|
||||
@@ -28,7 +28,8 @@ from hub_web_auth import (
|
||||
is_public_path,
|
||||
password_required,
|
||||
validate_session_token,
|
||||
verify_password,
|
||||
expected_username,
|
||||
verify_credentials,
|
||||
)
|
||||
from url_public import browser_url, default_review_url, public_origin
|
||||
|
||||
@@ -142,6 +143,7 @@ def _login_page():
|
||||
|
||||
|
||||
class LoginBody(BaseModel):
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
|
||||
|
||||
@@ -149,16 +151,20 @@ class LoginBody(BaseModel):
|
||||
def api_auth_status(request: Request):
|
||||
required = password_required()
|
||||
logged_in = not required or validate_session_token(request.cookies.get(SESSION_COOKIE))
|
||||
return {"required": required, "logged_in": logged_in}
|
||||
return {
|
||||
"required": required,
|
||||
"logged_in": logged_in,
|
||||
"username_hint": expected_username() if required else None,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
def api_auth_login(body: LoginBody):
|
||||
if not password_required():
|
||||
return {"ok": True, "auth_disabled": True}
|
||||
if not verify_password(body.password):
|
||||
raise HTTPException(status_code=401, detail="密码错误")
|
||||
token = create_session_token()
|
||||
if not verify_credentials(body.username, body.password):
|
||||
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||||
token = create_session_token(body.username)
|
||||
resp = JSONResponse({"ok": True})
|
||||
resp.set_cookie(
|
||||
SESSION_COOKIE,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""中控 Web 登录:HUB_PASSWORD 非空时启用会话 Cookie。"""
|
||||
"""中控 Web 登录:HUB_USERNAME + HUB_PASSWORD 配置后启用会话 Cookie。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,23 +12,43 @@ from secrets import compare_digest
|
||||
|
||||
SESSION_COOKIE = "hub_sess"
|
||||
SESSION_MAX_AGE_SEC = max(3600, int(os.getenv("HUB_SESSION_DAYS", "7")) * 86400)
|
||||
DEFAULT_USERNAME = "admin"
|
||||
|
||||
|
||||
def _env_username() -> str:
|
||||
return (os.getenv("HUB_USERNAME") or "").strip()
|
||||
|
||||
|
||||
def _env_password() -> str:
|
||||
return (os.getenv("HUB_PASSWORD") or "").strip()
|
||||
|
||||
|
||||
def password_required() -> bool:
|
||||
return bool((os.getenv("HUB_PASSWORD") or "").strip())
|
||||
"""已配置密码即要求登录(用户名未设时默认 admin)。"""
|
||||
return bool(_env_password())
|
||||
|
||||
|
||||
def expected_username() -> str:
|
||||
return _env_username() or DEFAULT_USERNAME
|
||||
|
||||
|
||||
def verify_credentials(username: str, password: str) -> bool:
|
||||
if not _env_password():
|
||||
return True
|
||||
u_ok = compare_digest(expected_username(), (username or "").strip())
|
||||
p_ok = compare_digest(_env_password(), (password or "").strip())
|
||||
return u_ok and p_ok
|
||||
|
||||
|
||||
def verify_password(password: str) -> bool:
|
||||
expected = (os.getenv("HUB_PASSWORD") or "").strip()
|
||||
if not expected:
|
||||
return True
|
||||
return compare_digest(expected, (password or "").strip())
|
||||
"""兼容旧调用:仅校验密码、用户名用默认值。"""
|
||||
return verify_credentials(expected_username(), password)
|
||||
|
||||
|
||||
def _secret() -> bytes:
|
||||
raw = (os.getenv("HUB_SESSION_SECRET") or os.getenv("HUB_PASSWORD") or "").strip()
|
||||
raw = (os.getenv("HUB_SESSION_SECRET") or "").strip()
|
||||
if not raw:
|
||||
return b"hub-dev-insecure"
|
||||
raw = "|".join(p for p in [_env_username(), _env_password()] if p) or "hub-dev-insecure"
|
||||
return raw.encode("utf-8")
|
||||
|
||||
|
||||
@@ -41,8 +61,12 @@ def _b64url_decode(text: str) -> bytes:
|
||||
return base64.urlsafe_b64decode(text + pad)
|
||||
|
||||
|
||||
def create_session_token() -> str:
|
||||
payload = {"exp": int(time.time()) + SESSION_MAX_AGE_SEC, "v": 1}
|
||||
def create_session_token(username: str | None = None) -> str:
|
||||
payload = {
|
||||
"exp": int(time.time()) + SESSION_MAX_AGE_SEC,
|
||||
"v": 2,
|
||||
"u": (username or expected_username()).strip(),
|
||||
}
|
||||
body = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
||||
sig = hmac.new(_secret(), body.encode("ascii"), hashlib.sha256).hexdigest()
|
||||
return f"{body}.{sig}"
|
||||
@@ -60,7 +84,12 @@ def validate_session_token(token: str | None) -> bool:
|
||||
except Exception:
|
||||
return False
|
||||
exp = int(payload.get("exp") or 0)
|
||||
return exp > int(time.time())
|
||||
if exp <= int(time.time()):
|
||||
return False
|
||||
sess_user = (payload.get("u") or "").strip()
|
||||
if sess_user and not compare_digest(sess_user, expected_username()):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def cookie_secure() -> bool:
|
||||
|
||||
@@ -264,8 +264,8 @@
|
||||
const el = document.getElementById("settings-meta-line");
|
||||
if (!el) return;
|
||||
const parts = [];
|
||||
if (m.password_required) parts.push("已启用 HUB_PASSWORD 登录保护");
|
||||
else parts.push("未设 HUB_PASSWORD(反代公网暴露时建议设置)");
|
||||
if (m.password_required) parts.push("已启用用户名+密码登录");
|
||||
else parts.push("未设 HUB_PASSWORD(反代公网暴露时建议设置 HUB_USERNAME + HUB_PASSWORD)");
|
||||
if (m.hub_bridge_token_set) parts.push("中控已配置 HUB_BRIDGE_TOKEN");
|
||||
else parts.push("中控未设 HUB_BRIDGE_TOKEN(实例需 APP_AUTH_DISABLED 或同令牌)");
|
||||
if (m.public_origin) parts.push("浏览器外链基址: " + m.public_origin);
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="hint-body">
|
||||
保存后写入 <code>hub_settings.json</code>。Flask / Agent 填本机地址即可;复盘链接可留空(由 Flask 地址自动生成)。<br />
|
||||
<code>HUB_DISABLED_IDS</code> 可强制关闭账户;<code>HUB_BRIDGE_TOKEN</code> 与实例一致,或实例 <code>APP_AUTH_DISABLED=true</code>。<br />
|
||||
公网反代请在 hub <code>.env</code> 设置 <code>HUB_PASSWORD</code>;HTTPS 反代建议 <code>HUB_COOKIE_SECURE=true</code>。
|
||||
公网反代请在 hub <code>.env</code> 设置 <code>HUB_USERNAME</code> 与 <code>HUB_PASSWORD</code>;HTTPS 反代建议 <code>HUB_COOKIE_SECURE=true</code>。
|
||||
</div>
|
||||
</details>
|
||||
<p id="settings-meta-line" class="settings-meta-line"></p>
|
||||
|
||||
@@ -21,50 +21,57 @@
|
||||
</div>
|
||||
<form id="login-form" class="login-form" autocomplete="on">
|
||||
<label class="field">
|
||||
<span>访问密码</span>
|
||||
<input type="password" name="password" id="login-password" required autofocus placeholder="HUB_PASSWORD" />
|
||||
<span>用户名</span>
|
||||
<input type="text" name="username" id="login-username" required autocomplete="username" placeholder="HUB_USERNAME" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>密码</span>
|
||||
<input type="password" name="password" id="login-password" required autocomplete="current-password" placeholder="HUB_PASSWORD" />
|
||||
</label>
|
||||
<button type="submit" class="primary login-submit">进入系统</button>
|
||||
<p id="login-err" class="login-err" hidden></p>
|
||||
</form>
|
||||
<p class="login-foot">反代暴露公网时请在 hub <code>.env</code> 设置 <code>HUB_PASSWORD</code></p>
|
||||
<p class="login-foot">在 hub <code>.env</code> 设置 <code>HUB_USERNAME</code> 与 <code>HUB_PASSWORD</code>(未设用户名时默认为 <code>admin</code>)</p>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById("login-form");
|
||||
const err = document.getElementById("login-err");
|
||||
const userInput = document.getElementById("login-username");
|
||||
const params = new URLSearchParams(location.search);
|
||||
const next = params.get("next") || "/monitor";
|
||||
|
||||
fetch("/api/auth/status")
|
||||
.then((r) => r.json())
|
||||
.then((s) => {
|
||||
if (!s.required || s.logged_in) location.href = next;
|
||||
if (s.username_hint && !userInput.value) userInput.value = s.username_hint;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
err.hidden = true;
|
||||
const pwd = document.getElementById("login-password").value;
|
||||
const username = userInput.value.trim();
|
||||
const password = document.getElementById("login-password").value;
|
||||
try {
|
||||
const r = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password: pwd }),
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
const j = await r.json().catch(() => ({}));
|
||||
if (r.ok && j.ok) {
|
||||
location.href = next.startsWith("/") ? next : "/monitor";
|
||||
return;
|
||||
}
|
||||
err.textContent = j.detail || j.msg || "密码错误";
|
||||
err.textContent = j.detail || j.msg || "用户名或密码错误";
|
||||
err.hidden = false;
|
||||
} catch (ex) {
|
||||
err.textContent = String(ex);
|
||||
err.hidden = false;
|
||||
}
|
||||
};
|
||||
|
||||
fetch("/api/auth/status")
|
||||
.then((r) => r.json())
|
||||
.then((s) => {
|
||||
if (!s.required || s.logged_in) location.href = next;
|
||||
})
|
||||
.catch(() => {});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -199,7 +199,7 @@ python hub.py
|
||||
访问控制:
|
||||
|
||||
- **IP**:默认允许本机与 RFC1918 私网(`HUB_TRUST_LAN=true`);公网 IP 直连返回 403。
|
||||
- **密码**:设置 `HUB_PASSWORD` 后,所有页面与 API(除 `/login`、`/assets`、`/api/auth/*`)须先登录;反代到公网时**务必设置**。
|
||||
- **登录**:设置 `HUB_PASSWORD` 后须用户名+密码登录(`HUB_USERNAME`,未设时默认 `admin`);反代到公网时**务必设置**。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
@@ -209,7 +209,7 @@ python hub.py
|
||||
| POST | `/api/close/{id}` | 单户全平 |
|
||||
| POST | `/api/close-all` | 全局全平,body 可选 `exclude_ids` |
|
||||
| GET | `/api/auth/status` | 是否需登录、是否已登录 |
|
||||
| POST | `/api/auth/login` | body `{"password":"..."}` |
|
||||
| POST | `/api/auth/login` | body `{"username":"...","password":"..."}` |
|
||||
| POST | `/api/auth/logout` | 退出 |
|
||||
|
||||
实例侧(中控只读调用 `/api/hub/monitor` 等;下单请在实例网页):
|
||||
@@ -232,8 +232,9 @@ python hub.py
|
||||
| `HUB_BRIDGE_TOKEN` | 空 | Flask 桥接令牌;可同 `CONTROL_TOKEN` |
|
||||
| `HUB_DISABLED_IDS` | `1` | 逗号分隔,强制关闭的账户 id |
|
||||
| `HUB_TRUST_LAN` | `true` | `false` 时仅本机可访问中控页面 |
|
||||
| `HUB_USERNAME` | `admin` | 登录用户名(仅当已设密码时生效) |
|
||||
| `HUB_PASSWORD` | (空) | 非空即启用 Web 登录 |
|
||||
| `HUB_SESSION_SECRET` | 回退 `HUB_PASSWORD` | 会话 Cookie 签名密钥 |
|
||||
| `HUB_SESSION_SECRET` | 用户名+密码 | 会话 Cookie 签名密钥 |
|
||||
| `HUB_COOKIE_SECURE` | `false` | HTTPS 反代时设 `true` |
|
||||
| `HUB_SESSION_DAYS` | `7` | 登录保持天数 |
|
||||
|
||||
@@ -259,7 +260,7 @@ python hub.py
|
||||
1. **中控不下单**:开仓、关键位、趋势回调仅在各实例网页操作。
|
||||
2. **全平为市价减仓**:监控区全平不可撤销,操作前二次确认。
|
||||
3. **子代理建议只监听 127.0.0.1**,不要对局域网暴露 API Key 通道。
|
||||
4. **公网暴露 hub**:必须设置 `HUB_PASSWORD`;HTTPS 反代建议 `HUB_COOKIE_SECURE=true`;亦可 `HUB_HOST=127.0.0.1` 仅本机监听 + 反代。
|
||||
4. **公网暴露 hub**:必须设置 `HUB_USERNAME` + `HUB_PASSWORD`;HTTPS 反代建议 `HUB_COOKIE_SECURE=true`;亦可 `HUB_HOST=127.0.0.1` 仅本机监听 + 反代。
|
||||
5. **复盘不在中控**:时间筛选、导出 CSV、编辑笔记仍在各实例 `/records`。
|
||||
6. **OKX 默认关**:避免未部署 OKX 时监控卡片持续报错。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user