Skip to content

Commit 4464a40

Browse files
committed
feat: magic-dash-pro模板登录功能安全化升级,使用rsa加密传输代替原有的明文密码传输
1 parent 115dd40 commit 4464a40

File tree

9 files changed

+305
-24
lines changed

9 files changed

+305
-24
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
__pycache__/
22
dist/
33
**/*.db
4-
.vscode/
4+
.vscode/
5+
*.pem

magic_dash/templates/magic-dash-pro/assets/js/basic_callbacks.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,60 @@ window.dash_clientside = Object.assign({}, window.dash_clientside, {
139139
[true, 'antd-full-screen-exit'] :
140140
[false, 'antd-full-screen']
141141
)
142+
},
143+
// 使用Web Crypto API加密密码
144+
encryptPassword: async (password, publicKeyPem) => {
145+
if (!password || !publicKeyPem) {
146+
return null;
147+
}
148+
149+
// 将PEM格式的公钥转换为ArrayBuffer
150+
const pemHeader = '-----BEGIN PUBLIC KEY-----';
151+
const pemFooter = '-----END PUBLIC KEY-----';
152+
const pemContents = publicKeyPem
153+
.replace(pemHeader, '')
154+
.replace(pemFooter, '')
155+
.replace(/\s/g, '');
156+
157+
const binaryString = window.atob(pemContents);
158+
const bytes = new Uint8Array(binaryString.length);
159+
for (let i = 0; i < binaryString.length; i++) {
160+
bytes[i] = binaryString.charCodeAt(i);
161+
}
162+
163+
// 导入公钥
164+
const publicKey = await window.crypto.subtle.importKey(
165+
'spki',
166+
bytes.buffer,
167+
{
168+
name: 'RSA-OAEP',
169+
hash: 'SHA-256'
170+
},
171+
false,
172+
['encrypt']
173+
);
174+
175+
// 加密密码
176+
const encoder = new TextEncoder();
177+
const passwordData = encoder.encode(password);
178+
179+
const encryptedData = await window.crypto.subtle.encrypt(
180+
{
181+
name: 'RSA-OAEP'
182+
},
183+
publicKey,
184+
passwordData
185+
);
186+
187+
// 将加密后的数据转换为Base64字符串
188+
const encryptedBytes = new Uint8Array(encryptedData);
189+
let binary = '';
190+
for (let i = 0; i < encryptedBytes.byteLength; i++) {
191+
binary += String.fromCharCode(encryptedBytes[i]);
192+
}
193+
const encryptedBase64 = window.btoa(binary);
194+
195+
return encryptedBase64;
142196
}
143197
}
144198
});

magic_dash/templates/magic-dash-pro/callbacks/login_c.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,56 @@
77
from dash import set_props, dcc
88
from flask_login import login_user
99
import feffery_antd_components as fac
10-
from dash.dependencies import Input, Output, State
1110
from flask_principal import identity_changed, Identity
11+
from dash.dependencies import Input, Output, State, ClientsideFunction
1212

1313
from server import app, User
1414
from models.users import Users
1515
from configs import BaseConfig
1616
from models.logs import LoginLogs
17+
from utils.crypto_utils import decrypt_password
18+
19+
20+
app.clientside_callback(
21+
# 基于浏览器内置Web Crypto API加密密码
22+
ClientsideFunction(namespace="clientside_basic", function_name="encryptPassword"),
23+
Output("login-password-crypto", "data"),
24+
Input("login-password", "value"),
25+
State("login-rsa-pubkey", "data"),
26+
)
1727

1828

1929
@app.callback(
20-
[Output("login-form", "helps"), Output("login-form", "validateStatuses")],
30+
[
31+
Output("login-user-name-form-item", "help"),
32+
Output("login-password-form-item", "help"),
33+
Output("login-user-name-form-item", "validateStatus"),
34+
Output("login-password-form-item", "validateStatus"),
35+
],
2136
[Input("login-button", "nClicks"), Input("login-password", "nSubmit")],
22-
[State("login-form", "values"), State("login-remember-me", "checked")],
37+
[
38+
State("login-user-name", "value"),
39+
State("login-password-crypto", "data"),
40+
State("login-remember-me", "checked"),
41+
],
2342
running=[
2443
[Output("login-button", "loading"), True, False],
2544
],
2645
prevent_initial_call=True,
2746
)
28-
def handle_login(nClicks, nSubmit, values, remember_me):
47+
def handle_login(nClicks, nSubmit, user_name, password_crypto, remember_me):
2948
"""处理用户登录逻辑"""
3049

3150
time.sleep(0.25)
3251

33-
values = values or {}
52+
# 解密前端传输的加密密码
53+
password = decrypt_password(password_crypto)
54+
55+
# 构造兼容原有判断逻辑的表单values
56+
values = {
57+
"login-user-name": user_name,
58+
"login-password": password,
59+
}
3460

3561
# 提取当前登录行为对应的系统、浏览器信息
3662
user_agent = parse(str(request.user_agent))
@@ -55,15 +81,11 @@ def handle_login(nClicks, nSubmit, values, remember_me):
5581

5682
return [
5783
# 表单帮助信息
58-
{
59-
"用户名": "请输入用户名" if not values.get("login-user-name") else None,
60-
"密码": "请输入密码" if not values.get("login-password") else None,
61-
},
84+
"请输入用户名" if not values.get("login-user-name") else None,
85+
"请输入密码" if not values.get("login-password") else None,
6286
# 表单帮助状态
63-
{
64-
"用户名": "error" if not values.get("login-user-name") else None,
65-
"密码": "error" if not values.get("login-password") else None,
66-
},
87+
"error" if not values.get("login-user-name") else None,
88+
"error" if not values.get("login-password") else None,
6789
]
6890

6991
# 校验用户登录信息
@@ -96,9 +118,11 @@ def handle_login(nClicks, nSubmit, values, remember_me):
96118

97119
return [
98120
# 表单帮助信息
99-
{"用户名": "用户不存在"},
121+
"用户不存在",
122+
None,
100123
# 表单帮助状态
101-
{"用户名": "error"},
124+
"error",
125+
None,
102126
]
103127

104128
else:
@@ -129,9 +153,11 @@ def handle_login(nClicks, nSubmit, values, remember_me):
129153

130154
return [
131155
# 表单帮助信息
132-
{"密码": "密码错误"},
156+
None,
157+
"密码错误",
133158
# 表单帮助状态
134-
{"密码": "error"},
159+
None,
160+
"error",
135161
]
136162

137163
# 更新用户信息表session_token字段
@@ -174,4 +200,4 @@ def handle_login(nClicks, nSubmit, values, remember_me):
174200
login_datetime=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
175201
)
176202

177-
return [{}, {}]
203+
return [None] * 4

magic_dash/templates/magic-dash-pro/configs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .router_config import RouterConfig # noqa: F401
33
from .layout_config import LayoutConfig # noqa: F401
44
from .auth_config import AuthConfig # noqa: F401
5+
from .database_config import DatabaseConfig # noqa: F401

magic_dash/templates/magic-dash-pro/configs/base_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,9 @@ class BaseConfig:
5050
fullscreen_watermark_generator: Callable = (
5151
lambda current_user: current_user.user_name
5252
)
53+
54+
# 用于登录密码加密传输的RSA公钥文件路径
55+
rsa_public_key_path: str = "magic_dash_pro_public_key.pem"
56+
57+
# 用于登录密码加密传输的RSA私钥文件路径
58+
rsa_private_key_path: str = "magic_dash_pro_private_key.pem"

magic_dash/templates/magic-dash-pro/models/init_db.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,68 @@
1+
import os
12
import questionary
23
from rich.console import Console
34
from rich.panel import Panel
45
from rich.text import Text
56
from werkzeug.security import generate_password_hash
7+
from cryptography.hazmat.primitives import serialization
8+
from cryptography.hazmat.primitives.asymmetric import rsa
9+
from cryptography.hazmat.backends import default_backend
610

711
from models import db
812

913
# 导入相关数据表模型
1014
from .users import Users
1115
from .departments import Departments
12-
from configs import AuthConfig
16+
from configs import AuthConfig, BaseConfig
1317

1418
# 创建rich console实例
1519
console = Console()
1620

1721
# 创建表(如果表不存在)
1822
db.create_tables([Users, Departments])
1923

24+
25+
def generate_rsa_key_pair():
26+
"""生成RSA密钥对并保存到项目根目录"""
27+
28+
private_key_path = BaseConfig.rsa_private_key_path
29+
public_key_path = BaseConfig.rsa_public_key_path
30+
31+
# 生成RSA密钥对
32+
private_key = rsa.generate_private_key(
33+
public_exponent=65537, key_size=2048, backend=default_backend()
34+
)
35+
36+
# 序列化私钥
37+
private_pem = private_key.private_bytes(
38+
encoding=serialization.Encoding.PEM,
39+
format=serialization.PrivateFormat.PKCS8,
40+
encryption_algorithm=serialization.NoEncryption(),
41+
)
42+
43+
# 序列化公钥
44+
public_key = private_key.public_key()
45+
public_pem = public_key.public_bytes(
46+
encoding=serialization.Encoding.PEM,
47+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
48+
)
49+
50+
# 保存到文件
51+
with open(private_key_path, "wb") as f:
52+
f.write(private_pem)
53+
54+
with open(public_key_path, "wb") as f:
55+
f.write(public_pem)
56+
57+
58+
def check_rsa_keys_exist():
59+
"""检查RSA密钥对文件是否已存在"""
60+
61+
return os.path.exists(BaseConfig.rsa_private_key_path) and os.path.exists(
62+
BaseConfig.rsa_public_key_path
63+
)
64+
65+
2066
# 统一的questionary样式
2167
custom_style = questionary.Style(
2268
[
@@ -43,6 +89,68 @@
4389
executed_operations = []
4490
admin_created = False
4591

92+
# 0. RSA密钥对生成
93+
console.print("\n[dim]请确认以下安全密钥操作:[/dim]\n")
94+
95+
# 检查是否已存在RSA密钥对
96+
if check_rsa_keys_exist():
97+
# 已存在密钥,询问是否覆盖
98+
confirm_override = questionary.confirm(
99+
f"检测到已存在RSA密钥对文件({BaseConfig.rsa_private_key_path}{BaseConfig.rsa_public_key_path}),是否重新生成并覆盖?",
100+
default=False,
101+
style=custom_style,
102+
).ask()
103+
104+
if confirm_override:
105+
try:
106+
generate_rsa_key_pair()
107+
executed_operations.append(
108+
(
109+
"RSA密钥对",
110+
f"已重新生成并覆盖\n 私钥: {BaseConfig.rsa_private_key_path}\n 公钥: {BaseConfig.rsa_public_key_path}",
111+
"yellow",
112+
"green",
113+
)
114+
)
115+
except Exception as e:
116+
executed_operations.append(
117+
("RSA密钥对", f"重新生成失败: {str(e)}", "yellow", "red")
118+
)
119+
else:
120+
executed_operations.append(
121+
(
122+
"RSA密钥对",
123+
f"保留现有密钥文件\n 私钥: {BaseConfig.rsa_private_key_path}\n 公钥: {BaseConfig.rsa_public_key_path}",
124+
"dim",
125+
"dim",
126+
)
127+
)
128+
else:
129+
# 不存在密钥,询问是否生成
130+
confirm_generate = questionary.confirm(
131+
"当前项目根目录中不存在RSA密钥对文件,是否生成新的RSA密钥对?",
132+
default=True,
133+
style=custom_style,
134+
).ask()
135+
136+
if confirm_generate:
137+
try:
138+
generate_rsa_key_pair()
139+
executed_operations.append(
140+
(
141+
"RSA密钥对",
142+
f"已生成\n 私钥: {BaseConfig.rsa_private_key_path}\n 公钥: {BaseConfig.rsa_public_key_path}",
143+
"yellow",
144+
"green",
145+
)
146+
)
147+
except Exception as e:
148+
executed_operations.append(
149+
("RSA密钥对", f"生成失败: {str(e)}", "yellow", "red")
150+
)
151+
else:
152+
executed_operations.append(("RSA密钥对", "已跳过生成", "dim", "dim"))
153+
46154
# 1. 询问是否重置部门表
47155
console.print("\n[dim]请确认以下数据库操作:[/dim]\n")
48156
confirm_departments = questionary.confirm(

magic_dash/templates/magic-dash-pro/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ flask-compress
99
flask-principal
1010
yarl
1111
pandas
12+
cryptography

0 commit comments

Comments
 (0)