feat(auth): add session-based auth service with tests
这个提交包含在:
145
backend/src/services/auth_service.cc
普通文件
145
backend/src/services/auth_service.cc
普通文件
@@ -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
|
||||
42
backend/src/services/crypto.cc
普通文件
42
backend/src/services/crypto.cc
普通文件
@@ -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
|
||||
在新工单中引用
屏蔽一个用户