From 3af977443901627c6e21199b7a4447fee4022840 Mon Sep 17 00:00:00 2001 From: jinchao <383321154@qq.com> Date: Tue, 17 Jun 2025 15:57:11 +0800 Subject: [PATCH] =?UTF-8?q?V0.24.0.250617=5Falpha:=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Login/login.cpp | 149 +++++++++++++++- Login/test_encrypt/CMakeLists.txt | 20 +++ .../test_access_level_encrypt.cpp | 36 ++++ Release/database/XNSim.db | Bin 1224704 -> 1224704 bytes XNSimHtml/components/run-sim.js | 2 +- XNSimHtml/components/user-info.js | 160 ++++++++++++------ XNSimHtml/main.html | 20 --- XNSimHtml/routes/auth.js | 2 + 8 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 Login/test_encrypt/CMakeLists.txt create mode 100644 Login/test_encrypt/test_access_level_encrypt.cpp diff --git a/Login/login.cpp b/Login/login.cpp index e89e0f7..803da44 100644 --- a/Login/login.cpp +++ b/Login/login.cpp @@ -14,6 +14,7 @@ #include #include #include +#include using json = nlohmann::json; namespace fs = std::filesystem; @@ -82,6 +83,94 @@ std::string base64_encode(const unsigned char *data, size_t length) return ret; } +// 利用用户名和固定密钥派生32字节AES密钥 +std::vector derive_aes_key(const std::string &username, const std::string &fixed_key) +{ + unsigned char hash[SHA256_DIGEST_LENGTH]; + std::string key_material = username + fixed_key; + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); + EVP_DigestUpdate(ctx, key_material.c_str(), key_material.length()); + EVP_DigestFinal_ex(ctx, hash, NULL); + EVP_MD_CTX_free(ctx); + return std::vector(hash, hash + SHA256_DIGEST_LENGTH); +} + +// AES-256-CBC加密 +std::vector aes_encrypt(const std::string &plaintext, + const std::vector &key, + std::vector &iv_out) +{ + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + throw std::runtime_error("EVP_CIPHER_CTX_new failed"); + iv_out.resize(16); + if (!RAND_bytes(iv_out.data(), iv_out.size())) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("RAND_bytes failed"); + } + if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key.data(), iv_out.data())) + throw std::runtime_error("EVP_EncryptInit_ex failed"); + std::vector ciphertext(plaintext.size() + 16); + int len = 0, ciphertext_len = 0; + if (1 + != EVP_EncryptUpdate(ctx, ciphertext.data(), &len, + reinterpret_cast(plaintext.c_str()), + plaintext.size())) + throw std::runtime_error("EVP_EncryptUpdate failed"); + ciphertext_len = len; + if (1 != EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len)) + throw std::runtime_error("EVP_EncryptFinal_ex failed"); + ciphertext_len += len; + ciphertext.resize(ciphertext_len); + EVP_CIPHER_CTX_free(ctx); + return ciphertext; +} + +// AES-256-CBC解密 +std::string aes_decrypt(const std::vector &ciphertext, + const std::vector &key, const std::vector &iv) +{ + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + throw std::runtime_error("EVP_CIPHER_CTX_new failed"); + if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key.data(), iv.data())) + throw std::runtime_error("EVP_DecryptInit_ex failed"); + std::vector plaintext(ciphertext.size()); + int len = 0, plaintext_len = 0; + if (1 != EVP_DecryptUpdate(ctx, plaintext.data(), &len, ciphertext.data(), ciphertext.size())) + throw std::runtime_error("EVP_DecryptUpdate failed"); + plaintext_len = len; + if (1 != EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len)) + throw std::runtime_error("EVP_DecryptFinal_ex failed"); + plaintext_len += len; + plaintext.resize(plaintext_len); + EVP_CIPHER_CTX_free(ctx); + return std::string(plaintext.begin(), plaintext.end()); +} + +// 二进制转16进制字符串 +std::string bin2hex(const std::vector &data) +{ + std::ostringstream oss; + for (auto c : data) { + oss << std::hex << std::setw(2) << std::setfill('0') << (int)c; + } + return oss.str(); +} + +// 16进制字符串转二进制 +std::vector hex2bin(const std::string &hex) +{ + std::vector out; + for (size_t i = 0; i + 1 < hex.length(); i += 2) { + std::string byteString = hex.substr(i, 2); + unsigned char byte = (unsigned char)strtol(byteString.c_str(), nullptr, 16); + out.push_back(byte); + } + return out; +} + extern "C" LOGIN_EXPORT int validateUser(const char *username_buffer, size_t username_length, const char *password_buffer, size_t password_length) { @@ -159,7 +248,27 @@ extern "C" LOGIN_EXPORT int getUserInfo(int user_id, char *result, int result_le json userInfo; userInfo["id"] = sqlite3_column_int(stmt, 0); userInfo["username"] = reinterpret_cast(sqlite3_column_text(stmt, 1)); - userInfo["access_level"] = sqlite3_column_int(stmt, 3); + // access_level 解密 + std::string accessLevelEnc = + reinterpret_cast(sqlite3_column_text(stmt, 3)); + std::vector iv_and_ciphertext = hex2bin(accessLevelEnc); + if (iv_and_ciphertext.size() < 16) { + userInfo["access_level"] = nullptr; + } else { + std::vector iv(iv_and_ciphertext.begin(), + iv_and_ciphertext.begin() + 16); + std::vector ciphertext(iv_and_ciphertext.begin() + 16, + iv_and_ciphertext.end()); + std::string fixed_key = "XNSim_Access_Key"; + std::string username = userInfo["username"].get(); + std::vector key = derive_aes_key(username, fixed_key); + try { + std::string accessLevelStr = aes_decrypt(ciphertext, key, iv); + userInfo["access_level"] = std::stoi(accessLevelStr); + } catch (...) { + userInfo["access_level"] = nullptr; + } + } userInfo["full_name"] = reinterpret_cast(sqlite3_column_text(stmt, 4)); userInfo["phone"] = reinterpret_cast(sqlite3_column_text(stmt, 5)); @@ -297,6 +406,17 @@ extern "C" LOGIN_EXPORT int registerUser(const char *username_buffer, int userna // 验证权限级别 int accessLevel = 0; + if (userInfo.contains("access_level")) { + accessLevel = userInfo["access_level"].get(); + } + // access_level 加密 + std::string fixed_key = "XNSim_Access_Key"; + std::vector key = derive_aes_key(username_str, fixed_key); + std::vector iv; + std::vector ciphertext = aes_encrypt(std::to_string(accessLevel), key, iv); + std::vector iv_and_ciphertext = iv; + iv_and_ciphertext.insert(iv_and_ciphertext.end(), ciphertext.begin(), ciphertext.end()); + std::string accessLevelEnc = bin2hex(iv_and_ciphertext); // 生成加密密码 std::string salt = generateSalt(username_str); @@ -331,7 +451,7 @@ extern "C" LOGIN_EXPORT int registerUser(const char *username_buffer, int userna if (sqlite3_prepare_v2(db, queryStr, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, username_str.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, encryptedPassword.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 3, accessLevel); + sqlite3_bind_text(stmt, 3, accessLevelEnc.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 4, userInfo["full_name"].get().c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 5, userInfo["phone"].get().c_str(), -1, @@ -578,7 +698,30 @@ extern "C" LOGIN_EXPORT int updateUserAccessLevel(int user_id, int access_level) const char *queryStr = "UPDATE users SET access_level = ? WHERE id = ?"; if (sqlite3_prepare_v2(db, queryStr, -1, &stmt, nullptr) == SQLITE_OK) { - sqlite3_bind_int(stmt, 1, access_level); + // 需要查用户名 + std::string username; + { + sqlite3_stmt *stmt2; + const char *q2 = "SELECT username FROM users WHERE id = ?"; + if (sqlite3_prepare_v2(db, q2, -1, &stmt2, nullptr) == SQLITE_OK) { + sqlite3_bind_int(stmt2, 1, user_id); + if (sqlite3_step(stmt2) == SQLITE_ROW) { + username = + reinterpret_cast(sqlite3_column_text(stmt2, 0)); + } + sqlite3_finalize(stmt2); + } + } + std::string fixed_key = "XNSim_Access_Key"; + std::vector key = derive_aes_key(username, fixed_key); + std::vector iv; + std::vector ciphertext = + aes_encrypt(std::to_string(access_level), key, iv); + std::vector iv_and_ciphertext = iv; + iv_and_ciphertext.insert(iv_and_ciphertext.end(), ciphertext.begin(), + ciphertext.end()); + std::string accessLevelEnc = bin2hex(iv_and_ciphertext); + sqlite3_bind_text(stmt, 1, accessLevelEnc.c_str(), -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, user_id); if (sqlite3_step(stmt) == SQLITE_DONE) { diff --git a/Login/test_encrypt/CMakeLists.txt b/Login/test_encrypt/CMakeLists.txt new file mode 100644 index 0000000..249bcbd --- /dev/null +++ b/Login/test_encrypt/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) +project(test_access_level_encrypt) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 查找依赖包 +find_package(OpenSSL REQUIRED) +find_package(SQLite3 REQUIRED) +find_package(nlohmann_json 3.9.1 REQUIRED) + +add_executable(test_access_level_encrypt test_access_level_encrypt.cpp) + +# 包含上级目录头文件(login.cpp依赖) +target_include_directories(test_access_level_encrypt PRIVATE ${CMAKE_SOURCE_DIR}/..) + +# 链接OpenSSL +if(OpenSSL_FOUND) + target_link_libraries(test_access_level_encrypt PRIVATE OpenSSL::SSL OpenSSL::Crypto SQLite::SQLite3 nlohmann_json::nlohmann_json) +endif() \ No newline at end of file diff --git a/Login/test_encrypt/test_access_level_encrypt.cpp b/Login/test_encrypt/test_access_level_encrypt.cpp new file mode 100644 index 0000000..418d209 --- /dev/null +++ b/Login/test_encrypt/test_access_level_encrypt.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include "../login.cpp" // 直接复用加密解密相关函数 + +int main() +{ + std::string username = "test3"; + std::string fixed_key = "XNSim_Access_Key"; + std::vector key = derive_aes_key(username, fixed_key); + + for (int access_level = 0; access_level <= 4; ++access_level) { + // 加密 + std::vector iv; + std::vector ciphertext = aes_encrypt(std::to_string(access_level), key, iv); + std::vector iv_and_ciphertext = iv; + iv_and_ciphertext.insert(iv_and_ciphertext.end(), ciphertext.begin(), ciphertext.end()); + std::string hexstr = bin2hex(iv_and_ciphertext); + + // 解密 + std::vector iv_and_ciphertext2 = hex2bin(hexstr); + std::vector iv2(iv_and_ciphertext2.begin(), iv_and_ciphertext2.begin() + 16); + std::vector ciphertext2(iv_and_ciphertext2.begin() + 16, + iv_and_ciphertext2.end()); + std::string decrypted = aes_decrypt(ciphertext2, key, iv2); + + std::cout << "username: " << username << std::endl; + std::cout << "access_level: " << access_level << std::endl; + std::cout << "加密后16进制: " << hexstr << std::endl; + std::cout << "解密还原: " << decrypted << std::endl; + std::cout << "-----------------------------" << std::endl; + } + return 0; +} \ No newline at end of file diff --git a/Release/database/XNSim.db b/Release/database/XNSim.db index b04d6e7a7335d375687043497749fd9519d171e7..d6eeccd084b48dd77a0068b39d17ab1799c2ba5f 100644 GIT binary patch delta 1076 zcmYL{U1%It6vt;~vpd<@nN1>+Nr=0f6yt+7;odv1|Njtn~&mySZQasd1~=NpCsNyf&;&q!*K5T z-}66b>e1-bqtS()j)$72zou#08wywP)8aud{ZJb$FMc1bw5{pGJEhvraU;Ff0{(0( z=2Ncz(PaF=q~+L&mdTya>}=bs`S)`toLlx^Yklp9b5@16I+|N?Do*ujp*&0=_TrEM z7E1v{Nd0&>5)qV$N#ILDIfNmm9tDE%7;uWhAdWDEA`+g5t3$=EYPo1zmDBou-LoZC z7Xd%;nU9gcfn-uLK?LPM0>XI6XcSVxV~hplVI&0bxfBROz#+jRLae$`I@Y5a?oj;y zRH(Ht^6aJZFoXnPz$o#2%!KqKj94TX!dze<2_by$QRzt#fWQxZ8U-+7L}CeIDyj6V zuNPjnDnr@*nrD3?(ihO0 z@kDj4FrZ8}_iMggEDvKupgO^ruWo}Q7AUV7Q(Xp(_(8-F!i=Ms0~~=sa@Btf7>Prb z6$T8()o+Xa**6VG>)D&zFU7Ip5W>I@Vj7~Dg&_9TgrK^vgdhS;1PnzaU>+etBGrlV zUX3lJkYY+9AsAKrOGDRt+o#9Z$MwuLy}4u>2h66uXph-r)xP$LyDJ9JP33(0lzw<| zcyG`4Ug_G7ZcNsG?AJT$bB5tq>F$hs#I602)w^o9ZNt_x z^)1^t_Bb`-4Aso%#u>vIs-Jsq{A}l}RJRTW5gr61_0fNfO(UhXYqMtRuGw-Dq*F@Y zm>&l%inJo5Fcq!!age?7dv88{Ud#WM|2W_3aQoNX-?@^tlm)F9%qi2!6w>F_#-B`I zs2mu{cK5l3BbBoIpB4g$3;;R+8J~FX9kp_Y2tEY-=9T-&)>8B8y<~PRx%BnZi(5~A zSZ!S1fP?Cr-1zLt=G<<|{AQ!J-njEkW98H2lX~Ne+sVdyH8v delta 488 zcmXZYziSjh6bJB`w?AffZf5slj+@9418P7p%7R+TDG3B_v$+V0V&g>;ZBmHmrO+aG z8j~W8=MYvQh^T*n8p0x63JY5;tSlra27+B|w2&jdtQMb{$9I^=n`w3{&2HsUsjou_ zJx&N2LmGW(kDPKk37vERQ zUh5$J->iQkqPBT9G5%sI|2F&;2xV#7sL$T63mjx`$n0%75A0fM*C;%v+p7!?vu)Xy zv5XDP&uy$hNNwi|Ew|4XO81YnicD)w7l-dmU!R-J`eX|v08md}9;1bH2|x))yZWq7 zzIk*Yc`ZRwE8UbZ{@psKDhb<#o9V4x_$1p?Dk=AXr5Adz4i*h=JY?1evoD5@i+|@V z*FqcZ?=E79r?t>~wRK3jgT(*p-}SkgmS1web3
-
- 用户等级 +
+ 用户
用户信息加载中...
用户名 @@ -203,6 +223,7 @@ class UserInfo extends HTMLElement { addEventListeners() { const userDropdown = this.shadowRoot.querySelector('.user-dropdown'); const userDropdownToggle = this.shadowRoot.querySelector('.user-dropdown-toggle'); + const userIconWrapper = this.shadowRoot.querySelector('.user-icon-wrapper'); // 点击切换下拉菜单 userDropdownToggle.addEventListener('click', (e) => { @@ -225,6 +246,33 @@ class UserInfo extends HTMLElement { userDropdown.classList.remove('active'); }); }); + + // 处理tooltip的定位 + userIconWrapper.addEventListener('mouseenter', () => { + const tooltip = this.shadowRoot.querySelector('.tooltip'); + const rect = userIconWrapper.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + + // 检查右边界 + if (rect.left + tooltipRect.width > window.innerWidth) { + // 计算tooltip应该距离右边界的位置 + const rightEdge = window.innerWidth; + const tooltipWidth = tooltipRect.width; + tooltip.style.position = 'fixed'; + tooltip.style.left = 'auto'; + tooltip.style.right = '0'; + tooltip.style.transform = 'none'; + tooltip.style.margin = '0'; + tooltip.style.top = `${rect.bottom + 8}px`; + } else { + tooltip.style.position = 'absolute'; + tooltip.style.left = '50%'; + tooltip.style.right = 'auto'; + tooltip.style.transform = 'translateX(-50%)'; + tooltip.style.margin = '8px 0 0 0'; + tooltip.style.top = '100%'; + } + }); } handleDropdownAction(action) { @@ -273,48 +321,49 @@ class UserInfo extends HTMLElement { // 设置用户名 this.shadowRoot.getElementById('userName').textContent = userInfo.username; - // 设置用户等级图标和tooltip信息 - const userLevelIcon = this.shadowRoot.getElementById('userLevelIcon'); + // 设置用户图标和tooltip信息 + const userIcon = this.shadowRoot.getElementById('userIcon'); const userTooltip = this.shadowRoot.getElementById('userTooltip'); - const accessLevel = parseInt(userInfo.access_level); - let levelName = ''; - switch(accessLevel) { - case 1: - userLevelIcon.src = 'assets/icons/png/user.png'; - userLevelIcon.alt = '普通用户'; - levelName = '普通用户'; - break; - case 2: - userLevelIcon.src = 'assets/icons/png/dvlp.png'; - userLevelIcon.alt = '开发者'; - levelName = '开发者'; - break; - case 3: - userLevelIcon.src = 'assets/icons/png/master.png'; - userLevelIcon.alt = '组长'; - levelName = '组长'; - break; - case 4: - userLevelIcon.src = 'assets/icons/png/admin.png'; - userLevelIcon.alt = '管理员'; - levelName = '管理员'; - break; - default: - userLevelIcon.src = 'assets/icons/png/guest.png'; - userLevelIcon.alt = '访客'; - levelName = '访客'; + // 使用用户自定义图标 + if (userInfo.icon) { + userIcon.src = userInfo.icon; + } else { + // 如果没有自定义图标,使用默认用户图标 + userIcon.src = 'assets/icons/png/user_b.png'; } + userIcon.alt = userInfo.username; // 构建HTML内容 const tooltipContent = ` -
用户名:${userInfo.username}
-
真实姓名:${userInfo.full_name || '未设置'}
-
权限级别:${levelName}
-
所属部门:${userInfo.department || '未设置'}
-
职位:${userInfo.position || '未设置'}
-
电子邮箱:${userInfo.email || '未设置'}
-
联系电话:${userInfo.phone || '未设置'}
+
+ 用户名 + ${userInfo.username || '未设置'} +
+
+ 真实姓名 + ${userInfo.full_name || '未设置'} +
+
+ 权限级别 + ${this.getAccessLevelName(userInfo.access_level)} +
+
+ 所属部门 + ${userInfo.department || '未设置'} +
+
+ 职位 + ${userInfo.position || '未设置'} +
+
+ 电子邮箱 + ${userInfo.email || '未设置'} +
+
+ 联系电话 + ${userInfo.phone || '未设置'} +
`; userTooltip.innerHTML = tooltipContent; @@ -322,7 +371,18 @@ class UserInfo extends HTMLElement { // 控制用户管理选项的显示 const userManagementItem = this.shadowRoot.querySelector('.admin-only'); if (userManagementItem) { - userManagementItem.style.display = accessLevel >= 3 ? 'flex' : 'none'; + userManagementItem.style.display = parseInt(userInfo.access_level) >= 3 ? 'flex' : 'none'; + } + } + + getAccessLevelName(accessLevel) { + const level = parseInt(accessLevel); + switch(level) { + case 1: return '普通用户'; + case 2: return '开发者'; + case 3: return '组长'; + case 4: return '管理员'; + default: return '访客'; } } @@ -342,6 +402,8 @@ class UserInfo extends HTMLElement { mainContainer.classList.remove('visible'); // 等待过渡效果完成 setTimeout(() => { + location.reload(); + mainContainer.style.display = 'none'; // 显示认证容器 authContainer.style.display = 'block'; diff --git a/XNSimHtml/main.html b/XNSimHtml/main.html index 271cfef..cc47c40 100644 --- a/XNSimHtml/main.html +++ b/XNSimHtml/main.html @@ -413,26 +413,6 @@ fetch('/api/logout', { method: 'POST', credentials: 'include' - }).then(() => { - // 清除所有标签页 - tabsContainer.clearAllTabs(); - - // 显示退出成功提示 - showToast('已安全退出登录'); - - // 隐藏主容器 - mainContainer.classList.remove('visible'); - setTimeout(() => { - mainContainer.style.display = 'none'; - authContainer.style.display = 'block'; - requestAnimationFrame(() => { - authContainer.classList.add('visible'); - const authComponent = document.querySelector('auth-component'); - if (authComponent) { - authComponent.reset(); - } - }); - }, 300); }).catch(error => { console.error('登出错误:', error); showToast('登出过程中发生错误'); diff --git a/XNSimHtml/routes/auth.js b/XNSimHtml/routes/auth.js index 137b4b1..4546909 100644 --- a/XNSimHtml/routes/auth.js +++ b/XNSimHtml/routes/auth.js @@ -39,6 +39,8 @@ router.post('/login', (req, res) => { // 设置 session req.session.user = userInfo; + + console.log('用户', userInfo.username, '登录成功,', '权限等级:', userInfo.access_level); res.json({ success: true,