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 b04d6e7..d6eeccd 100644 Binary files a/Release/database/XNSim.db and b/Release/database/XNSim.db differ diff --git a/XNSimHtml/components/run-sim.js b/XNSimHtml/components/run-sim.js index 817da30..87be1c2 100644 --- a/XNSimHtml/components/run-sim.js +++ b/XNSimHtml/components/run-sim.js @@ -771,7 +771,7 @@ class RunSim extends HTMLElement { .list-content { overflow: auto; padding: 10px; - max-height: 200px; + max-height: 350px; } .list-group { diff --git a/XNSimHtml/components/user-info.js b/XNSimHtml/components/user-info.js index 6018cc7..99f8dff 100644 --- a/XNSimHtml/components/user-info.js +++ b/XNSimHtml/components/user-info.js @@ -52,7 +52,7 @@ class UserInfo extends HTMLElement { font-weight: 500; } - .user-level-icon { + .user-icon { width: 24px; height: 24px; margin-right: 0; @@ -60,10 +60,12 @@ class UserInfo extends HTMLElement { cursor: help; } - .user-level-icon-wrapper { + .user-icon-wrapper { position: relative; display: inline-block; z-index: 1002; + padding: 0; + margin: 0; } .icon-small { @@ -124,34 +126,52 @@ class UserInfo extends HTMLElement { position: absolute; background: rgba(0, 0, 0, 0.8); color: white; - padding: 12px 15px; + padding: 8px 12px; border-radius: 4px; - font-size: 14px; - white-space: pre-line; + font-size: 13px; visibility: hidden; opacity: 0; transition: opacity 0.3s; top: 100%; left: 50%; transform: translateX(-50%); - margin-top: 10px; + margin-top: 8px; z-index: 1001; min-width: 200px; text-align: left; - line-height: 1.5; + line-height: 1.4; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } + .tooltip.right-aligned { + left: auto; + right: 0; + transform: none; + margin: 8px 0 0 0; + padding: 8px 12px 8px 12px; + } + .tooltip .info-row { - margin: 4px 0; + margin: 2px 0; + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; } .tooltip .label { color: #a8c8ff; - margin-right: 8px; + flex-shrink: 0; + min-width: 70px; } - .user-level-icon-wrapper:hover .tooltip { + .tooltip .value { + color: #ffffff; + flex: 1; + white-space: nowrap; + } + + .user-icon-wrapper:hover .tooltip { visibility: visible; opacity: 1; } @@ -174,8 +194,8 @@ class UserInfo extends HTMLElement {
-
- 用户等级 +
+ 用户
用户信息加载中...
用户名 @@ -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,