实现登录接口。
This commit is contained in:
parent
63aa6a6270
commit
d1b8bf6342
@ -7,6 +7,7 @@
|
|||||||
#include "HttpSession.h"
|
#include "HttpSession.h"
|
||||||
#include "Router/router.hpp"
|
#include "Router/router.hpp"
|
||||||
#include "ServiceLogic.h"
|
#include "ServiceLogic.h"
|
||||||
|
#include "SessionStore.h"
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
#include "WeChat/Corporation/Context.h"
|
#include "WeChat/Corporation/Context.h"
|
||||||
#include <boost/asio/strand.hpp>
|
#include <boost/asio/strand.hpp>
|
||||||
@ -31,6 +32,7 @@ Application::Application() : m_d{new ApplicationPrivate()} {
|
|||||||
|
|
||||||
m_database = Singleton<Database>::construct();
|
m_database = Singleton<Database>::construct();
|
||||||
m_database->open(m_settings->sqlitePath());
|
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 = Singleton<WeChat::Corporation::Context>::construct(*m_ioContext->ioContext());
|
||||||
m_corporationContext->start();
|
m_corporationContext->start();
|
||||||
@ -84,6 +86,7 @@ int Application::exec() {
|
|||||||
auto settings = Singleton<Settings>::instance();
|
auto settings = Singleton<Settings>::instance();
|
||||||
ServiceLogic::live2dBackend();
|
ServiceLogic::live2dBackend();
|
||||||
ServiceLogic::visitAnalysis();
|
ServiceLogic::visitAnalysis();
|
||||||
|
ServiceLogic::userAccount();
|
||||||
startAcceptHttpConnections(settings->server(), settings->port());
|
startAcceptHttpConnections(settings->server(), settings->port());
|
||||||
m_ioContext->run();
|
m_ioContext->run();
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -23,6 +23,7 @@ class Settings;
|
|||||||
class ApplicationPrivate;
|
class ApplicationPrivate;
|
||||||
class HttpSession;
|
class HttpSession;
|
||||||
class Database;
|
class Database;
|
||||||
|
class SessionStore;
|
||||||
|
|
||||||
class Application : public std::enable_shared_from_this<Application> {
|
class Application : public std::enable_shared_from_this<Application> {
|
||||||
public:
|
public:
|
||||||
@ -43,6 +44,7 @@ private:
|
|||||||
std::shared_ptr<Settings> m_settings;
|
std::shared_ptr<Settings> m_settings;
|
||||||
std::shared_ptr<Core::IoContext> m_ioContext;
|
std::shared_ptr<Core::IoContext> m_ioContext;
|
||||||
std::shared_ptr<Core::MessageManager> m_messageManager;
|
std::shared_ptr<Core::MessageManager> m_messageManager;
|
||||||
|
std::shared_ptr<SessionStore> m_sessionStore;
|
||||||
std::shared_ptr<Database> m_database;
|
std::shared_ptr<Database> m_database;
|
||||||
std::shared_ptr<WeChat::Corporation::Context> m_corporationContext;
|
std::shared_ptr<WeChat::Corporation::Context> m_corporationContext;
|
||||||
};
|
};
|
||||||
|
12
Base/CMakeLists.txt
Normal file
12
Base/CMakeLists.txt
Normal 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
71
Base/DataStructure.cpp
Normal 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
|
@ -1,17 +1,34 @@
|
|||||||
#ifndef __DATASTRUCTURE_H__
|
#ifndef __DATASTRUCTURE_H__
|
||||||
#define __DATASTRUCTURE_H__
|
#define __DATASTRUCTURE_H__
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
struct VisitorStats{
|
namespace Older {
|
||||||
|
struct VisitorStats {
|
||||||
std::string url;
|
std::string url;
|
||||||
int visitorCount=0;
|
int visitorCount = 0;
|
||||||
int totalViews =0;
|
int totalViews = 0;
|
||||||
int64_t lastViewTime =0;
|
int64_t lastViewTime = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SiteStats{
|
struct SiteStats {
|
||||||
int totalViews =0;
|
int totalViews = 0;
|
||||||
int totalVisitors =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__
|
#endif // __DATASTRUCTURE_H__
|
@ -1,7 +1,10 @@
|
|||||||
find_package(Boost REQUIRED COMPONENTS json)
|
find_package(Boost REQUIRED COMPONENTS json)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
add_subdirectory(/root/Projects/Kylin Kylin)
|
add_subdirectory(/root/Projects/Kylin Kylin)
|
||||||
add_subdirectory(3rdparty)
|
add_subdirectory(3rdparty)
|
||||||
|
add_subdirectory(Base)
|
||||||
|
add_subdirectory(UnitTest)
|
||||||
|
|
||||||
add_executable(Older main.cpp
|
add_executable(Older main.cpp
|
||||||
WeChat/Corporation/Context.h WeChat/Corporation/Context.cpp
|
WeChat/Corporation/Context.h WeChat/Corporation/Context.cpp
|
||||||
@ -11,6 +14,7 @@ add_executable(Older main.cpp
|
|||||||
HttpSession.h HttpSession.cpp
|
HttpSession.h HttpSession.cpp
|
||||||
ResponseUtility.h ResponseUtility.cpp
|
ResponseUtility.h ResponseUtility.cpp
|
||||||
ServiceLogic.h ServiceLogic.inl ServiceLogic.cpp
|
ServiceLogic.h ServiceLogic.inl ServiceLogic.cpp
|
||||||
|
SessionStore.h SessionStore.cpp
|
||||||
Settings.h Settings.cpp
|
Settings.h Settings.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,6 +23,7 @@ target_include_directories(Older
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(Older
|
target_link_libraries(Older
|
||||||
|
PRIVATE Base
|
||||||
PRIVATE Kylin::Core
|
PRIVATE Kylin::Core
|
||||||
PRIVATE Kylin::Http
|
PRIVATE Kylin::Http
|
||||||
PRIVATE Kylin::Router
|
PRIVATE Kylin::Router
|
||||||
|
131
Database.cpp
131
Database.cpp
@ -114,8 +114,118 @@ SiteStats Database::siteStats() {
|
|||||||
return ret;
|
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() {
|
void Database::initialize() {
|
||||||
createVisitAnalysisTable();
|
createVisitAnalysisTable();
|
||||||
|
createUsersTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Database::createVisitAnalysisTable() {
|
void Database::createVisitAnalysisTable() {
|
||||||
@ -138,4 +248,23 @@ void Database::createVisitAnalysisTable() {
|
|||||||
sqlite3_free(message);
|
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
|
@ -18,9 +18,13 @@ public:
|
|||||||
std::list<VisitorStats> mostViewedUrls(int n);
|
std::list<VisitorStats> mostViewedUrls(int n);
|
||||||
std::list<VisitorStats> latestViewedUrls(int n);
|
std::list<VisitorStats> latestViewedUrls(int n);
|
||||||
SiteStats siteStats();
|
SiteStats siteStats();
|
||||||
|
void createUser(const Account &account);
|
||||||
|
Account user(const std::string &identifier)const;
|
||||||
|
Account user(int64_t id)const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void createVisitAnalysisTable();
|
void createVisitAnalysisTable();
|
||||||
|
void createUsersTable();
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
188
ServiceLogic.cpp
188
ServiceLogic.cpp
@ -2,12 +2,32 @@
|
|||||||
#include "Core/Singleton.h"
|
#include "Core/Singleton.h"
|
||||||
#include "Database.h"
|
#include "Database.h"
|
||||||
#include "HttpSession.h"
|
#include "HttpSession.h"
|
||||||
|
#include "SessionStore.h"
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
namespace ServiceLogic {
|
namespace ServiceLogic {
|
||||||
using namespace boost::beast;
|
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>
|
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) {
|
serverError(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view errorMessage) {
|
||||||
using namespace boost::beast;
|
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
|
} // namespace ServiceLogic
|
||||||
|
@ -43,6 +43,7 @@ boost::beast::http::response<ResponseBody> make_200(const boost::beast::http::re
|
|||||||
|
|
||||||
void live2dBackend();
|
void live2dBackend();
|
||||||
void visitAnalysis();
|
void visitAnalysis();
|
||||||
|
void userAccount();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
106
SessionStore.cpp
Normal file
106
SessionStore.cpp
Normal 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
39
SessionStore.h
Normal 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
10
UnitTest/Account.cpp
Normal 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
11
UnitTest/CMakeLists.txt
Normal 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
2
UnitTest/main.cpp
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#define BOOST_TEST_MODULE OlderTest
|
||||||
|
#include "boost/test/included/unit_test.hpp"
|
Loading…
x
Reference in New Issue
Block a user