diff --git a/Application.cpp b/Application.cpp index 237d9e5..7f2669f 100644 --- a/Application.cpp +++ b/Application.cpp @@ -7,6 +7,7 @@ #include "HttpSession.h" #include "Router/router.hpp" #include "ServiceLogic.h" +#include "SessionStore.h" #include "Settings.h" #include "WeChat/Corporation/Context.h" #include @@ -31,6 +32,7 @@ Application::Application() : m_d{new ApplicationPrivate()} { m_database = Singleton::construct(); m_database->open(m_settings->sqlitePath()); + m_sessionStore = Singleton::construct(*m_ioContext->ioContext()); m_corporationContext = Singleton::construct(*m_ioContext->ioContext()); m_corporationContext->start(); @@ -84,6 +86,7 @@ int Application::exec() { auto settings = Singleton::instance(); ServiceLogic::live2dBackend(); ServiceLogic::visitAnalysis(); + ServiceLogic::userAccount(); startAcceptHttpConnections(settings->server(), settings->port()); m_ioContext->run(); return 0; diff --git a/Application.h b/Application.h index 4a85161..5a4f743 100644 --- a/Application.h +++ b/Application.h @@ -23,6 +23,7 @@ class Settings; class ApplicationPrivate; class HttpSession; class Database; +class SessionStore; class Application : public std::enable_shared_from_this { public: @@ -43,6 +44,7 @@ private: std::shared_ptr m_settings; std::shared_ptr m_ioContext; std::shared_ptr m_messageManager; + std::shared_ptr m_sessionStore; std::shared_ptr m_database; std::shared_ptr m_corporationContext; }; diff --git a/Base/CMakeLists.txt b/Base/CMakeLists.txt new file mode 100644 index 0000000..15ac834 --- /dev/null +++ b/Base/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(Base + DataStructure.h DataStructure.cpp +) + +get_filename_component(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +target_include_directories(Base + INTERFACE ${PARENT_DIR} +) + +target_link_libraries(Base + PRIVATE OpenSSL::Crypto +) \ No newline at end of file diff --git a/Base/DataStructure.cpp b/Base/DataStructure.cpp new file mode 100644 index 0000000..8ed1bef --- /dev/null +++ b/Base/DataStructure.cpp @@ -0,0 +1,71 @@ +#include "DataStructure.h" +#include +#include +#include +#include +#include + +namespace Older { +int PBKDF2_ITERATIONS = 100000; +constexpr int SALT_LENGTH = 16; +constexpr int HASH_LENGTH = 32; + +Account Account::hashPassword(const std::string &password) { + Account ret; + + // 生成随机盐 + ret.salt.resize(SALT_LENGTH); + if (RAND_bytes(ret.salt.data(), SALT_LENGTH) != 1) { + throw std::runtime_error("Salt generation failed"); + } + + // PBKDF2 派生 + EVP_KDF *kdf = EVP_KDF_fetch(nullptr, "PBKDF2", nullptr); + EVP_KDF_CTX *ctx = EVP_KDF_CTX_new(kdf); + + OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string("digest", "SHA256", 0), + OSSL_PARAM_construct_octet_string("salt", ret.salt.data(), ret.salt.size()), + OSSL_PARAM_construct_octet_string("pass", (void *)password.data(), password.size()), + OSSL_PARAM_construct_int("iter", &PBKDF2_ITERATIONS), OSSL_PARAM_construct_end()}; + + ret.passwordHash.resize(HASH_LENGTH); + if (EVP_KDF_derive(ctx, ret.passwordHash.data(), HASH_LENGTH, params) != 1) { + EVP_KDF_CTX_free(ctx); + EVP_KDF_free(kdf); + throw std::runtime_error("PBKDF2 failed"); + } + + EVP_KDF_CTX_free(ctx); + EVP_KDF_free(kdf); + return ret; +} + +bool Account::verifyPassword(const Account &account, const std::string &password) { + // 重新计算哈希 + EVP_KDF *kdf = EVP_KDF_fetch(nullptr, "PBKDF2", nullptr); + EVP_KDF_CTX *ctx = EVP_KDF_CTX_new(kdf); + + OSSL_PARAM params[5] = {OSSL_PARAM_construct_utf8_string("digest", const_cast("SHA256"), 0), + OSSL_PARAM_construct_octet_string("salt", (void *)account.salt.data(), account.salt.size()), + OSSL_PARAM_construct_octet_string("pass", const_cast(password.data()), password.size()), + OSSL_PARAM_construct_int("iter", &PBKDF2_ITERATIONS), OSSL_PARAM_construct_end()}; + + std::vector new_hash(HASH_LENGTH); + if (EVP_KDF_derive(ctx, new_hash.data(), HASH_LENGTH, params) != 1) { + EVP_KDF_CTX_free(ctx); + EVP_KDF_free(kdf); + return false; + } + + EVP_KDF_CTX_free(ctx); + EVP_KDF_free(kdf); + + // 安全比较(防时序攻击) + return CRYPTO_memcmp(account.passwordHash.data(), new_hash.data(), HASH_LENGTH) == 0; +} + +bool Account::validateEmail(const std::string &email) { + const std::regex pattern(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"); + return std::regex_match(email, pattern); +} +} // namespace Older \ No newline at end of file diff --git a/Base/DataStructure.h b/Base/DataStructure.h index 7530eea..a039483 100644 --- a/Base/DataStructure.h +++ b/Base/DataStructure.h @@ -1,17 +1,34 @@ #ifndef __DATASTRUCTURE_H__ #define __DATASTRUCTURE_H__ +#include #include +#include -struct VisitorStats{ +namespace Older { +struct VisitorStats { std::string url; - int visitorCount=0; - int totalViews =0; - int64_t lastViewTime =0; + int visitorCount = 0; + int totalViews = 0; + int64_t lastViewTime = 0; }; -struct SiteStats{ - int totalViews =0; - int totalVisitors =0; +struct SiteStats { + int totalViews = 0; + int totalVisitors = 0; }; + +struct Account { + int64_t id = 0; + std::string username; + std::string email; + std::vector passwordHash; + std::vector salt; + int64_t createdAt = 0; + + static Account hashPassword(const std::string &password); + static bool verifyPassword(const Account &account, const std::string &password); + static bool validateEmail(const std::string& email); +};} + #endif // __DATASTRUCTURE_H__ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index a9909e8..6d82777 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,10 @@ find_package(Boost REQUIRED COMPONENTS json) +find_package(OpenSSL REQUIRED) add_subdirectory(/root/Projects/Kylin Kylin) add_subdirectory(3rdparty) +add_subdirectory(Base) +add_subdirectory(UnitTest) add_executable(Older main.cpp WeChat/Corporation/Context.h WeChat/Corporation/Context.cpp @@ -11,6 +14,7 @@ add_executable(Older main.cpp HttpSession.h HttpSession.cpp ResponseUtility.h ResponseUtility.cpp ServiceLogic.h ServiceLogic.inl ServiceLogic.cpp + SessionStore.h SessionStore.cpp Settings.h Settings.cpp ) @@ -19,6 +23,7 @@ target_include_directories(Older ) target_link_libraries(Older + PRIVATE Base PRIVATE Kylin::Core PRIVATE Kylin::Http PRIVATE Kylin::Router diff --git a/Database.cpp b/Database.cpp index 7ea7853..63f6e03 100644 --- a/Database.cpp +++ b/Database.cpp @@ -114,8 +114,118 @@ SiteStats Database::siteStats() { return ret; } +void Database::createUser(const Account &account) { + if (!Account::validateEmail(account.email)) { + throw std::runtime_error("Invalid email format"); + } + + sqlite3_stmt *statement = nullptr; + const char *sql = "INSERT INTO users (username, email, password_hash, salt, created_at) VALUES (?, ?, ?, ?, ?);"; + + if (sqlite3_prepare_v2(m_sqlite, sql, -1, &statement, nullptr) != SQLITE_OK) { + throw std::runtime_error(sqlite3_errmsg(m_sqlite)); + } + + sqlite3_bind_text(statement, 1, account.username.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(statement, 2, account.email.c_str(), -1, SQLITE_STATIC); // 新增绑定 + sqlite3_bind_blob(statement, 3, account.passwordHash.data(), account.passwordHash.size(), SQLITE_STATIC); + sqlite3_bind_blob(statement, 4, account.salt.data(), account.salt.size(), SQLITE_STATIC); + sqlite3_bind_int64(statement, 5, account.createdAt); + + int rc = sqlite3_step(statement); + sqlite3_finalize(statement); + + if (rc != SQLITE_DONE) { + const char *message = sqlite3_errmsg(m_sqlite); + if (std::string(message).find("UNIQUE") != std::string::npos) { + throw std::runtime_error("username or email already exists"); + } + throw std::runtime_error(message); + } +} + +Account Database::user(const std::string &identifier) const { + sqlite3_stmt *stmt; + const char *sql = "SELECT id, username, email, password_hash, salt, created_at " + "FROM users WHERE username = ? OR email = ?;"; + + if (sqlite3_prepare_v2(m_sqlite, sql, -1, &stmt, nullptr) != SQLITE_OK) { + throw std::runtime_error(sqlite3_errmsg(m_sqlite)); + } + + sqlite3_bind_text(stmt, 1, identifier.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, identifier.c_str(), -1, SQLITE_STATIC); + + if (sqlite3_step(stmt) != SQLITE_ROW) { + sqlite3_finalize(stmt); + throw std::runtime_error("User not found"); + } + + Account ret; + + ret.id = sqlite3_column_int(stmt, 0); + + ret.username = reinterpret_cast(sqlite3_column_text(stmt, 1)); + ret.email = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + const void *hash_blob = sqlite3_column_blob(stmt, 3); + int hash_size = sqlite3_column_bytes(stmt, 3); + ret.passwordHash.resize(hash_size); + memcpy(ret.passwordHash.data(), hash_blob, hash_size); + + const void *salt_blob = sqlite3_column_blob(stmt, 4); + int salt_size = sqlite3_column_bytes(stmt, 4); + ret.salt.resize(salt_size); + memcpy(ret.salt.data(), salt_blob, salt_size); + + ret.createdAt = sqlite3_column_int64(stmt, 5); + + sqlite3_finalize(stmt); + return ret; +} + +Account Database::user(int64_t id) const { + sqlite3_stmt *stmt; + const char *sql = "SELECT id, username, email, password_hash, salt, created_at " + "FROM users WHERE id = ?;"; + + if (sqlite3_prepare_v2(m_sqlite, sql, -1, &stmt, nullptr) != SQLITE_OK) { + throw std::runtime_error(sqlite3_errmsg(m_sqlite)); + } + + sqlite3_bind_int64(stmt, 1, id); + + if (sqlite3_step(stmt) != SQLITE_ROW) { + sqlite3_finalize(stmt); + throw std::runtime_error("User not found"); + } + + Account ret; + + ret.id = sqlite3_column_int(stmt, 0); + + ret.username = reinterpret_cast(sqlite3_column_text(stmt, 1)); + ret.email = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + const void *hash_blob = sqlite3_column_blob(stmt, 3); + int hash_size = sqlite3_column_bytes(stmt, 3); + ret.passwordHash.resize(hash_size); + memcpy(ret.passwordHash.data(), hash_blob, hash_size); + + const void *salt_blob = sqlite3_column_blob(stmt, 4); + int salt_size = sqlite3_column_bytes(stmt, 4); + ret.salt.resize(salt_size); + memcpy(ret.salt.data(), salt_blob, salt_size); + + ret.createdAt = sqlite3_column_int64(stmt, 5); + + sqlite3_finalize(stmt); + return ret; +} + void Database::initialize() { createVisitAnalysisTable(); + createUsersTable(); } void Database::createVisitAnalysisTable() { @@ -138,4 +248,23 @@ void Database::createVisitAnalysisTable() { sqlite3_free(message); } } -} \ No newline at end of file + +void Database::createUsersTable() { + const char *sql = R"( + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash BLOB NOT NULL, + salt BLOB NOT NULL, + created_at INTEGER NOT NULL + ); + )"; + char *message = nullptr; + int rc = sqlite3_exec(m_sqlite, sql, nullptr, nullptr, &message); + if (rc != SQLITE_OK) { + LOG(error) << "Error creating table: " << message; + sqlite3_free(message); + } +} +} // namespace Older \ No newline at end of file diff --git a/Database.h b/Database.h index 2b266c0..34700c3 100644 --- a/Database.h +++ b/Database.h @@ -18,9 +18,13 @@ public: std::list mostViewedUrls(int n); std::list latestViewedUrls(int n); SiteStats siteStats(); + void createUser(const Account &account); + Account user(const std::string &identifier)const; + Account user(int64_t id)const; protected: void createVisitAnalysisTable(); + void createUsersTable(); void initialize(); private: diff --git a/ServiceLogic.cpp b/ServiceLogic.cpp index 502983b..0b8a173 100644 --- a/ServiceLogic.cpp +++ b/ServiceLogic.cpp @@ -2,12 +2,32 @@ #include "Core/Singleton.h" #include "Database.h" #include "HttpSession.h" +#include "SessionStore.h" #include "Settings.h" #include namespace ServiceLogic { using namespace boost::beast; +std::string extractToken(const std::string &cookieHeader, const std::string &tokenName = "access_token") { + // 格式示例:"access_token=abc123; Path=/; Expires=Wed, 21 Oct 2023 07:28:00 GMT" + size_t startPos = cookieHeader.find(tokenName + "="); + if (startPos == std::string::npos) { + return ""; + } + startPos += tokenName.size() + 1; // 跳过 "token_name=" + size_t endPos = cookieHeader.find(';', startPos); + if (endPos == std::string::npos) { + endPos = cookieHeader.size(); + } + std::string token = cookieHeader.substr(startPos, endPos - startPos); + + // 移除可能的引号和空格 + token.erase(std::remove(token.begin(), token.end(), '"'), token.end()); + token.erase(std::remove(token.begin(), token.end(), ' '), token.end()); + return token; +} + boost::beast::http::response serverError(const boost::beast::http::request &request, std::string_view errorMessage) { using namespace boost::beast; @@ -206,4 +226,172 @@ void live2dBackend() { }); } +void userAccount() { + using namespace Core; + auto application = Singleton::instance(); + application->insertUrl("/api/v1/user/register", [](Older::HttpSession &session, const Older::Application::Request &request, + const boost::urls::matches &matches) { + auto rootJson = boost::json::parse(request.body()); + auto &root = rootJson.as_object(); + boost::json::object reply; + if (root.contains("username") && root.contains("password") && root.contains("email")) { + try { + auto account = Older::Account::hashPassword(std::string(root.at("password").as_string())); + account.username = root.at("username").as_string(); + account.email = root.at("email").as_string(); + auto database = Singleton::instance(); + database->createUser(account); + + reply["code"] = 200; + reply["message"] = "register success"; + account = database->user(account.username); + + boost::json::object data; + data["user_id"] = account.id; + reply["data"] = std::move(data); + } catch (const std::exception &e) { + reply["code"] = 400; + reply["message"] = e.what(); + } + + } else { + reply["code"] = 400; + reply["message"] = "missing username, password or email"; + } + http::response s{boost::beast::http::status::ok, request.version()}; + s.set(http::field::server, BOOST_BEAST_VERSION_STRING); + s.set(http::field::content_type, "application/json;charset=UTF-8"); + s.keep_alive(request.keep_alive()); + s.body() = boost::json::serialize(reply); + s.prepare_payload(); + session.reply(std::move(s)); + }); + + application->insertUrl("/api/v1/user/login", [](Older::HttpSession &session, const Older::Application::Request &request, + const boost::urls::matches &matches) { + auto rootJson = boost::json::parse(request.body()); + auto &root = rootJson.as_object(); + boost::json::object reply; + std::string cookie; + if (root.contains("username") && root.contains("password")) { + try { + auto database = Singleton::instance(); + auto sessions = Singleton::instance(); + auto account = database->user(std::string(root.at("username").as_string())); + bool logined = Older::Account::verifyPassword(account, std::string(root.at("password").as_string())); + + reply["code"] = logined ? 200 : 404; + reply["message"] = logined ? "login success" : "wrong password or user"; + account = database->user(account.username); + if (logined) { + boost::json::object data; + data["user_id"] = account.id; + reply["data"] = std::move(data); + // clang-format off + cookie = "older_auth=" + sessions->addSession(account.id) + + "; Path=/" + + "; HttpOnly" // 阻止JS访问 + + "; Secure" // 仅HTTPS传输 + + "; SameSite=Strict" // 防止CSRF + + "; Max-Age=86400"; // 24小时过期 + // clang-format on + } + } catch (const std::exception &e) { + reply["code"] = 400; + reply["message"] = e.what(); + } + } else { + reply["code"] = 400; + reply["message"] = "missing username, password or email"; + } + http::response s{boost::beast::http::status::ok, request.version()}; + s.set(http::field::server, BOOST_BEAST_VERSION_STRING); + s.set(http::field::content_type, "application/json;charset=UTF-8"); + if (!cookie.empty()) { + s.set(http::field::set_cookie, cookie); + } + s.keep_alive(request.keep_alive()); + s.body() = boost::json::serialize(reply); + s.prepare_payload(); + session.reply(std::move(s)); + }); + + application->insertUrl("/api/v1/user/verify", [](Older::HttpSession &session, const Older::Application::Request &request, + const boost::urls::matches &matches) { + auto cookies = request.find(http::field::cookie); + boost::json::object reply; + std::string cookie; + if (cookies == request.end()) { + reply["code"] = 401; + reply["message"] = "cookie is not exists"; + } else { + auto sessions = Singleton::instance(); + auto database = Singleton::instance(); + std::string accessToken = extractToken(cookies->value(), "older_auth"); + auto [valid, newAccessToken] = sessions->validateAndRefresh(accessToken); + + if (valid) { + if (newAccessToken != accessToken) { // 需要刷新令牌 + // clang-format off + cookie = "older_auth=" + newAccessToken + + "; Path=/" + + "; HttpOnly" // 阻止JS访问 + + "; Secure" // 仅HTTPS传输 + + "; SameSite=Strict" // 防止CSRF + + "; Max-Age=86400"; // 24小时过期 + // clang-format on + } + reply["code"] = 200; + reply["message"] = "verify success"; + boost::json::object data; + auto d = sessions->at(newAccessToken); + auto account = database->user(d.userId); + data["user_id"] = d.userId; + data["username"] = account.username; + data["email"] = account.email; + reply["data"] = std::move(data); + } else { + reply["code"] = 401; + reply["message"] = "cookie is invalid"; + } + } + + http::response s{boost::beast::http::status::ok, request.version()}; + s.set(http::field::server, BOOST_BEAST_VERSION_STRING); + s.set(http::field::content_type, "application/json;charset=UTF-8"); + if (!cookie.empty()) { + s.set(http::field::set_cookie, cookie); + } + s.keep_alive(request.keep_alive()); + s.body() = boost::json::serialize(reply); + s.prepare_payload(); + session.reply(std::move(s)); + }); + + application->insertUrl("/api/v1/user/logout", [](Older::HttpSession &session, const Older::Application::Request &request, + const boost::urls::matches &matches) { + http::response res{boost::beast::http::status::ok, request.version()}; + try { + auto sessions = Singleton::instance(); + auto cookies = request.find(http::field::cookie); + if (cookies != request.end()) { + std::string accessToken = extractToken(cookies->value(), "older_auth"); + if (!accessToken.empty()) { + sessions->removeSession(accessToken); + } + } + + res.set(http::field::content_type, "application/json"); + res.set(http::field::set_cookie, "older_auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure"); + res.set(http::field::set_cookie, + "older_refresh_token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure"); + res.body() = R"({"code": 401, "message": "logout successfully"})"; + } catch (const std::exception &e) { + res.result(http::status::internal_server_error); + res.body() = R"({"status": "error", "message": ")" + std::string(e.what()) + "\"}"; + } + session.reply(std::move(res)); + }); +} + } // namespace ServiceLogic diff --git a/ServiceLogic.h b/ServiceLogic.h index 6fa70ab..8f2bbe2 100644 --- a/ServiceLogic.h +++ b/ServiceLogic.h @@ -43,6 +43,7 @@ boost::beast::http::response make_200(const boost::beast::http::re void live2dBackend(); void visitAnalysis(); +void userAccount(); diff --git a/SessionStore.cpp b/SessionStore.cpp new file mode 100644 index 0000000..7fafe20 --- /dev/null +++ b/SessionStore.cpp @@ -0,0 +1,106 @@ +#include "SessionStore.h" +#include +#include +namespace Older { +SessionStore::SessionStore(boost::asio::io_context &ioContext) : m_ioContext(ioContext), m_cleanupTimer(ioContext) { + startCleanupTask(); +} + +std::string SessionStore::addSession(int userId, std::chrono::minutes sessionLifetime, std::chrono::minutes refreshInterval) { + using namespace boost::uuids; + using namespace std::chrono; + std::unique_lock lock(m_mutex); + + random_generator uuid_gen; + auto now = system_clock::now(); + + SessionData data{userId, + to_string(uuid_gen()), // 生成刷新令牌 + now + sessionLifetime, now + refreshInterval}; + + std::string access_token = to_string(uuid_gen()); + m_sessions[access_token] = data; + + m_refreshMap[data.refreshToken] = access_token; + + return access_token; +} + +void SessionStore::removeSession(const std::string &token) { + std::unique_lock lock(m_mutex); + auto it = m_sessions.find(token); + if (it != m_sessions.end()) { + m_refreshMap.erase(it->second.refreshToken); + m_sessions.erase(it); + } +} + +std::pair SessionStore::validateAndRefresh(const std::string &token) { + using namespace std::chrono; + using namespace boost::uuids; + std::unique_lock lock(m_mutex); + auto it = m_sessions.find(token); + if (it == m_sessions.end()) return {false, ""}; + + auto now = system_clock::now(); + SessionData &data = it->second; + + if (now > data.expireTime) { // 检查是否过期 + m_sessions.erase(it); + m_refreshMap.erase(data.refreshToken); + return {false, ""}; + } + + std::string newToken = token; + if (now > data.refreshTime) { // 检查是否需要刷新 + random_generator uuid_gen; + newToken = to_string(uuid_gen()); + + // 保留刷新令牌 + SessionData newData = data; + newData.refreshTime = now + minutes{15}; + + m_sessions.erase(it); + m_sessions[newToken] = newData; + + m_refreshMap[data.refreshToken] = newToken; + } + + return {true, newToken}; +} + +SessionData SessionStore::at(const std::string &token) { + SessionData ret; + if (m_sessions.count(token) > 0) { + ret = m_sessions.at(token); + } + return ret; +} + +void SessionStore::startCleanupTask() { + using namespace std::chrono_literals; + m_cleanupTimer.expires_after(5min); + m_cleanupTimer.async_wait([this](boost::system::error_code ec) { + if (!ec) { + cleanupExpiredSessions(); + startCleanupTask(); + } + }); +} + +void SessionStore::cleanupExpiredSessions() { + using namespace std::chrono; + std::unique_lock lock(m_mutex); + auto now = system_clock::now(); + + for (auto it = m_sessions.begin(); it != m_sessions.end();) { + if (now > it->second.expireTime) { + m_refreshMap.erase(it->second.refreshToken); + it = m_sessions.erase(it); + } else { + ++it; + } + } +} + +} // namespace Older \ No newline at end of file diff --git a/SessionStore.h b/SessionStore.h new file mode 100644 index 0000000..4f446b5 --- /dev/null +++ b/SessionStore.h @@ -0,0 +1,39 @@ +#ifndef __SESSIONSTORE_H__ +#define __SESSIONSTORE_H__ + +#include +#include +#include + +namespace Older { + +struct SessionData { + int userId; + std::string refreshToken; + std::chrono::system_clock::time_point expireTime; + std::chrono::system_clock::time_point refreshTime; +}; + +class SessionStore { +public: + SessionStore(boost::asio::io_context &ioContext); + std::string addSession(int userId, std::chrono::minutes sessionLifetime = std::chrono::minutes{1440}, // 24小时 + std::chrono::minutes refreshInterval = std::chrono::minutes{15}); + void removeSession(const std::string &token); + std::pair validateAndRefresh(const std::string &token); + SessionData at(const std::string &token); + +protected: + void startCleanupTask(); + void cleanupExpiredSessions(); + +private: + boost::asio::io_context &m_ioContext; + boost::asio::steady_timer m_cleanupTimer; + std::shared_mutex m_mutex; + std::unordered_map m_sessions; + std::unordered_map m_refreshMap; +}; +} // namespace Older + +#endif // __SESSIONSTORE_H__ \ No newline at end of file diff --git a/UnitTest/Account.cpp b/UnitTest/Account.cpp new file mode 100644 index 0000000..d770d78 --- /dev/null +++ b/UnitTest/Account.cpp @@ -0,0 +1,10 @@ +#include "Base/DataStructure.h" +#include + +BOOST_AUTO_TEST_CASE(SyncMessage) { + using namespace Older; + constexpr auto password = "123456"; + auto account = Account::hashPassword(password); + BOOST_CHECK_EQUAL(Account::verifyPassword(account, password), true); + BOOST_CHECK_EQUAL(Account::verifyPassword(account, "23456"), false); +} \ No newline at end of file diff --git a/UnitTest/CMakeLists.txt b/UnitTest/CMakeLists.txt new file mode 100644 index 0000000..49936ae --- /dev/null +++ b/UnitTest/CMakeLists.txt @@ -0,0 +1,11 @@ +find_package(Boost REQUIRED COMPONENTS unit_test_framework) + +add_executable(OlderUnitTest main.cpp + Account.cpp +) + +target_link_libraries(OlderUnitTest + PRIVATE Base + PRIVATE Kylin::Core + PRIVATE Boost::unit_test_framework +) \ No newline at end of file diff --git a/UnitTest/main.cpp b/UnitTest/main.cpp new file mode 100644 index 0000000..ebf9395 --- /dev/null +++ b/UnitTest/main.cpp @@ -0,0 +1,2 @@ +#define BOOST_TEST_MODULE OlderTest +#include "boost/test/included/unit_test.hpp"