V0.24.0.250617_alpha:登录相关功能修改

This commit is contained in:
jinchao 2025-06-17 15:57:11 +08:00
parent ae419e5b83
commit 3af9774439
8 changed files with 316 additions and 73 deletions

View File

@ -14,6 +14,7 @@
#include <iostream>
#include <vector>
#include <algorithm>
#include <openssl/rand.h>
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<unsigned char> 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<unsigned char>(hash, hash + SHA256_DIGEST_LENGTH);
}
// AES-256-CBC加密
std::vector<unsigned char> aes_encrypt(const std::string &plaintext,
const std::vector<unsigned char> &key,
std::vector<unsigned char> &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<unsigned char> ciphertext(plaintext.size() + 16);
int len = 0, ciphertext_len = 0;
if (1
!= EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
reinterpret_cast<const unsigned char *>(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<unsigned char> &ciphertext,
const std::vector<unsigned char> &key, const std::vector<unsigned char> &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<unsigned char> 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<unsigned char> &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<unsigned char> hex2bin(const std::string &hex)
{
std::vector<unsigned char> 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<const char *>(sqlite3_column_text(stmt, 1));
userInfo["access_level"] = sqlite3_column_int(stmt, 3);
// access_level 解密
std::string accessLevelEnc =
reinterpret_cast<const char *>(sqlite3_column_text(stmt, 3));
std::vector<unsigned char> iv_and_ciphertext = hex2bin(accessLevelEnc);
if (iv_and_ciphertext.size() < 16) {
userInfo["access_level"] = nullptr;
} else {
std::vector<unsigned char> iv(iv_and_ciphertext.begin(),
iv_and_ciphertext.begin() + 16);
std::vector<unsigned char> ciphertext(iv_and_ciphertext.begin() + 16,
iv_and_ciphertext.end());
std::string fixed_key = "XNSim_Access_Key";
std::string username = userInfo["username"].get<std::string>();
std::vector<unsigned char> 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<const char *>(sqlite3_column_text(stmt, 4));
userInfo["phone"] = reinterpret_cast<const char *>(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<int>();
}
// access_level 加密
std::string fixed_key = "XNSim_Access_Key";
std::vector<unsigned char> key = derive_aes_key(username_str, fixed_key);
std::vector<unsigned char> iv;
std::vector<unsigned char> ciphertext = aes_encrypt(std::to_string(accessLevel), key, iv);
std::vector<unsigned char> 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<std::string>().c_str(), -1,
SQLITE_STATIC);
sqlite3_bind_text(stmt, 5, userInfo["phone"].get<std::string>().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<const char *>(sqlite3_column_text(stmt2, 0));
}
sqlite3_finalize(stmt2);
}
}
std::string fixed_key = "XNSim_Access_Key";
std::vector<unsigned char> key = derive_aes_key(username, fixed_key);
std::vector<unsigned char> iv;
std::vector<unsigned char> ciphertext =
aes_encrypt(std::to_string(access_level), key, iv);
std::vector<unsigned char> 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) {

View File

@ -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()

View File

@ -0,0 +1,36 @@
#include <iostream>
#include <vector>
#include <string>
#include <iomanip>
#include <sstream>
#include "../login.cpp" // 直接复用加密解密相关函数
int main()
{
std::string username = "test3";
std::string fixed_key = "XNSim_Access_Key";
std::vector<unsigned char> key = derive_aes_key(username, fixed_key);
for (int access_level = 0; access_level <= 4; ++access_level) {
// 加密
std::vector<unsigned char> iv;
std::vector<unsigned char> ciphertext = aes_encrypt(std::to_string(access_level), key, iv);
std::vector<unsigned char> 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<unsigned char> iv_and_ciphertext2 = hex2bin(hexstr);
std::vector<unsigned char> iv2(iv_and_ciphertext2.begin(), iv_and_ciphertext2.begin() + 16);
std::vector<unsigned char> 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;
}

Binary file not shown.

View File

@ -771,7 +771,7 @@ class RunSim extends HTMLElement {
.list-content {
overflow: auto;
padding: 10px;
max-height: 200px;
max-height: 350px;
}
.list-group {

View File

@ -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 {
</style>
<div class="user-dropdown">
<div class="user-dropdown-toggle">
<div class="user-level-icon-wrapper">
<img id="userLevelIcon" src="assets/icons/png/user.png" alt="用户等级" class="user-level-icon">
<div class="user-icon-wrapper">
<img id="userIcon" src="assets/icons/png/user.png" alt="用户" class="user-icon">
<div class="tooltip" id="userTooltip">用户信息加载中...</div>
</div>
<span id="userName">用户名</span>
@ -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 = `
<div class="info-row"><span class="label">用户名</span>${userInfo.username}</div>
<div class="info-row"><span class="label">真实姓名</span>${userInfo.full_name || ''}</div>
<div class="info-row"><span class="label">权限级别</span>${levelName}</div>
<div class="info-row"><span class="label">所属部门</span>${userInfo.department || ''}</div>
<div class="info-row"><span class="label">职位</span>${userInfo.position || ''}</div>
<div class="info-row"><span class="label">电子邮箱</span>${userInfo.email || ''}</div>
<div class="info-row"><span class="label">联系电话</span>${userInfo.phone || ''}</div>
<div class="info-row">
<span class="label">用户名</span>
<span class="value">${userInfo.username || '未设置'}</span>
</div>
<div class="info-row">
<span class="label">真实姓名</span>
<span class="value">${userInfo.full_name || '未设置'}</span>
</div>
<div class="info-row">
<span class="label">权限级别</span>
<span class="value">${this.getAccessLevelName(userInfo.access_level)}</span>
</div>
<div class="info-row">
<span class="label">所属部门</span>
<span class="value">${userInfo.department || '未设置'}</span>
</div>
<div class="info-row">
<span class="label">职位</span>
<span class="value">${userInfo.position || '未设置'}</span>
</div>
<div class="info-row">
<span class="label">电子邮箱</span>
<span class="value">${userInfo.email || '未设置'}</span>
</div>
<div class="info-row">
<span class="label">联系电话</span>
<span class="value">${userInfo.phone || '未设置'}</span>
</div>
`;
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';

View File

@ -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('登出过程中发生错误');

View File

@ -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,