文章目录
- 0. 前言
- 开发环境 & 涉及技术
- 1. 宏观结构
- 2. 后端部分
- ① sqlite 管理类
- ② user 管理类
- 3. 前端部分(与后端交互)
- ① 登录
- ② 注册
- ③ 查看登录用户的信息
- ④ 更新用户信息
- ⑤ 登出用户 & 注销用户
- 注意
- 效果演示
0. 前言
源码链接:
源码 - onlineJudge / users
开发环境 & 涉及技术
开发环境
- Linux ubuntu20.4
- vscode
涉及技术
- jsoncpp 进行数据
- httplib网络库 使用
- sqlite 轻量化数据库
1. 宏观结构
该功能只是一个扩展,需要的内容并不多:
—comm(负责公共的功能模块,在项目中便已实现)
------- 数据库管理
------- 字符串切割
------- 日志文件
------- … …
—users.hpp
—sqlite数据库:轻量化数据库,可以直接创建使用
2. 后端部分
① sqlite 管理类
#include <sqlite3.h>
#include <string>
#include <vector>
#include <iostream>
#include "../comm/log.hpp"using namespace ns_log;
class SQLiteManager
{
public:SQLiteManager() = default; // 默认构造函数SQLiteManager(const std::string &dbName) : db(nullptr), dbName(dbName) {}~SQLiteManager(){closeDatabase();}// 打开数据库bool openDatabase(){}void closeDatabase(){}bool executeQuery(const std::string &query){}std::vector<std::vector<std::string>> executeSelect(const std::string &query){}std::string getErrorMessage() const{}std::vector<std::vector<std::string>> getUserInfoByName(const std::string &name){}private:static int callback(void *data, int argc, char **argv, char **azColName){}sqlite3 *db;std::string dbName;std::string errorMessage;
};
下面对这些函数一一进行介绍:
sqlite数据库打开与关闭
对于这一部分,当创建sqliteManager类后,根据传入的数据库位置,创建数据库对象(调用系统接口)
bool openDatabase()
{int result = sqlite3_open(dbName.c_str(), &db);if (result != SQLITE_OK){errorMessage = "Cannot open database: " + std::string(sqlite3_errmsg(db));LOG("FATAL") << errorMessage << "\n";return false;}return true;
}void closeDatabase()
{if (db){sqlite3_close(db);db = nullptr;}
}
执行选择语句(获取返回值)
executeSelect用于实现select语句,将获取到的内容通过二维的字符串数组返回,比如执行语句:select * from users where username = ‘alice’;
此时result中的元素为:
{{alice, password123, 123@emial.com, 123-4567-8910}}
std::vector<std::vector<std::string>> executeSelect(const std::string &query)
{// LOG(DEBUG) << "执行语句: " << query << "\n";std::vector<std::vector<std::string>> result;char *errMsg = nullptr;char **results = nullptr;int rows, cols;int resultCode = sqlite3_get_table(db, query.c_str(), &results, &rows, &cols, &errMsg);if (resultCode != SQLITE_OK){errorMessage = "SQL error: " + std::string(errMsg);sqlite3_free(errMsg);return result;}LOG(DEBUG) << "查询结果行数: " << rows << ", 列数: " << cols << "\n";if (results == nullptr){errorMessage = "Results are null";return result;}// for (int i = 0; i < (rows + 1) * cols; ++i) {// std::cout << "results[" << i << "] = " << (results[i] ? results[i] : "NULL") << std::endl;// }for (int i = 0; i < rows; ++i) // 从第一行数据开始{std::vector<std::string> row;for (int j = 0; j < cols; ++j){row.push_back(results[(i + 1) * cols + j] ? results[(i + 1) * cols + j] : "");}result.push_back(row);}sqlite3_free_table(results);LOG(DEBUG) << "查询结果: " << result.size() << "行\n";return result;
}
执行语句
该函数直接调用系统接口,判断是否出错;
bool executeQuery(const std::string &query)
{char *errMsg = nullptr;int result = sqlite3_exec(db, query.c_str(), nullptr, nullptr, &errMsg);if (result != SQLITE_OK){errorMessage = "SQL error: " + std::string(errMsg);sqlite3_free(errMsg);return false;}return true;
}
其他函数
getErrorMessage()
:当调用了执行语句的函数后,可以通过调用该函数,来获取错误信息(也可以直接传给执行语句一个输出型参数);getUserInfoByName
:获取用户的所有信息;
std::string getErrorMessage() const{return errorMessage;}std::vector<std::vector<std::string>> getUserInfoByName(const std::string &name){std::string query = "SELECT * FROM users WHERE name = '" + name + "';";return executeSelect(query);}
② user 管理类
首先封装一个用户类,相关的接口函数根据网站待实现的功能而定:
namespace ns_users
{class Users{private:pthread_mutex_t _mutex;SQLiteManager dbManager; // 数据库管理器public:Users(const std::string &dbName) : dbManager(dbName){pthread_mutex_init(&_mutex, nullptr);if (!dbManager.openDatabase()){std::cerr << "无法打开数据库 " << dbManager.getErrorMessage() << std::endl;}LOG(DEBUG) << "初始化用户管理器" << std::endl;}~Users(){pthread_mutex_destroy(&_mutex);dbManager.closeDatabase();}// sqlite版本// 判断用户是否存在bool isUserExist(const std::string &username){}// 登录函数bool login(const std::string &username, const std::string &password, const std::string &email){}// 注册用户bool registerUser(const std::string &username, const std::string &password, const std::string &email, std::string &resp){}// 获取用户信息Json::Value getUserInfo(const std::string &username){}// 更新用户信息bool updateUserInfo(const std::string &username, const Json::Value &newInfo, std::string &resp) {}
}
根据上面的框架,用户在初始化时会同时初始化存储用户信息的数据库;
登录与注册
下面是实现的登录界面,服务器提取用户输入的内容后,将参数传给底层,底层调用login函数,再将响应发回客户端,实现登录:
- 具体的login与register并不困难:
login()
: 通过调用executeSelect函数,直接获取数据库中的用户信息,再对比即可。register()
: 同样调用函数,在判断用户信息不重复有效之后,将数据插入到用户表中。
// 登录函数
bool login(const std::string &username, const std::string &password, const std::string &email)
{// 查询是否存在指定用户名的用户std::string query = "SELECT password, email FROM users WHERE username = '" + username + "';";auto results = dbManager.executeSelect(query);// 检查用户是否存在if (!results.empty() && results[0].size() >= 2){// 用户存在,提取存储的密码和电子邮件std::string storedPassword = results[0][0]; // 第0列是passwordstd::string storedEmail = results[0][1]; // 第1列是emailLOG(INFO) << "数据库信息: " << "密码: " << storedPassword << " " << "电子邮件:" << storedEmail << std::endl;// LOG(INFO) << "用户输入信息: " << "密码: " << password << " " << "电子邮件:" << email << std::endl;// 验证密码和电子邮件if (storedPassword == password && storedEmail == email){LOG(DEBUG) << "登录成功" << std::endl;return true;}else{LOG(DEBUG) << "登录失败, 信息不匹配" << std::endl;return false;}}else{LOG(DEBUG) << "登录失败, 用户不存在" << std::endl;return false;}
}bool isUserExist(const std::string &username)
{std::string query = "SELECT username FROM users WHERE username = '" + username + "';";auto results = dbManager.executeSelect(query);return !results.empty() && !results[0].empty();
}bool registerUser(const std::string &username, const std::string &password, const std::string &email, std::string &resp)
{LOG(DEBUG) << "开始注册用户" << std::endl;// 检查用户名是否已经存在std::string checkUsernameQuery = "SELECT 1 FROM users WHERE username = '" + username + "';";auto usernameResults = dbManager.executeSelect(checkUsernameQuery);if (!usernameResults.empty()){// 用户名已存在resp = "注册失败,用户名已存在";return false;}// 检查邮箱是否已经存在std::string checkEmailQuery = "SELECT 1 FROM users WHERE email = '" + email + "';";auto emailResults = dbManager.executeSelect(checkEmailQuery);if (!emailResults.empty()){// 邮箱已存在resp = "注册失败,邮箱已存在";return false;}LOG(DEBUG) << username << " " << password << " " << email << std::endl;// 用户名和邮箱都不存在,执行插入操作std::string insertQuery = "INSERT INTO users (username, password, email) VALUES ('" + username + "', '" + password + "', '" + email + "');";bool success = dbManager.executeQuery(insertQuery);if (success){LOG(INFO) << "注册用户成功" << std::endl;resp = "注册成功";return true;}else{resp = "注册失败,数据库错误";return false;}
}
获取用户信息 与 修改用户信息
- getUserInfo 与 updateUserInfo 一致:均调用executeSelete后,前者获取用户信息封装成Json:Value,后者直接更新数据库信息。
// 获取用户信息
Json::Value getUserInfo(const std::string &username)
{LOG(DEBUG) << "getUserInfo代码执行" << "\n";Json::Value userInfo;// 查询用户信息std::string query = "SELECT * FROM users WHERE username = '" + username + "';";auto results = dbManager.executeSelect(query);std::string errMsg = dbManager.getErrorMessage();if (!errMsg.empty()){LOG(ERROR) << "查询用户信息失败: " << errMsg << std::endl;userInfo["error"] = errMsg;return userInfo;}// 检查用户是否存在if (!results.empty() && !results[0].empty()){// 用户存在,提取存储的信息if (results[0].size() > 0)userInfo["id"] = results[0][0];if (results[0].size() > 1)userInfo["username"] = results[0][1];if (results[0].size() > 2)userInfo["password"] = results[0][2];if (results[0].size() > 3)userInfo["email"] = results[0][3];if (results[0].size() > 4)userInfo["phone"] = results[0][4];if (results[0].size() > 5)userInfo["gender"] = results[0][5];if (results[0].size() > 6)userInfo["description"] = results[0][6];}else{// 用户不存在userInfo["error"] = "用户不存在";}// LOG(DEBUG) << "获取用户信息: " << userInfo.toStyledString() << std::endl;return userInfo;
}// 更新用户信息
bool updateUserInfo(const std::string &username, const Json::Value &newInfo, std::string &resp) {LOG(DEBUG) << "updateUserInfo代码执行" << "\n";// 检查用户是否存在Json::Value userInfo = getUserInfo(username);if (userInfo.isMember("error")) {resp = userInfo["error"].asString();return false;}// 构建更新语句std::string updateQuery = "UPDATE users SET ";bool first = true;if (newInfo.isMember("password")) {updateQuery += "password = '" + newInfo["password"].asString() + "'";first = false;}if (newInfo.isMember("email")) {if (!first) updateQuery += ", ";updateQuery += "email = '" + newInfo["email"].asString() + "'";first = false;}if (newInfo.isMember("phone")) {if (!first) updateQuery += ", ";updateQuery += "phone = '" + newInfo["phone"].asString() + "'";first = false;}if (newInfo.isMember("gender")) {if (!first) updateQuery += ", ";std::string gender = newInfo["gender"].asString();if (gender == "男") {gender = "Male";} else if (gender == "女") {gender = "Female";} else {gender = "Other";}updateQuery += "gender = '" + gender + "'";first = false;}if (newInfo.isMember("description")) {if (!first) updateQuery += ", ";updateQuery += "description = '" + newInfo["description"].asString() + "'";}updateQuery += " WHERE username = '" + username + "';";// 执行更新if (dbManager.executeQuery(updateQuery)) {std::cout << "用户信息更新成功!" << std::endl;return true;} else {resp = "更新失败: " + dbManager.getErrorMessage();LOG(ERROR) << "更新用户信息失败: " << resp << std::endl;return false;}
}
注销账户
// 注销函数
bool logoff(const std::string &username)
{// 查询是否存在指定用户名的用户std::string query = "SELECT username FROM users WHERE username = '" + username + "';";auto results = dbManager.executeSelect(query);// 检查用户是否存在if (!results.empty() && results[0].size() >= 1){// 用户存在,记录注销日志query = "delete from users where username = '" + username + "';";dbManager.executeQuery(query);LOG(DEBUG) << "注销成功,用户: " << username << std::endl;return true;}else{LOG(DEBUG) << "注销失败,用户不存在: " << username << std::endl;return false;}
}
此时用户类封装完毕:
3. 前端部分(与后端交互)
① 登录
下面简单看一下前端代码:
login.html
<div class="content"><h1 class="font_">用户登录</h1><div class="login-form"><form id="login-form" action="/api/login" method="post"><!-- <form id="login-form" action="/login.html" method="post"> --><input type="text" id="username" name="username" placeholder="用户名" required><br><input type="email" id="email" name="email" placeholder="电子邮件" required><br><input type="password" id="password" name="password" placeholder="密码" required><br><input type="button" value="还没有账号?点击注册" onclick="window.location.href='/register.html';"><br><input type="submit" value="登录"></form></div>
</div>
登录模块在login.html的content部分,下面是其js代码:
js部分在用户点击了登录按钮后,给服务器发送一个post请求,随后接收服务器处理的响应:
- 根据登录结果,弹出提示框;如果登录成功
document.addEventListener('DOMContentLoaded', () => {document.getElementById('login-form').addEventListener('submit', (event) => {event.preventDefault(); // 防止表单默认提交const username = document.getElementById('username').value;const password = document.getElementById('password').value;const email = document.getElementById('email').value;fetch('/api/login', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded'},body: new URLSearchParams({'username': username,'password': password,'email': email})}).then(response => {return response.text().then(text => {try {return JSON.parse(text);} catch (e) {throw new Error('服务器响应不是有效的 JSON: ' + text);}});}).then(data => {if (data.status === 'success') {alert("登录成功!");window.location.href = '/userInfo.html';} else {alert(data.message);}}).catch(error => {console.error('请求失败:', error);alert('发生错误,请重试!');});});
});
服务器对登录请求的处理
svr->Post("/api/login", [&users](const httplib::Request &req, httplib::Response &resp)
{LOG(INFO) << "Received a login request" << std::endl;std::string username = req.get_param_value("username");std::string password = req.get_param_value("password");std::string email = req.get_param_value("email");Json::Value responseJson;if (user.login(username, password, email)) {responseJson["status"] = "success";responseJson["message"] = "登录成功";// 生成会话 IDstd::string session_id = StringUtil::generate_session_id();session_store[session_id] = username;// 设置会话 ID 到 Cookieresp.set_header("Set-Cookie", "session_id=" + session_id);// 设置响应头和状态码resp.status = 200; // 使用 200 状态码resp.set_header("Content-Type", "application/json");resp.set_content(responseJson.toStyledString(), "application/json");} else {responseJson["status"] = "error";responseJson["message"] = "登录失败: 用户名或密码错误";resp.status = 200; // 使用 200 状态码resp.set_header("Content-Type", "application/json");resp.set_content(responseJson.toStyledString(), "application/json");} });
② 注册
注册功能与登录如出一辙,regiser.html的部分内容:
<div class="content"><h1 class="font_">用户注册</h1><div class="register-form"><form id="register-form" action="/api/register" method="post"><!-- <form id="register-form"> --><input type="text" id = "username" name="username" placeholder="用户名" required><br><input type="email" id = "email" name="email" placeholder="电子邮件" required><br><input type="password" id = "password" name="password" placeholder="密码" required><br><input type="submit" value="注册"><br><input type="button" value="返回登录" onclick="window.location.href='/login.html';"></form><!-- 用于显示响应消息 --><div id="response-message" style="color: red; margin-top: 10px;"></div></div>
</div>
对于js代码,与登录功能基本一致,这里不放出来:
服务器对注册请求的处理
svr->Post("/api/register", [&users](const httplib::Request &req, httplib::Response &resp)
{LOG(INFO) << "Received a register request" << std::endl;std::string username = req.get_param_value("username");std::string email = req.get_param_value("email");std::string password = req.get_param_value("password");std::string message;// 创建一个 JSON 对象Json::Value jsonResponse;// 假设 user.registerUser 返回一个 bool 表示成功或失败bool success = user.registerUser(username, password, email, message);// 设置 JSON 响应内容jsonResponse["status"] = success ? "success" : "error";jsonResponse["message"] = message;// 将 JSON 对象转换为字符串Json::StreamWriterBuilder writer;std::string jsonString = Json::writeString(writer, jsonResponse);// 设置响应内容resp.set_content(jsonString, "application/json; charset=utf-8");resp.status = success ? 200 : 400; // 根据成功或失败设置状态码
});
③ 查看登录用户的信息
该功能用于实现查看已登录用户的相关信息。
userInfo.html
<div class="content"><div class="profile"><h1>用户信息</h1><p><strong>姓名:</strong> <span id="username"></span></p><p><strong>电子邮件:</strong> <span id="email"></span></p><p><strong>电话:</strong> <span id="phone"></span></p><p><strong>性别:</strong> <span id="gender"></span></p><p><strong>个人描述:</strong> <span id="description"></span></p></div><div class="form-button"><input type="button" value="返回首页" onclick="window.location.href='/index.html';"><br></div></div><!-- 遮罩层 --><div id="editModalBackdrop"></div><!-- 模态窗口 --><div id="editModal"><h2>编辑个人信息</h2><form id="editForm"><label for="newName">姓名:</label><input type="text" id="newName" name="newName"><label for="newEmail">邮箱:</label><input type="email" id="newEmail" name="newEmail"><label for="newPhone">电话:</label><input type="tel" id="newPhone" name="newPhone"><label for="newGender">性别:</label><select id="newGender" name="newGender"><option value="男">男</option><option value="女">女</option></select><label for="newDescription">描述:</label><textarea id="newDescription" name="newDescription"></textarea><div class="form-buttons"><button type="submit" class="btn-primary">保存</button><button type="button" id="closeModal" class="btn-secondary">关闭</button></div></form></div>
下面是userInfo.js
:
document.addEventListener('DOMContentLoaded', function () {const editModal = document.getElementById('editModal');const editModalBackdrop = document.getElementById('editModalBackdrop');const editProfileLink = document.getElementById('editProfileLink');const closeModal = document.getElementById('closeModal');const editForm = document.getElementById('editForm');// 从服务器获取用户信息fetch('/api/user-info', {method: "GET",credentials: 'include'}).then(response => response.json()).then(data => {const userInfo = data.data;document.getElementById('username').textContent = userInfo.username;document.getElementById('email').textContent = userInfo.email;document.getElementById('phone').textContent = userInfo.phone;document.getElementById('gender').textContent = userInfo.gender;document.getElementById('description').textContent = userInfo.description;editProfileLink.addEventListener('click', function () {document.getElementById('newName').value = userInfo.username;document.getElementById('newEmail').value = userInfo.email;document.getElementById('newPhone').value = userInfo.phone;document.getElementById('newGender').value = userInfo.gender;document.getElementById('newDescription').value = userInfo.description;editModal.style.display = 'block';editModalBackdrop.style.display = 'block';});}).catch(error => console.error('Error fetching user info:', error));
});
服务器对查看用户信息请求的处理
svr->Get("/api/user-info", [](const httplib::Request &req, httplib::Response &resp)
{LOG(INFO) << "Received a userInfo request" << std::endl;auto cookies = req.get_header_value("Cookie");std::string session_id = StringUtil::extract_session_id(cookies);Json::Value responseJson;if (session_store.find(session_id) != session_store.end()) {std::string username = session_store[session_id];// 用户已登录,获取用户信息LOG(INFO) << "用户 \"" << username << "\" 处于登录状态中。" << std::endl;Json::Value userInfo = user.getUserInfo(username);LOG(INFO) << "getInfo执行完毕" << "\n";// 检查 userInfo 是否有效if (!userInfo.isNull()) {responseJson["status"] = "success";responseJson["message"] = "用户信息获取成功";responseJson["data"] = userInfo; // 将用户信息包含在响应中} else {responseJson["status"] = "error";responseJson["message"] = "无法获取用户信息";}} else {// 用户未登录responseJson["status"] = "error";responseJson["message"] = "用户未登录";}LOG(INFO) << "Response JSON: " << responseJson << std::endl;Json::StreamWriterBuilder writer;std::string responseBody = Json::writeString(writer, responseJson);resp.set_content(responseBody, "application/json; charset=utf-8"); });
在上面的代码中,我们如何记录当前主机的登录用户?👇
【跳转】会话ID的记录
④ 更新用户信息
更新用户信息我们将其作为一个模态窗口:
closeModal.addEventListener('click', function () {editModal.style.display = 'none';editModalBackdrop.style.display = 'none';});editModalBackdrop.addEventListener('click', function () {editModal.style.display = 'none';editModalBackdrop.style.display = 'none';
});editForm.addEventListener('submit', function (event) {event.preventDefault();const updatedUserInfo = {username: document.getElementById('newName').value,email: document.getElementById('newEmail').value,phone: document.getElementById('newPhone').value,gender: document.getElementById('newGender').value,description: document.getElementById('newDescription').value};// 发送更新请求到服务器fetch('/api/update-user-info', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify(updatedUserInfo),credentials: 'include'}).then(response => response.json()).then(data => {if (data.status === 'success') {document.getElementById('username').textContent = updatedUserInfo.username;document.getElementById('email').textContent = updatedUserInfo.email;document.getElementById('phone').textContent = updatedUserInfo.phone;document.getElementById('gender').textContent = updatedUserInfo.gender;document.getElementById('description').textContent = updatedUserInfo.description;editModal.style.display = 'none';editModalBackdrop.style.display = 'none';} else {console.error('Error updating user info:', data.message);}}).catch(error => console.error('Error updating user info:', error));
});
同理于前面的代码,这里不再写出其他代码,请在源码处查看。
⑤ 登出用户 & 注销用户
根据上面提供的登录、注册、查看用户信息的html、js与服务器处理的c++代码,对于这两个功能的实现已经很简单了(属于userInfo.html的子部分):
// 登出用户const logoutLink = document.querySelector("#logoutLink");logoutLink.addEventListener("click", function (event) {event.preventDefault(); // 阻止默认行为if (confirm("确定要退出账户吗?")) {fetch('/api/logout', {method: 'GET',credentials: 'include' // 确保包括 Cookie}).then(response => {if (response.redirected) {window.location.href = response.url;}}).catch(error => {console.error('登出请求失败:', error);});}});// 注销账户const logoffLink = document.querySelector("#logoffLink");logoffLink.addEventListener("click", function (event) {event.preventDefault(); // 阻止默认行为if (confirm("确定要注销账户吗?")) {fetch('/api/log-off', {method: 'POST', // 将 GET 更改为 POST 方法credentials: 'include' // 确保包括 Cookie}).then(response => {if (response.redirected) {window.location.href = response.url;}}).catch(error => {console.error('登出请求失败:', error);});}});
注意
关于记录用户信息 - 会话ID
在实现 登录、查看用户信息等功能时,需要思考的问题是,网站如何知道当前主机的登录用户是哪一个?
这里就用到cookie的概念:
Cookie 是一种由服务器发送到客户端并存储在客户端设备上的小型数据文件。它们通常用于保存用户的会话状态、偏好设置或其他跟踪信息。当用户重新访问网站时,浏览器会将这些 Cookie 发送回服务器,帮助服务器识别用户并维持会话。例如,登录状态、购物车内容等信息通常通过 Cookie 进行管理。
就像上面代码中服务器响应的部分:
// 生成会话 ID
std::string session_id = StringUtil::generate_session_id();
session_store[session_id] = username;// 设置会话 ID 到 Cookie
resp.set_header("Set-Cookie", "session_id=" + session_id);
对于上面的generate_session_id(),是我们自实现的模拟生成会话id的功能,通过生成会话id与从Cookie中获取id,就可以实现在网站中找到当前登录用户的信息
// 生成一个随机的32位session_id
static std::string generate_session_id()
{std::stringstream ss;std::random_device rd;std::mt19937 mt(rd());std::uniform_int_distribution<int> dist(0, 15);for (int i = 0; i < 32; ++i){int r = dist(mt);ss << (r < 10 ? char('0' + r) : char('a' + r - 10));}return ss.str();
}// 从cookies中提取session_id
static std::string extract_session_id(const std::string &cookies)
{std::string session_id;std::istringstream cookie_stream(cookies);std::string cookie;while (std::getline(cookie_stream, cookie, ';')){std::string::size_type pos = cookie.find("session_id=");if (pos != std::string::npos){session_id = cookie.substr(pos + 11); // 11 is the length of "session_id="break;}}return session_id;
}