实现登录接口。

This commit is contained in:
root 2025-03-01 09:09:30 +00:00
parent 63aa6a6270
commit d1b8bf6342
15 changed files with 608 additions and 8 deletions

View File

@ -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 <boost/asio/strand.hpp>
@ -31,6 +32,7 @@ Application::Application() : m_d{new ApplicationPrivate()} {
m_database = Singleton<Database>::construct();
m_database->open(m_settings->sqlitePath());
m_sessionStore = Singleton<SessionStore>::construct(*m_ioContext->ioContext());
m_corporationContext = Singleton<WeChat::Corporation::Context>::construct(*m_ioContext->ioContext());
m_corporationContext->start();
@ -84,6 +86,7 @@ int Application::exec() {
auto settings = Singleton<Settings>::instance();
ServiceLogic::live2dBackend();
ServiceLogic::visitAnalysis();
ServiceLogic::userAccount();
startAcceptHttpConnections(settings->server(), settings->port());
m_ioContext->run();
return 0;

View File

@ -23,6 +23,7 @@ class Settings;
class ApplicationPrivate;
class HttpSession;
class Database;
class SessionStore;
class Application : public std::enable_shared_from_this<Application> {
public:
@ -43,6 +44,7 @@ private:
std::shared_ptr<Settings> m_settings;
std::shared_ptr<Core::IoContext> m_ioContext;
std::shared_ptr<Core::MessageManager> m_messageManager;
std::shared_ptr<SessionStore> m_sessionStore;
std::shared_ptr<Database> m_database;
std::shared_ptr<WeChat::Corporation::Context> m_corporationContext;
};

12
Base/CMakeLists.txt Normal file
View File

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

71
Base/DataStructure.cpp Normal file
View File

@ -0,0 +1,71 @@
#include "DataStructure.h"
#include <openssl/evp.h>
#include <openssl/kdf.h>
#include <openssl/rand.h>
#include <regex>
#include <stdexcept>
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<char *>("SHA256"), 0),
OSSL_PARAM_construct_octet_string("salt", (void *)account.salt.data(), account.salt.size()),
OSSL_PARAM_construct_octet_string("pass", const_cast<char *>(password.data()), password.size()),
OSSL_PARAM_construct_int("iter", &PBKDF2_ITERATIONS), OSSL_PARAM_construct_end()};
std::vector<unsigned char> 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

View File

@ -1,17 +1,34 @@
#ifndef __DATASTRUCTURE_H__
#define __DATASTRUCTURE_H__
#include <cstdint>
#include <string>
#include <vector>
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<uint8_t> passwordHash;
std::vector<uint8_t> 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__

View File

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

View File

@ -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<const char *>(sqlite3_column_text(stmt, 1));
ret.email = reinterpret_cast<const char *>(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<const char *>(sqlite3_column_text(stmt, 1));
ret.email = reinterpret_cast<const char *>(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);
}
}
}
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

View File

@ -18,9 +18,13 @@ public:
std::list<VisitorStats> mostViewedUrls(int n);
std::list<VisitorStats> 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:

View File

@ -2,12 +2,32 @@
#include "Core/Singleton.h"
#include "Database.h"
#include "HttpSession.h"
#include "SessionStore.h"
#include "Settings.h"
#include <sstream>
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<boost::beast::http::string_body>
serverError(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view errorMessage) {
using namespace boost::beast;
@ -206,4 +226,172 @@ void live2dBackend() {
});
}
void userAccount() {
using namespace Core;
auto application = Singleton<Older::Application>::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<Older::Database>::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<boost::beast::http::string_body> 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<Older::Database>::instance();
auto sessions = Singleton<Older::SessionStore>::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<boost::beast::http::string_body> 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<Older::SessionStore>::instance();
auto database = Singleton<Older::Database>::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<boost::beast::http::string_body> 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<boost::beast::http::string_body> res{boost::beast::http::status::ok, request.version()};
try {
auto sessions = Singleton<Older::SessionStore>::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

View File

@ -43,6 +43,7 @@ boost::beast::http::response<ResponseBody> make_200(const boost::beast::http::re
void live2dBackend();
void visitAnalysis();
void userAccount();

106
SessionStore.cpp Normal file
View File

@ -0,0 +1,106 @@
#include "SessionStore.h"
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
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<bool, std::string> 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

39
SessionStore.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef __SESSIONSTORE_H__
#define __SESSIONSTORE_H__
#include <boost/asio/io_context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <shared_mutex>
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<bool, std::string> 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<std::string, SessionData> m_sessions;
std::unordered_map<std::string, std::string> m_refreshMap;
};
} // namespace Older
#endif // __SESSIONSTORE_H__

10
UnitTest/Account.cpp Normal file
View File

@ -0,0 +1,10 @@
#include "Base/DataStructure.h"
#include <boost/test/unit_test.hpp>
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);
}

11
UnitTest/CMakeLists.txt Normal file
View File

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

2
UnitTest/main.cpp Normal file
View File

@ -0,0 +1,2 @@
#define BOOST_TEST_MODULE OlderTest
#include "boost/test/included/unit_test.hpp"