feat(auth): add session-based auth service with tests

这个提交包含在:
anygen-build-bot
2026-02-12 09:00:27 +00:00
父节点 76b512939d
当前提交 a6d087d5a9
修改 6 个文件,包含 255 行新增0 行删除

查看文件

@@ -6,11 +6,14 @@ cmake_policy(SET CMP0153 OLD)
find_package(Drogon CONFIG REQUIRED)
find_package(Catch2 3 REQUIRED)
find_package(OpenSSL REQUIRED)
add_library(csp_core
src/version.cc
src/db/sqlite_db.cc
src/app_state.cc
src/services/crypto.cc
src/services/auth_service.cc
)
target_include_directories(csp_core PUBLIC
@@ -19,6 +22,7 @@ target_include_directories(csp_core PUBLIC
target_link_libraries(csp_core PUBLIC
SQLite3_lib
OpenSSL::Crypto
)
add_executable(csp_server
@@ -40,6 +44,7 @@ add_executable(csp_tests
tests/test_main.cc
tests/version_test.cc
tests/sqlite_db_test.cc
tests/auth_service_test.cc
)
target_link_libraries(csp_tests PRIVATE

查看文件

@@ -0,0 +1,30 @@
#pragma once
#include "csp/db/sqlite_db.h"
#include <optional>
#include <string>
namespace csp::services {
struct AuthResult {
int user_id = 0;
std::string token;
int64_t expires_at = 0;
};
class AuthService {
public:
explicit AuthService(db::SqliteDb& db) : db_(db) {}
// Throws on error; for controller we will catch and convert to JSON.
AuthResult Register(const std::string& username, const std::string& password);
AuthResult Login(const std::string& username, const std::string& password);
std::optional<int> VerifyToken(const std::string& token);
private:
db::SqliteDb& db_;
};
} // namespace csp::services

查看文件

@@ -0,0 +1,10 @@
#pragma once
#include <string>
namespace csp::crypto {
std::string RandomHex(size_t bytes);
std::string Sha256Hex(const std::string& data);
} // namespace csp::crypto

查看文件

@@ -0,0 +1,145 @@
#include "csp/services/auth_service.h"
#include "csp/services/crypto.h"
#include <sqlite3.h>
#include <chrono>
#include <stdexcept>
namespace csp::services {
namespace {
int64_t NowSec() {
using namespace std::chrono;
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
}
void CheckSqlite(int rc, sqlite3* db, const char* what) {
if (rc == SQLITE_OK || rc == SQLITE_DONE || rc == SQLITE_ROW) return;
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
}
int64_t StepInt64(sqlite3_stmt* stmt, int col) {
return static_cast<int64_t>(sqlite3_column_int64(stmt, col));
}
std::string StepText(sqlite3_stmt* stmt, int col) {
const unsigned char* t = sqlite3_column_text(stmt, col);
return t ? reinterpret_cast<const char*>(t) : std::string();
}
} // namespace
AuthResult AuthService::Register(const std::string& username,
const std::string& password) {
if (username.empty() || password.size() < 6) {
throw std::runtime_error("invalid username or password");
}
const auto salt = crypto::RandomHex(16);
const auto hash = crypto::Sha256Hex(salt + ":" + password);
const auto now = NowSec();
sqlite3* db = db_.raw();
sqlite3_stmt* stmt = nullptr;
const char* sql =
"INSERT INTO users(username,password_salt,password_hash,created_at) "
"VALUES(?,?,?,?)";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare insert user");
CheckSqlite(sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT),
db, "bind username");
CheckSqlite(sqlite3_bind_text(stmt, 2, salt.c_str(), -1, SQLITE_TRANSIENT), db,
"bind salt");
CheckSqlite(sqlite3_bind_text(stmt, 3, hash.c_str(), -1, SQLITE_TRANSIENT), db,
"bind hash");
CheckSqlite(sqlite3_bind_int64(stmt, 4, now), db, "bind created_at");
const int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
// likely UNIQUE constraint
throw std::runtime_error(std::string("register failed: ") + sqlite3_errmsg(db));
}
const int user_id = static_cast<int>(sqlite3_last_insert_rowid(db));
// Auto-login
return Login(username, password);
}
AuthResult AuthService::Login(const std::string& username,
const std::string& password) {
sqlite3* db = db_.raw();
sqlite3_stmt* stmt = nullptr;
const char* sql =
"SELECT id,password_salt,password_hash FROM users WHERE username=?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare select user");
CheckSqlite(sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT),
db, "bind username");
const int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
throw std::runtime_error("invalid credentials");
}
const int user_id = sqlite3_column_int(stmt, 0);
const auto salt = StepText(stmt, 1);
const auto stored = StepText(stmt, 2);
sqlite3_finalize(stmt);
const auto computed = crypto::Sha256Hex(salt + ":" + password);
if (computed != stored) {
throw std::runtime_error("invalid credentials");
}
const auto token = crypto::RandomHex(32);
const auto now = NowSec();
const auto expires = now + 7 * 24 * 3600;
sqlite3_stmt* ins = nullptr;
const char* ins_sql =
"INSERT INTO sessions(token,user_id,expires_at,created_at) VALUES(?,?,?,?)";
CheckSqlite(sqlite3_prepare_v2(db, ins_sql, -1, &ins, nullptr), db,
"prepare insert session");
CheckSqlite(sqlite3_bind_text(ins, 1, token.c_str(), -1, SQLITE_TRANSIENT), db,
"bind token");
CheckSqlite(sqlite3_bind_int(ins, 2, user_id), db, "bind user_id");
CheckSqlite(sqlite3_bind_int64(ins, 3, expires), db, "bind expires");
CheckSqlite(sqlite3_bind_int64(ins, 4, now), db, "bind created_at");
CheckSqlite(sqlite3_step(ins), db, "insert session");
sqlite3_finalize(ins);
return AuthResult{.user_id = user_id, .token = token, .expires_at = expires};
}
std::optional<int> AuthService::VerifyToken(const std::string& token) {
if (token.empty()) return std::nullopt;
sqlite3* db = db_.raw();
sqlite3_stmt* stmt = nullptr;
const char* sql =
"SELECT user_id,expires_at FROM sessions WHERE token=?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare select session");
CheckSqlite(sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_TRANSIENT),
db, "bind token");
const int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return std::nullopt;
}
const int user_id = sqlite3_column_int(stmt, 0);
const int64_t expires = StepInt64(stmt, 1);
sqlite3_finalize(stmt);
if (expires < NowSec()) return std::nullopt;
return user_id;
}
} // namespace csp::services

