Skip to content

Commit 540b257

Browse files
authored
feat: 添加 WebUI 密码自动生成和帮助提示功能,增强安全性 (#376)
2 parents 8f35930 + a97e728 commit 540b257

3 files changed

Lines changed: 103 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dev_tools/debug_tools
1919
*.lock
2020
config/reloadflag
2121
config/reloadalas
22+
/password.txt
2223
test.py
2324
test/
2425
!test/

module/webui/app.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import json
77
import queue
88
import requests
9+
import secrets
10+
import string
911
import threading
1012
import time
1113
import re
@@ -141,6 +143,10 @@
141143
"你的公网IP已泄露 请加群https://join.nanoda.work/#/join联系我们解除安全限制"
142144
)
143145
PUBLIC_WEBUI_WITHOUT_PASSWORD_MESSAGE = "当前配置允许所有设备访问,请添加密码\n\n设置方法:\n在config/deploy.yaml中添加:\nWebUI:\n Password: 你的密码\n然后重启\n\n温馨提示:密码推荐大小写字母+数字不小于六位\n\n目前配置允許所有裝置存取,請新增密碼。\n\n設定方法:\n在config/deploy.yaml中添加:\nWebUI:\n Password: 你的密碼\n然後重新啟動\n\n溫馨提示:密碼建議包含大小寫英文字母與數字,且不少於六位。\n\nThe current configuration allows access from all devices. Please set a password.\n\nHow to configure:\nAdd the following to config/deploy.yaml:\nWebUI:\n Password: your_password\nThen restart the application.\n\nTip: It is recommended to use a password containing uppercase and lowercase letters as well as numbers, with a minimum length of 6 characters.\n\n現在の設定では、すべてのデバイスからアクセスできます。パスワードを設定してください。\n\n設定方法:\nconfig/deploy.yaml に以下を追加してください:\nWebUI:\n Password: あなたのパスワード\nその後、アプリケーションを再起動してください。\n\nヒント:パスワードは英大文字・英小文字・数字を含み、6文字以上にすることを推奨します。"
146+
PUBLIC_WEBUI_PASSWORD_GENERATE_FAILED_MESSAGE = (
147+
"当前配置允许所有设备访问,但自动生成密码失败,请手动在 config/deploy.yaml 设置 Password 后重启。"
148+
)
149+
WEBUI_AUTO_PASSWORD_FILE = "password.txt"
144150

145151

146152
def is_public_webui_host(host):
@@ -170,6 +176,57 @@ def is_webui_password_set(password):
170176
return bool(str(password or "").strip())
171177

172178

179+
def generate_webui_password(length=32):
180+
"""
181+
生成包含大小写字母和数字的 WebUI 密码。
182+
183+
Args:
184+
length (int): 密码长度。
185+
186+
Returns:
187+
str: 随机密码。
188+
"""
189+
letters_upper = string.ascii_uppercase
190+
letters_lower = string.ascii_lowercase
191+
digits = string.digits
192+
alphabet = letters_upper + letters_lower + digits
193+
password = [
194+
secrets.choice(letters_upper),
195+
secrets.choice(letters_lower),
196+
secrets.choice(digits),
197+
]
198+
password.extend(secrets.choice(alphabet) for _ in range(length - len(password)))
199+
secrets.SystemRandom().shuffle(password)
200+
return "".join(password)
201+
202+
203+
def ensure_public_webui_password(key):
204+
"""
205+
公网监听且未设置密码时自动生成密码。
206+
207+
Args:
208+
key: 命令行或部署配置中的 WebUI 密码。
209+
210+
Returns:
211+
tuple[str | None, str | None]: 有效密码和失败原因。
212+
"""
213+
host = State.webui_host or State.deploy_config.WebuiHost
214+
if not is_public_webui_host(host) or is_webui_password_set(key):
215+
return key, None
216+
217+
try:
218+
password = generate_webui_password()
219+
from deploy.atomic import atomic_write
220+
221+
atomic_write(WEBUI_AUTO_PASSWORD_FILE, f"{password}\n")
222+
State.deploy_config.Password = password
223+
logger.warning(f"WebUI 已自动生成密码,请在根目录 {WEBUI_AUTO_PASSWORD_FILE} 查看。")
224+
return password, None
225+
except Exception as e:
226+
logger.exception(f"WebUI 自动生成密码失败: {e}")
227+
return None, str(e)
228+
229+
173230
def timedelta_to_text(delta=None):
174231
time_delta_name_suffix_dict = {
175232
"Y": "YearsAgo",
@@ -5126,6 +5183,7 @@ def app():
51265183
AlasGUI.set_theme(theme=theme)
51275184
lang.LANG = State.deploy_config.Language
51285185
key = args.key or State.deploy_config.Password
5186+
key, password_error = ensure_public_webui_password(key)
51295187
cdn = args.cdn if args.cdn else State.deploy_config.CDN
51305188
runs = None
51315189
if args.run:
@@ -5160,13 +5218,12 @@ def _block_restricted_device():
51605218
)
51615219
return True
51625220

