实现登录接口。
This commit is contained in:
parent
63aa6a6270
commit
d1b8bf6342
@ -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;
|
||||
|
@ -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
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__
|
||||
#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__
|
@ -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
|
||||
|
131
Database.cpp
131
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<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
|
@ -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:
|
||||
|
188
ServiceLogic.cpp
188
ServiceLogic.cpp
@ -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
|
||||
|
@ -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
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