Skip to content

Commit 3074d9b

Browse files
committed
feat: #442 密码进行hash存储
1 parent 81ee640 commit 3074d9b

File tree

4 files changed

+72
-12
lines changed

4 files changed

+72
-12
lines changed

apps/admin/services.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from apps.base.utils import get_expire_info, get_file_path_name
99
from fastapi import HTTPException
1010
from core.settings import data_root
11+
from core.utils import hash_password, is_password_hashed
1112

1213

1314
class FileService:
@@ -76,6 +77,9 @@ async def update_config(self, data: dict):
7677
if admin_token is None or admin_token == "":
7778
raise HTTPException(status_code=400, detail="管理员密码不能为空")
7879

80+
if not is_password_hashed(admin_token):
81+
data["admin_token"] = hash_password(admin_token)
82+
7983
for key, value in data.items():
8084
if key not in settings.default_config:
8185
continue

apps/admin/views.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,16 @@
1717
from apps.base.models import FileCodes, KeyValue
1818
from apps.admin.dependencies import create_token
1919
from core.settings import settings
20-
from core.utils import get_now
20+
from core.utils import get_now, verify_password
2121

2222
admin_api = APIRouter(prefix="/admin", tags=["管理"])
2323

2424

2525
@admin_api.post("/login")
2626
async def login(data: LoginData):
27-
# 验证管理员密码
28-
if data.password != settings.admin_token:
27+
if not verify_password(data.password, settings.admin_token):
2928
raise HTTPException(status_code=401, detail="密码错误")
3029

31-
# 生成包含管理员身份的token
3230
token = create_token({"is_admin": True})
3331
return APIResponse(detail={"token": token, "token_type": "Bearer"})
3432

core/utils.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ async def get_select_token(code: str):
4747
:return:
4848
"""
4949
token = settings.admin_token
50-
return hashlib.sha256(f"{code}{int(time.time() / 1000)}000{token}".encode()).hexdigest()
50+
return hashlib.sha256(
51+
f"{code}{int(time.time() / 1000)}000{token}".encode()
52+
).hexdigest()
5153

5254

5355
async def get_file_url(code: str):
@@ -95,6 +97,44 @@ def gen_desc_en(value: int, desc: str):
9597
return desc_zh, desc_en
9698

9799

100+
def hash_password(password: str) -> str:
101+
"""
102+
使用 SHA256 + salt 哈希密码
103+
返回格式: sha256$<salt>$<hash>
104+
"""
105+
salt = os.urandom(16).hex()
106+
password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
107+
return f"sha256${salt}${password_hash}"
108+
109+
110+
def verify_password(password: str, hashed: str) -> bool:
111+
"""
112+
验证密码是否匹配
113+
支持新格式 (sha256$salt$hash) 和旧格式 (明文)
114+
"""
115+
if not hashed:
116+
return False
117+
118+
# 新格式: sha256$salt$hash
119+
if hashed.startswith("sha256$"):
120+
parts = hashed.split("$")
121+
if len(parts) != 3:
122+
return False
123+
_, salt, stored_hash = parts
124+
password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
125+
return password_hash == stored_hash
126+
127+
# 旧格式: 明文比较 (兼容迁移前的数据)
128+
return password == hashed
129+
130+
131+
def is_password_hashed(password: str) -> bool:
132+
"""
133+
检查密码是否已经是哈希格式
134+
"""
135+
return password.startswith("sha256$") and len(password.split("$")) == 3
136+
137+
98138
async def sanitize_filename(filename: str) -> str:
99139
"""
100140
安全处理文件名:
@@ -105,15 +145,15 @@ async def sanitize_filename(filename: str) -> str:
105145
filename = os.path.basename(filename)
106146
illegal_chars = r'[\\/*?:"<>|\x00-\x1F]' # 包含控制字符
107147
# 替换非法字符为下划线
108-
cleaned = re.sub(illegal_chars, '_', filename)
148+
cleaned = re.sub(illegal_chars, "_", filename)
109149
# 处理空格(可选替换为_)
110-
cleaned = cleaned.replace(' ', '_')
150+
cleaned = cleaned.replace(" ", "_")
111151
# 处理连续下划线
112-
cleaned = re.sub(r'_+', '_', cleaned)
152+
cleaned = re.sub(r"_+", "_", cleaned)
113153
# 处理首尾特殊字符
114-
cleaned = cleaned.strip('._')
154+
cleaned = cleaned.strip("._")
115155
# 处理空文件名情况
116156
if not cleaned:
117-
cleaned = 'unnamed_file'
157+
cleaned = "unnamed_file"
118158
# 长度限制(按需调整)
119159
return cleaned[:255]

main.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from core.response import APIResponse
2323
from core.settings import data_root, settings, BASE_DIR, DEFAULT_CONFIG
2424
from core.tasks import delete_expire_files, clean_incomplete_uploads
25+
from core.utils import hash_password, is_password_hashed
2526

2627

2728
@asynccontextmanager
@@ -63,13 +64,26 @@ async def load_config():
6364
key="sys_start", defaults={"value": int(time.time() * 1000)}
6465
)
6566
settings.user_config = user_config.value
66-
# 更新 ip_limit 配置
67+
68+
await migrate_password_to_hash()
69+
6770
ip_limit["error"].minutes = settings.errorMinute
6871
ip_limit["error"].count = settings.errorCount
6972
ip_limit["upload"].minutes = settings.uploadMinute
7073
ip_limit["upload"].count = settings.uploadCount
7174

7275

76+
async def migrate_password_to_hash():
77+
if not is_password_hashed(settings.admin_token):
78+
hashed = hash_password(settings.admin_token)
79+
settings.admin_token = hashed
80+
config_record = await KeyValue.filter(key="settings").first()
81+
if config_record and config_record.value:
82+
config_record.value["admin_token"] = hashed
83+
await config_record.save()
84+
logger.info("已将管理员密码迁移为哈希存储")
85+
86+
7387
app = FastAPI(lifespan=lifespan)
7488

7589
app.add_middleware(
@@ -149,5 +163,9 @@ async def get_config():
149163
import uvicorn
150164

151165
uvicorn.run(
152-
app="main:app", host=settings.serverHost, port=settings.serverPort, reload=False, workers=settings.serverWorkers
166+
app="main:app",
167+
host=settings.serverHost,
168+
port=settings.serverPort,
169+
reload=False,
170+
workers=settings.serverWorkers,
153171
)

0 commit comments

Comments
 (0)