5163-
def _block_public_webui_without_password():
5164-
host = State.webui_host or State.deploy_config.WebuiHost
5165-
if not is_public_webui_host(host) or is_webui_password_set(key):
5221+
def _block_public_webui_password_error():
5222+
if password_error is None:
51665223
return False
51675224
popup(
51685225
"安全保护",
5169-
PUBLIC_WEBUI_WITHOUT_PASSWORD_MESSAGE,
5226+
PUBLIC_WEBUI_PASSWORD_GENERATE_FAILED_MESSAGE,
51705227
implicit_close=False,
51715228
closable=False,
51725229
)
@@ -5175,9 +5232,9 @@ def _block_public_webui_without_password():
51755232
def index():
51765233
if _block_restricted_device():
51775234
return
5178-
if _block_public_webui_without_password():
5235+
if _block_public_webui_password_error():
51795236
return
5180-
if key is not None and not login(key):
5237+
if is_webui_password_set(key) and not login(key):
51815238
logger.warning(f"{info.user_ip} login failed.")
51825239
time.sleep(1.5)
51835240
run_js("location.reload();")
@@ -5189,9 +5246,9 @@ def index():
51895246
def manage():
51905247
if _block_restricted_device():
51915248
return
5192-
if _block_public_webui_without_password():
5249+
if _block_public_webui_password_error():
51935250
return
5194-
if key is not None and not login(key):
5251+
if is_webui_password_set(key) and not login(key):
51955252
logger.warning(f"{info.user_ip} login failed.")
51965253
time.sleep(1.5)
51975254
run_js("location.reload();")

module/webui/utils.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from typing import Callable, Generator, List
1515

1616
import pywebio
17-
from pywebio.input import PASSWORD, input
18-
from pywebio.output import PopupSize, popup, put_html, toast
17+
from pywebio.input import PASSWORD, actions, input, input_group
18+
from pywebio.output import PopupSize, popup, put_html, put_text, toast
1919
from pywebio.session import eval_js, info as session_info, register_thread, run_js
2020
from rich.console import Console
2121
from rich.terminal_theme import TerminalTheme
@@ -530,13 +530,47 @@ def _record_login_failure():
530530
return _webui_login_failure_count
531531

532532

533+
def _show_password_help(action):
534+
if action == "new":
535+
popup(
536+
"没设置过密码?",
537+
put_text("系统已自动生成密码,请到项目根目录 password.txt 查看。"),
538+
)
539+
elif action == "forgot":
540+
popup(
541+
"忘记密码?",
542+
put_text("请到 config/deploy.yaml 的 Password 字段查看当前密码。"),
543+
)
544+
545+
546+
def _input_webui_password():
547+
while True:
548+
data = input_group(inputs=[
549+
input(
550+
name="password",
551+
label="请输入 WebUI 密码",
552+
type=PASSWORD,
553+
placeholder="PASSWORD",
554+
),
555+
actions(name="action", buttons=[
556+
{"label": "登录", "value": "login", "type": "submit", "color": "primary"},
557+
{"label": "没设置过密码?", "value": "new", "type": "submit", "color": "secondary"},
558+
{"label": "忘记密码?", "value": "forgot", "type": "submit", "color": "secondary"},
559+
]),
560+
])
561+
action = data["action"]
562+
if action == "login":
563+
return data["password"]
564+
_show_password_help(action)
565+
566+
533567
def login(password):
534568
if is_login_forbidden():
535569
toast("密码错误次数过多,请重启后再试。", color="error")
536570
return False
537571
if get_localstorage("password") == str(password):
538572
return True
539-
pwd = input(label="Please login below.", type=PASSWORD, placeholder="PASSWORD")
573+
pwd = _input_webui_password()
540574
if is_login_forbidden():
541575
toast("密码错误次数过多,请重启后再试。", color="error")
542576
return False

0 commit comments

Comments
 (0)