Skip to content

Commit 89d07ae

Browse files
wangbochao789WangDaBenShisunjinghua.vendorsunjh2222
authored
feat(signin/workflow): 新增ECDH加密&&优化大模型提供商操作逻辑 (#25)
* feat(signin): 新增免责声明 * fix(modelWarehouse): 初始化的时候调用get请求获取微调模型的list * fix(modelWarehouse): 将模型仓库中>模型管理>模型详情页获取微调模型从get请求改为post * fix(modelWarehouse): 将模型仓库中>模型管理>模型详情页获取微调模型从get请求改为post * fix: clear log * fix: 优化UploadModule和更新gitignore * feat(onlineModel): 将openai模型新增url字段 * fix(inferenceService): 将模型仓库详情页获取微调模型的get接口改为post * fix(inferenceService): 修改传入参数格式 * fix(inferenceService): 修改ts报错 * feat(signin): 新增ECDH加密机制 * fix: 修复接口地址报错 * fix: 修改请求 * feat(signin/workflow): 新增ECDH加密&&优化大模型提供商操作逻辑 * feat:安全加固:日志保存&文件后缀校验&用户登录信息加密 --------- Co-authored-by: wangbochao.vendor <15830881993@163.com> Co-authored-by: sunjinghua.vendor <sunjinghua.vendor@sensetime.com> Co-authored-by: sjh <111295061+sunjh2222@users.noreply.github.com>
1 parent 8984dd2 commit 89d07ae

18 files changed

Lines changed: 1202 additions & 73 deletions

File tree

back/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ Markdown
4040
markdown-it-py
4141
tokenizers
4242
pycryptodome
43+
cryptography
4344
safetensors
4445
# app
4546
# uvicorn
4647
# gradio<5.0.0
4748
typer
49+
filetype
4850
# docx2txt
4951
# EbookLib
5052
# html2text

back/src/app.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import warnings
2222
from appcmd import register_commands
2323
from datetime import datetime
24-
from logging.handlers import RotatingFileHandler
24+
from logging.handlers import TimedRotatingFileHandler
2525
from zoneinfo import ZoneInfo
2626

2727
from flask import Flask, Response, g, request
@@ -80,8 +80,11 @@ def initialize_logger(self, app):
8080
log_dir = os.path.dirname(log_file)
8181
os.makedirs(log_dir, exist_ok=True)
8282
log_handlers = [
83-
RotatingFileHandler(
84-
filename=log_file, maxBytes=1024 * 1024 * 1024, backupCount=5
83+
TimedRotatingFileHandler(
84+
filename=log_file,
85+
when="D", # 按天分块
86+
interval=6, # 每6天
87+
backupCount=30, # 保留180天的日志
8588
),
8689
logging.StreamHandler(sys.stdout),
8790
]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright (c) 2025 SenseTime. All Rights Reserved.
2+
# Author: LazyLLM Team, https://github.com/LazyAGI/LazyLLM
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""
17+
ECDH 密钥交换 API
18+
19+
提供 ECDH 密钥交换接口,用于前后端协商共享密钥。
20+
无需预先约定密钥,适合开源项目。
21+
"""
22+
23+
from flask_restful import reqparse
24+
25+
from core.restful import Resource
26+
from parts.urls import api
27+
from utils.util_ecdh import ECDHSessionManager
28+
29+
# 会话密钥过期时间(秒)
30+
SESSION_KEY_EXPIRY = 300 # 5分钟
31+
32+
33+
class KeyExchangeApi(Resource):
34+
"""ECDH 密钥交换 API
35+
36+
接收前端公钥,生成后端密钥对,计算共享密钥,返回后端公钥和会话 ID。
37+
前端使用返回的公钥计算相同的共享密钥,然后使用共享密钥加密数据。
38+
"""
39+
40+
def post(self):
41+
"""
42+
进行 ECDH 密钥交换
43+
44+
请求格式:
45+
{
46+
"frontend_public_key": "Base64编码的前端公钥"
47+
}
48+
49+
响应格式:
50+
{
51+
"backend_public_key": "Base64编码的后端公钥",
52+
"session_id": "会话ID(UUID)",
53+
"expires_in": 300,
54+
"algorithm": "ECDH-P256 + AES-256-GCM"
55+
}
56+
57+
Returns:
58+
dict: 包含后端公钥和会话 ID 的字典
59+
60+
Raises:
61+
ValueError: 当前端公钥格式错误时抛出
62+
"""
63+
parser = reqparse.RequestParser()
64+
parser.add_argument(
65+
"frontend_public_key",
66+
type=str,
67+
required=True,
68+
location="json",
69+
help="前端公钥(Base64 编码)"
70+
)
71+
body = parser.parse_args()
72+
73+
try:
74+
# 创建 ECDH 会话
75+
session_id, backend_public_key_b64, _ = ECDHSessionManager.create_session(
76+
body.frontend_public_key
77+
)
78+
79+
return {
80+
"backend_public_key": backend_public_key_b64,
81+
"session_id": session_id,
82+
"expires_in": SESSION_KEY_EXPIRY,
83+
"algorithm": "ECDH-P256 + AES-256-GCM",
84+
"curve": "secp256r1",
85+
"key_size": 256
86+
}
87+
88+
except Exception as e:
89+
import logging
90+
logger = logging.getLogger(__name__)
91+
logger.error(f"ECDH 密钥交换失败: {e}", exc_info=True)
92+
raise ValueError(f"密钥交换失败: {str(e)}")
93+
94+
95+
api.add_resource(KeyExchangeApi, "/key_exchange")
96+

back/src/parts/auth/login.py

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from parts.logs import Action, LogService, Module
3232
from parts.urls import api
3333
from utils.util_database import db
34+
from utils.util_ecdh import decrypt_ecdh_encrypted_data
3435

3536
from .sms import SmsChecker
3637

@@ -39,8 +40,8 @@ class RegisterApi(Resource):
3940
def post(self):
4041
"""注册新用户账号。
4142
42-
处理用户注册请求,验证用户输入信息并创建新账号
43-
包括验证短信验证码、检查密码一致性、创建账号和租户等。
43+
接收 encrypted_data 和 session_id 参数,使用 ECDH 会话密钥解密
44+
请求数据应包含:name, email, phone, password, confirm_password, verify_code
4445
4546
Returns:
4647
dict: 登录成功后的令牌信息
@@ -49,25 +50,37 @@ def post(self):
4950
ValueError: 当输入信息无效或密码不一致时抛出
5051
"""
5152
parser = reqparse.RequestParser()
52-
parser.add_argument("name", type=str, required=True, location="json")
53-
parser.add_argument("email", type=EmailType, required=True, location="json")
54-
parser.add_argument("phone", type=str, required=True, location="json")
55-
parser.add_argument("password", type=str, required=True, location="json")
56-
parser.add_argument(
57-
"confirm_password", type=str, required=True, location="json"
58-
)
59-
parser.add_argument("verify_code", type=str, required=True, location="json")
53+
parser.add_argument("encrypted_data", type=str, required=True, location="json")
54+
parser.add_argument("session_id", type=str, required=True, location="json")
6055
body = parser.parse_args()
6156

62-
AccountService.validate_name_email_phone(body.name, body.email, body.phone)
63-
if body.password != body.confirm_password:
57+
# 解密请求数据
58+
try:
59+
decrypted_data = decrypt_ecdh_encrypted_data(body.encrypted_data, body.session_id)
60+
except ValueError as e:
61+
raise ValueError(f"请求数据解密失败: {str(e)}")
62+
63+
# 从解密后的数据中提取参数
64+
name = decrypted_data.get("name")
65+
email = decrypted_data.get("email")
66+
phone = decrypted_data.get("phone")
67+
password = decrypted_data.get("password")
68+
confirm_password = decrypted_data.get("confirm_password")
69+
verify_code = decrypted_data.get("verify_code")
70+
71+
# 验证必需参数
72+
if not all([name, email, phone, password, confirm_password, verify_code]):
73+
raise ValueError("缺少必需参数:name, email, phone, password, confirm_password, verify_code")
74+
75+
AccountService.validate_name_email_phone(name, email, phone)
76+
if password != confirm_password:
6477
raise ValueError("两次输入的密码不相同")
6578

6679
# 校验验证码
67-
SmsChecker("register").check(body.phone, body.verify_code)
80+
SmsChecker("register").check(phone, verify_code)
6881

6982
account = RegisterService.register(
70-
body.email, body.phone, body.name, password=body.password
83+
email, phone, name, password=password
7184
)
7285
TenantService.create_private_tenant(account)
7386

@@ -199,6 +212,9 @@ class LoginApi(Resource):
199212
def post(self):
200213
"""用户密码登录。
201214
215+
接收 encrypted_data 和 session_id 参数,使用 ECDH 会话密钥解密。
216+
请求数据应包含:name 或 email 或 phone(至少一个),以及 password
217+
202218
使用用户名/邮箱和密码进行身份验证并登录系统。
203219
验证成功后记录登录日志并返回访问令牌。
204220
@@ -209,16 +225,31 @@ def post(self):
209225
ValueError: 当身份验证失败时抛出
210226
"""
211227
parser = reqparse.RequestParser()
212-
parser.add_argument("name", type=str, required=False, location="json")
213-
parser.add_argument("email", type=str, required=False, location="json")
214-
parser.add_argument("password", type=str, required=True, location="json")
215-
parser.add_argument(
216-
"remember_me", type=bool, required=False, default=False, location="json"
217-
)
228+
parser.add_argument("encrypted_data", type=str, required=True, location="json")
229+
parser.add_argument("session_id", type=str, required=True, location="json")
218230
body = parser.parse_args()
219231

232+
# 解密请求数据
233+
try:
234+
decrypted_data = decrypt_ecdh_encrypted_data(body.encrypted_data, body.session_id)
235+
except ValueError as e:
236+
raise ValueError(f"请求数据解密失败: {str(e)}")
237+
238+
# 从解密后的数据中提取参数
239+
name = decrypted_data.get("name")
240+
email = decrypted_data.get("email")
241+
phone = decrypted_data.get("phone", "")
242+
password = decrypted_data.get("password")
243+
remember_me = decrypted_data.get("remember_me", False)
244+
245+
# 验证必需参数
246+
if not password:
247+
raise ValueError("密码不能为空")
248+
if not any([name, email, phone]):
249+
raise ValueError("必须提供用户名/邮箱/手机号")
250+
220251
account = AccountService.authenticate_by_password(
221-
body.name, body.email, "", body.password
252+
name, email, phone, password
222253
)
223254
LogService().add(
224255
Module.USER_MANAGEMENT,
@@ -233,6 +264,9 @@ class LoginSmsApi(Resource):
233264
def post(self):
234265
"""短信验证码登录。
235266
267+
接收 encrypted_data 和 session_id 参数,使用 ECDH 会话密钥解密。
268+
请求数据应包含:phone, verify_code
269+
236270
使用手机号和短信验证码进行身份验证并登录系统。
237271
如果用户账号不存在,会缓存验证码用于后续注册流程。
238272
@@ -243,20 +277,36 @@ def post(self):
243277
ValueError: 当验证码验证失败或用户认证失败时抛出
244278
"""
245279
parser = reqparse.RequestParser()
246-
parser.add_argument("phone", type=str, required=True, location="json")
247-
parser.add_argument("verify_code", type=str, required=True, location="json")
280+
parser.add_argument("encrypted_data", type=str, required=True, location="json")
281+
parser.add_argument("session_id", type=str, required=True, location="json")
248282
body = parser.parse_args()
249283

284+
# 解密请求数据
285+
try:
286+
decrypted_data = decrypt_ecdh_encrypted_data(body.encrypted_data, body.session_id)
287+
except ValueError as e:
288+
raise ValueError(f"请求数据解密失败: {str(e)}")
289+
290+
# 从解密后的数据中提取参数
291+
phone = decrypted_data.get("phone")
292+
verify_code = decrypted_data.get("verify_code")
293+
294+
# 验证必需参数
295+
if not phone:
296+
raise ValueError("手机号不能为空")
297+
if not verify_code:
298+
raise ValueError("验证码不能为空")
299+
250300
# 校验登录验证码
251-
SmsChecker("login").check(body.phone, body.verify_code)
301+
SmsChecker("login").check(phone, verify_code)
252302

253-
account = Account.query.filter_by(phone=body.phone).first()
303+
account = Account.query.filter_by(phone=phone).first()
254304
if not account:
255305
SmsChecker("register").cached_phone_code_for_registration(
256-
body.phone, body.verify_code
306+
phone, verify_code
257307
)
258308

259-
account = AccountService.authenticate_by_sms(body.phone, body.verify_code)
309+
account = AccountService.authenticate_by_sms(phone, verify_code)
260310

261311
LogService().add(
262312
Module.USER_MANAGEMENT,

back/src/parts/data/controller.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from parts.logs import Action, LogService, Module
3636
from parts.urls import api
3737
from utils.util_database import db
38+
from utils.util_file_validation import validate_file_type_and_raise
3839

3940
from . import fields
4041
from .data_service import DataService
@@ -488,24 +489,32 @@ def post(self):
488489
if type is None or type == "":
489490
raise ValueError("没有选择文件类型")
490491
if type == "pic":
491-
if not file.filename.endswith(
492-
(
493-
".jpg",
494-
".png",
495-
".jpeg",
496-
".gif",
497-
".svg",
498-
".webp",
499-
".bmp",
500-
".tiff",
501-
".ico",
502-
".tar.gz",
503-
".zip",
504-
)
505-
):
492+
pic_extensions = [
493+
".jpg",
494+
".png",
495+
".jpeg",
496+
".gif",
497+
".svg",
498+
".webp",
499+
".bmp",
500+
".tiff",
501+
".ico",
502+
".tar.gz",
503+
".zip",
504+
]
505+
if not file.filename.endswith(tuple(pic_extensions)):
506506
raise ValueError("文件类型错误")
507507
if file.content_length > 2 * 1024 * 1024 * 1024:
508508
raise ValueError("文件大小不能超过2GB")
509+
510+
if not (file.filename.endswith(".zip") or file.filename.endswith(".tar.gz")):
511+
is_strict = not file.filename.lower().endswith(".svg")
512+
validate_file_type_and_raise(
513+
file,
514+
[ext.lstrip('.') for ext in pic_extensions if ext not in ['.tar.gz', '.zip']],
515+
strict=is_strict
516+
)
517+
509518
# 检查压缩包内文件类型
510519
allowed_ext = (
511520
".jpg",
@@ -521,13 +530,30 @@ def post(self):
521530
self.check_compres_package(file, allowed_ext)
522531

523532
if type == "doc":
524-
if not file.filename.endswith(
525-
(".json", ".csv", ".jsonl", ".txt", ".parquet", ".tar.gz", ".zip")
526-
):
533+
doc_extensions = [
534+
".json",
535+
".csv",
536+
".jsonl",
537+
".txt",
538+
".parquet",
539+
".tar.gz",
540+
".zip",
541+
]
542+
if not file.filename.endswith(tuple(doc_extensions)):
527543
raise ValueError("文件类型错误")
528544
if file.content_length > 1024 * 1024 * 1024:
529545
raise ValueError("文件大小不能超过1GB")
530546

547+
if not (file.filename.endswith(".zip") or file.filename.endswith(".tar.gz")):
548+
# 对于文档文件,大部分是文本文件,使用非严格模式
549+
# parquet 是二进制格式,需要特殊处理
550+
is_strict = file.filename.lower().endswith(".parquet")
551+
validate_file_type_and_raise(
552+
file,
553+
[ext.lstrip('.') for ext in doc_extensions if ext not in ['.tar.gz', '.zip']],
554+
strict=is_strict
555+
)
556+
531557
# 检查压缩包内文件类型
532558
allowed_ext = (".json", ".csv", ".jsonl", ".txt", ".parquet")
533559
self.check_compres_package(file, allowed_ext)

0 commit comments

Comments
 (0)