查看文件

@@ -0,0 +1,42 @@
#include "csp/services/crypto.h"
#include <openssl/sha.h>
#include <array>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <vector>
namespace csp::crypto {
namespace {
std::string ToHex(const unsigned char* data, size_t len) {
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (size_t i = 0; i < len; ++i) {
oss << std::setw(2) << static_cast<int>(data[i]);
}
return oss.str();
}
} // namespace
std::string RandomHex(size_t bytes) {
std::vector<unsigned char> buf(bytes);
std::ifstream urandom("/dev/urandom", std::ios::in | std::ios::binary);
if (!urandom) throw std::runtime_error("cannot open /dev/urandom");
urandom.read(reinterpret_cast<char*>(buf.data()), static_cast<std::streamsize>(bytes));
if (!urandom) throw std::runtime_error("failed to read /dev/urandom");
return ToHex(buf.data(), buf.size());
}
std::string Sha256Hex(const std::string& data) {
std::array<unsigned char, SHA256_DIGEST_LENGTH> out{};
SHA256(reinterpret_cast<const unsigned char*>(data.data()), data.size(), out.data());
return ToHex(out.data(), out.size());
}
} // namespace csp::crypto

查看文件

@@ -0,0 +1,23 @@
#include <catch2/catch_test_macros.hpp>
#include "csp/db/sqlite_db.h"
#include "csp/services/auth_service.h"
TEST_CASE("auth register/login/verify") {
auto db = csp::db::SqliteDb::OpenMemory();
csp::db::ApplyMigrations(db);
csp::services::AuthService auth(db);
const auto r = auth.Register("alice", "password123");
REQUIRE(r.user_id > 0);
REQUIRE(r.token.size() > 10);
const auto uid = auth.VerifyToken(r.token);
REQUIRE(uid.has_value());
REQUIRE(uid.value() == r.user_id);
const auto r2 = auth.Login("alice", "password123");
REQUIRE(r2.user_id == r.user_id);
REQUIRE(r2.token != r.token);
}