前面我们讲了工具层、数据库层和模型层,现在终于进入真正的业务逻辑层!AuthService 是整个系统第一个也是最重要的 Service------它负责用户注册、登录、鉴权和限流。这 414 行代码是整个 OJ 系统的"大门",没有它,其他功能都无从谈起。
1:今天的模块
project-cpp-oj-vibecoding/
├── src/
│ ├── main.cc
│ ├── server/
│ ├── handler/
│ ├── service/ ← ★ 今天的主角:Service 层 ★
│ │ ├── auth_service.hpp ← 认证服务(头文件,53 行)
│ │ ├── auth_service.cc ← 认证服务(实现,414 行)
│ │ ├── problem_service.cc ← 题目服务(之后讲)
│ │ └── executor_service.cc ← 判题引擎(最复杂,之后讲)
│ ├── model/
│ ├── db/
│ └── utils/
AuthService辅助什么?
AuthService 是 OJ 系统的身份认证系统,它管 5 件事:
| 功能 | 方法 | 一句话说明 |
|---|---|---|
| 注册 | register_user() |
创建一个新用户,密码加密后存数据库 |
| 登录 | login() |
验证用户名密码,创建 Session 文件,返回 session_id |
| 退出 | logout() |
删除 Session 文件 |
| 鉴权 | authenticate() |
根据 Session ID 读取 Session 文件,返回用户信息 |
| 限流 | check_rate_limit() |
同用户 10 秒内只能提交 1 次 |
核心流程:
注册流程:
用户填用户名+密码 → register_user() → 检查用户名是否已存在
→ 密码 bcrypt 加密 → INSERT INTO users → 返回 user_id
登录流程:
用户填用户名+密码 → login() → 查数据库 → 验证密码
→ 生成随机 session_id → 写 Session 文件到磁盘 → 返回 session_id
→ Handler 把 session_id 写入 Cookie 返回给浏览器
鉴权流程:
浏览器带着 Cookie 访问 → 后端拿到 session_id
→ authenticate() → 读 Session 文件 → 检查是否过期 → 返回用户信息
限流流程:
用户提交代码 → check_rate_limit(user_id)
→ 查上次提交时间 → 如果不到 10 秒 → 拒绝
→ 如果超过 10 秒 → 更新提交时间 → 允许
2:auth_service.hpp
cpp
// ============================================================
// 文件名: auth_service.hpp
// 作用: 定义 AuthService 认证服务
// 整个系统的"大门"------所有需要登录的功能都要通过它
// ============================================================
#pragma once
#include <string> // std::string
#include <unordered_map> // std::unordered_map --- 哈希表
// 用来存"用户 ID → 最后提交时间"的映射
// 用于限流判断
#include <chrono> // 时间库 --- 处理时间点、时间间隔
// 用于限流计时和 Session 过期判断
#include <mutex> // std::mutex --- 互斥锁
#include <condition_variable> // std::condition_variable --- 条件变量
#include <thread> // std::thread --- 线程
// 用于后台 Session 清理线程
// ============================================================
// SessionInfo 结构体
// 从 Session 文件中解析出来的用户信息
// 每次请求鉴权时返回这个结构体
// ============================================================
struct SessionInfo {
int id = 0; // 用户 ID(数据库中的主键)
std::string username; // 用户名
std::string role; // 角色:"user" 或 "admin"
};
// ═══════════════════════════════════════════════════════════
// AuthService 类 --- 同样使用单例模式
// 是整个项目中"内容最丰富"的 Service 之一
// ═══════════════════════════════════════════════════════════
class AuthService {
public:
// 单例模式标准接口
static AuthService& instance();
// init() --- 初始化认证服务
// 1. 从 Config 读取 Session 目录路径
// 2. 创建 Session 目录(如果不存在)
// 3. 启动后台线程:定期清理过期的 Session 文件
bool init();
// shutdown() --- 关闭认证服务
// 1. 通知清理线程停止
// 2. 等待线程结束
void shutdown();
// ──── 5 个核心业务方法 ────
// 注册新用户
// 参数: username(用户名), password(密码), error_msg(输出错误信息)
// 返回: >0 用户ID, -1 失败
int register_user(const std::string& username,
const std::string& password,
std::string& error_msg);
// 用户登录
// 参数: username(用户名), password(密码), error_msg(输出错误信息)
// 返回: session_id 字符串, 空字符串表示失败
std::string login(const std::string& username,
const std::string& password,
std::string& error_msg);
// 用户登出(删除 Session 文件)
bool logout(const std::string& session_id);
// 鉴权:根据 session_id 获取用户信息
SessionInfo authenticate(const std::string& session_id);
// 限流检查:同用户 10 秒内只能提交 1 次
bool check_rate_limit(int user_id);
private:
// 单例模式三件套
AuthService() = default;
~AuthService();
AuthService(const AuthService&) = delete;
AuthService& operator=(const AuthService&) = delete;
// ──── 私有辅助方法 ────
// 生成随机的 Session ID(64 位十六进制字符串)
std::string generate_session_id();
// 生成 bcrypt 加密用的 salt(盐值)
std::string make_salt();
// 对密码进行哈希加密
std::string hash_password(const std::string& password);
// 验证密码是否和存储的哈希匹配
bool verify_password(const std::string& password,
const std::string& stored_hash);
// 根据 session_id 构造 Session 文件的完整路径
std::string session_path(const std::string& session_id);
// 执行一次清理:扫描 Session 目录,删除过期文件
void cleanup_expired();
// 后台清理线程的主循环
void cleanup_loop();
// ──── 成员变量 ────
std::thread cleanup_thread_; // 后台清理线程
bool stop_cleanup_ = false; // 通知线程停止的标记
std::mutex cleanup_mutex_; // 保护 stop_cleanup_ 的锁
std::condition_variable cleanup_cv_; // 让清理线程"定时醒来"
std::mutex rate_limit_mutex_; // 保护限流数据的锁
// 限流表:user_id → 最后一次提交的时间点
std::unordered_map<int, std::chrono::steady_clock::time_point> last_submit_;
std::string session_dir_; // Session 文件存放目录
int submit_window_sec_ = 10; // 提交窗口(秒)
int cleanup_interval_min_ = 30; // 清理间隔(分钟)
};
为什么用std::unordered_map做限流
cpp
// 限流表的结构
last_submit_(unordered_map)
┌──────────┬──────────────────────────────┐
│ key │ value │
│ (user_id)│ (最后提交时间) │
├──────────┼──────────────────────────────┤
│ 1 │ 2026-06-25 14:00:00 │
│ 2 │ 2026-06-25 14:00:05 │
│ 3 │ 2026-06-25 14:01:00 │
│ ... │ ... │
└──────────┴──────────────────────────────┘
// 查 user_id=2 的最后提交时间
auto it = last_submit_.find(2);
// 哈希表直接定位到 key=2 的记录,不需要遍历!
// 即使表里有 10000 个用户,查找速度也一样快
3:init()服务初始化
cpp
// ============================================================
// 初始化认证服务
//
// 做了 3 件事:
// 1. 从 Config 读取配置(Session 目录、限流窗口、清理间隔)
// 2. 确保 Session 目录存在(逐级创建目录)
// 3. 启动后台线程,定期清理过期 Session
// ============================================================
bool AuthService::init() {
// 第 1 步:从 Config 读取配置参数
auto& cfg = Config::instance();
session_dir_ = cfg.session().dir; // 如 "/var/oj/sessions"
submit_window_sec_ = cfg.rate_limit().submit_window_sec; // 如 10
cleanup_interval_min_ = cfg.session().cleanup_interval_min; // 如 30
// 第 2 步:创建 Session 目录(逐级创建)
// 为什么逐级?
// 如果 session_dir_ = "/var/oj/sessions"
// 需要先创建 /var → 再创建 /var/oj → 再创建 /var/oj/sessions
// mkdir() 不支持一次创建多级目录!
{
bool ok = true;
size_t pos = 0;
// 逐个查找 '/' 字符,逐级创建目录
while ((pos = session_dir_.find('/', pos + 1)) != std::string::npos) {
std::string sub = session_dir_.substr(0, pos);
// mkdir() 创建目录,权限 0755(所有者可读写执行,其他人可读执行)
// errno == EEXIST 表示目录已存在,不算错误
if (mkdir(sub.c_str(), 0755) != 0 && errno != EEXIST) {
ok = false;
break;
}
}
// 创建最后一级目录
if (ok) {
ok = (mkdir(session_dir_.c_str(), 0755) == 0 || errno == EEXIST);
}
// 如果创建失败(比如 /var 没有写权限),回退到 /tmp/oj_sessions
if (!ok) {
session_dir_ = "/tmp/oj_sessions";
mkdir(session_dir_.c_str(), 0755);
Logger::instance().warn("Session dir fallback to " + session_dir_);
}
}
// 第 3 步:启动后台清理线程
stop_cleanup_ = false;
cleanup_thread_ = std::thread(&AuthService::cleanup_loop, this);
// 这行代码创建了一个新线程,运行 cleanup_loop() 函数
// this 表示"在这个对象上调用 cleanup_loop"
Logger::instance().info("AuthService initialized, session dir: " + session_dir_);
return true;
}
目录创建流程图
cpp
session_dir_ = "/var/oj/sessions"
逐级创建目录:
│
├── pos = 4 → "/var"
│ mkdir("/var", 0755) → 已存在(EEXIST),没事
│
├── pos = 7 → "/var/oj"
│ mkdir("/var/oj", 0755) → 不存在,创建
│
└── pos = npos(找不到 '/' 了)
mkdir("/var/oj/sessions", 0755) → 不存在,创建
如果 /var 没权限:
session_dir_ = "/tmp/oj_sessions"
mkdir("/tmp/oj_sessions", 0755) → 一定能创建成功
shutdown()------优雅关闭
cpp
// ============================================================
// 关闭认证服务
// 优雅地停止后台清理线程
// "优雅" = 通知线程 → 等待线程结束,而不是强行杀掉
// ============================================================
void AuthService::shutdown() {
{
std::lock_guard<std::mutex> lock(cleanup_mutex_);
stop_cleanup_ = true; // 设置停止标记
}
cleanup_cv_.notify_one(); // 叫醒等待的线程,让它检查标记
if (cleanup_thread_.joinable()) { // 线程还在运行吗?
cleanup_thread_.join(); // 等待线程结束
}
}
4:密码安全
在讲注册和登录之前,先理解密码是怎么安全存储的。这可能是整个项目里最专业的安全设计。
1:秘密哈希VS明文
sql
// 错误做法:明文存密码
// 如果数据库被黑客攻击,所有用户的密码都暴露了
INSERT INTO users (username, password) VALUES ('alice', '123456');
// 正确做法:存哈希值
// "123456" 经过 bcrypt 加密后变成乱码
// 这个乱码无法还原回 "123456"
INSERT INTO users (username, password) VALUES ('alice', '$2b$12$rRLy3uV8Hoq1aKm...');
2:什么是Salt(盐值)
sql
// 不加盐的哈希:
// "123456" → SHA256 → "8d969eef6ecad3c29a3a629280e686cf..."
// 问题是:所有把密码设成 "123456" 的人,哈希值都一样!
// 黑客可以预先算好常见密码的哈希值(彩虹表),直接比对
// 加盐的哈希:
// Salt = 随机生成的 16 字节数据,比如 "a1b2c3..."
// 要哈希的是:Salt + 密码 = "a1b2c3...123456"
// 即使两个人的密码都是 "123456",因为 Salt 不同,哈希值也不同!
3:AuthService的秘密系统
AuthService 实现了 两层密码保护:
sql
第 1 层:bcrypt(首选)
目前最流行的密码哈希算法
特点:故意设计得很慢(慢到暴力破解需要几万年)
第 2 层:SHA-256 + Salt(备选)
如果系统不支持 bcrypt,用这个做降级
也是加盐的,安全性仍然不错
cpp
// ============================================================
// 生成 bcrypt salt
// Salt = 16 字节随机数,编码成 bcrypt 格式的字符串
// 最终格式:"$2b$12$" + 22 字符的 base64 编码
// $2b$ --- bcrypt 算法版本
// 12 --- 成本因子(2^12 轮迭代,大约 0.1 秒)
// 后面的 22 字符 --- 随机盐值
// ============================================================
std::string AuthService::make_salt() {
// 64 字符的 base64 编码表(bcrypt 专用版本)
static const char b64[] =
"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// 从 /dev/urandom(Linux 的随机数生成器)读取 16 字节随机数
unsigned char random[16];
FILE* f = fopen("/dev/urandom", "rb");
if (!f) return "";
fread(random, 1, 16, f);
fclose(f);
// 拼装成 "$2b$12$" + base64 编码的随机数
char salt[30];
std::memcpy(salt, "$2b$12$", 7);
// 这里有一段 24 行的 base64 编码算法
// 把 16 字节随机数编码成 22 字符的字符串
//(编码细节略,理解原理即可)
salt[j] = '\0';
return std::string(salt);
}
// ============================================================
// 哈希密码(主方法)
// 优先使用 bcrypt,如果不可用则降级到 SHA-256
// ============================================================
std::string AuthService::hash_password(const std::string& password) {
std::string salt = make_salt(); // 生成随机盐值
if (salt.empty()) {
Logger::instance().error("Failed to generate bcrypt salt");
return sha256_hash(password); // 降级到 SHA-256
}
// 用 crypt_r() 函数执行 bcrypt 哈希
// crypt_r 是线程安全的版本(_r = reentrant,可重入)
struct crypt_data data;
data.initialized = 0;
char* result = crypt_r(password.c_str(), salt.c_str(), &data);
if (result && result[0] == '$' && result[1] == '2') {
return std::string(result); // bcrypt 成功
}
// bcrypt 不可用,降级到 SHA-256
Logger::instance().warn("bcrypt not available, falling back to SHA-256");
return sha256_hash(password);
}
// ============================================================
// 验证密码
// 检查用户输入的密码是否和数据库里存储的哈希匹配
// ============================================================
bool AuthService::verify_password(const std::string& password,
const std::string& stored_hash) {
// 如果是 SHA-256 格式的哈希,用对应的验证方法
if (stored_hash.substr(0, 7) == "sha256$") {
return verify_sha256(password, stored_hash);
}
// 默认用 bcrypt 验证
struct crypt_data data;
data.initialized = 0;
char* result = crypt_r(password.c_str(), stored_hash.c_str(), &data);
if (!result) return false;
return stored_hash == result; // 哈希值一致 = 密码正确
}
理解crypt_r原理
cpp
`crypt_r(password, stored_hash, &data)`
- 第一个参数:你输入的密码
- 第二个参数:数据库里存的哈希(包含了盐值和算法信息)
- crypt_r 从 `stored_hash` 中提取出盐值,用它对 `password` 重新哈希
- 如果结果和 `stored_hash` 一样 → 密码正确!
5:register_user()------用户注册
cpp
// ============================================================
// 注册新用户
//
// 参数:
// username --- 用户名
// password --- 密码(明文)
// error_msg --- 输出参数,如果失败会在这里写错误信息
//
// 返回值:
// > 0 --- 新用户的 ID
// -1 --- 注册失败(看 error_msg 知道原因)
//
// 做了 4 件事:
// 1. 校验:用户名和密码不能为空
// 2. SQL 注入防护:对特殊字符做转义
// 3. 密码哈希:绝对不存明文
// 4. 插入数据库:处理唯一键冲突
// ============================================================
int AuthService::register_user(const std::string& username,
const std::string& password,
std::string& error_msg) {
// ── 第 1 步:基本校验 ──
if (username.empty() || password.empty()) {
error_msg = "Username and password are required";
return -1;
}
// ── 第 2 步:从连接池拿数据库连接 ──
auto* conn = ConnectionPool::instance().get();
if (!conn) {
error_msg = "Database connection failed";
return -1;
}
// ── 第 3 步:SQL 注入防护 ──
// 如果用户名里有单引号 "'",直接拼接到 SQL 里会破坏语法
// 比如用户名是 "admin'--",SQL 会变成:
// SELECT * FROM users WHERE username = 'admin'--'
// 后面的内容被注释掉了!黑客可以绕过认证!
//
// mysql_real_escape_string() 会把特殊字符转义
// 比如 "admin'--" → "admin\'--"(加反斜杠,失去特殊含义)
std::vector<char> esc_buf(username.size() * 2 + 1);
unsigned long esc_len = mysql_real_escape_string(conn, esc_buf.data(),
username.c_str(),
username.size());
std::string esc_username(esc_buf.data(), esc_len);
// ── 第 4 步:哈希密码 ──
std::string password_hash = hash_password(password);
if (password_hash.empty()) {
error_msg = "Failed to hash password";
ConnectionPool::instance().release(conn);
return -1;
}
// 密码哈希里可能也有特殊字符,也要转义
std::vector<char> hash_buf(password_hash.size() * 2 + 1);
unsigned long hash_len = mysql_real_escape_string(conn, hash_buf.data(),
password_hash.c_str(),
password_hash.size());
std::string esc_hash(hash_buf.data(), hash_len);
// ── 第 5 步:执行 INSERT ──
std::string query = "INSERT INTO users (username, password, role) VALUES ('"
+ esc_username + "', '" + esc_hash + "', 'user')";
if (mysql_query(conn, query.c_str()) != 0) {
// MySQL 错误码 1062 = 唯一键冲突(用户名已存在)
if (mysql_errno(conn) == 1062) {
error_msg = "Username already exists";
} else {
error_msg = "Database error";
Logger::instance().error(std::string(mysql_error(conn)));
}
ConnectionPool::instance().release(conn);
return -1;
}
// ── 第 6 步:获取自动生成的用户 ID ──
int user_id = mysql_insert_id(conn);
ConnectionPool::instance().release(conn);
Logger::instance().info("User registered: " + username +
" (id=" + std::to_string(user_id) + ")");
return user_id;
}
流程图
cpp
register_user("alice", "123456")
│
├── username 和 password 不为空? →
│
├── 从连接池拿连接 →
│
├── 转义用户名 → "alice"(没有特殊字符,不变)
│
├── 哈希密码 → "$2b$12$rRLy3uV8Hoq1aKm..."
│
├── 转义哈希 → 不变
│
├── 执行 SQL:
│ INSERT INTO users (username, password, role)
│ VALUES ('alice', '$2b$12$...', 'user')
│ │
│ ├── 成功 → mysql_insert_id() = 5
│ │ → 返回 5
│ │
│ └── 失败(错误码 1062)→ "Username already exists"
│ → 返回 -1
│
└── 归还连接到池子
6:login()------用户登陆
cpp
// ============================================================
// 用户登录
//
// 做了 5 件事:
// 1. SQL 注入防护:转义用户名
// 2. 查数据库:找这个用户
// 3. 验证密码:用 bcrypt 比对
// 4. 生成 Session ID:64 位随机十六进制字符串
// 5. 写 Session 文件:存到磁盘上
//
// 返回值:
// session_id --- 成功,登录凭证
// 空字符串 --- 失败
// ============================================================
std::string AuthService::login(const std::string& username,
const std::string& password,
std::string& error_msg) {
// ── 第 1 步:拿连接 ──
auto* conn = ConnectionPool::instance().get();
if (!conn) {
error_msg = "Database connection failed";
return "";
}
// ── 第 2 步:转义用户名 ──
std::vector<char> esc_buf(username.size() * 2 + 1);
unsigned long esc_len = mysql_real_escape_string(conn, esc_buf.data(),
username.c_str(),
username.size());
std::string esc_username(esc_buf.data(), esc_len);
// ── 第 3 步:查用户 ──
std::string query = "SELECT id, password, role FROM users "
"WHERE username = '" + esc_username + "'";
if (mysql_query(conn, query.c_str()) != 0) {
error_msg = "Database query failed";
ConnectionPool::instance().release(conn);
return "";
}
MYSQL_RES* result = mysql_store_result(conn);
if (!result || mysql_num_rows(result) == 0) {
// 用户名不存在 ------ 但为了安全,不告诉用户"是用户名错了还是密码错了"
// 统一说"用户名或密码错误",防止黑客试探用户名是否存在
if (result) mysql_free_result(result);
error_msg = "Invalid username or password";
ConnectionPool::instance().release(conn);
return "";
}
// ── 第 4 步:获取用户数据 ──
MYSQL_ROW row = mysql_fetch_row(result);
int user_id = std::stoi(row[0]);
std::string password_hash = row[1] ? row[1] : "";
std::string role = row[2] ? row[2] : "user";
mysql_free_result(result);
ConnectionPool::instance().release(conn);
// ── 第 5 步:验证密码 ──
if (password_hash.empty() || !verify_password(password, password_hash)) {
error_msg = "Invalid username or password"; // 同一句话
return "";
}
// ── 第 6 步:生成随机的 Session ID ──
// Session ID 是 64 位十六进制字符串
// 比如 "a1b2c3d4e5f6...(64 位)"
// 从 /dev/urandom 读取 32 字节随机数,转成十六进制
std::string session_id = generate_session_id();
if (session_id.empty()) {
error_msg = "Failed to generate session ID";
return "";
}
// ── 第 7 步:构造 Session 数据 ──
// Session 有效期 24 小时
auto now = std::chrono::system_clock::now();
auto expires = now + std::chrono::hours(24);
auto expires_ts = std::chrono::system_clock::to_time_t(expires);
// 用 JSON 格式存 Session 数据
nlohmann::json j;
j["user_id"] = user_id;
j["username"] = username;
j["role"] = role;
j["expires_at"] = expires_ts; // 过期时间戳
// ── 第 8 步:写入 Session 文件 ──
// 文件路径:/var/oj/sessions/{session_id}.json
// 文件内容:{"user_id":5,"username":"alice","role":"user","expires_at":...}
std::string filepath = session_path(session_id);
std::ofstream file(filepath);
if (!file.is_open()) {
error_msg = "Failed to create session file";
Logger::instance().error("Cannot write session file: " + filepath);
return "";
}
file << j.dump();
file.close();
return session_id; // 返回给 Handler,Handler 会设置 Cookie
}
登陆流程图
cpp
login("alice", "123456")
│
├── 拿连接 →
├── 转义用户名 → "alice"
│
├── 执行 SQL:
│ SELECT id, password, role FROM users WHERE username = 'alice'
│ ↓
│ 找到用户 → id=5, password="...hash...", role="user"
│
├── verify_password("123456", "...hash...")
│ │
│ ├── crypt_r("123456", "...hash...") 重新哈希
│ ├── 结果 == "...hash..." → 密码正确
│ └── 结果 ≠ "...hash..." → 密码错误
│
├── 密码正确!→ 生成 Session ID
│ │
│ ├── fopen("/dev/urandom") 读取 32 字节随机数
│ └── 转成 64 位十六进制 → "a1b2c3d4..."
│
├── 构造 Session JSON:
│ {"user_id":5,"username":"alice","role":"user","expires_at":1719300000}
│
├── 写入文件:/var/oj/sessions/a1b2c3d4....json
│
└── 返回 "a1b2c3d4..."(session_id)
│
▼
Handler 收到 session_id
│
├── 设置 Cookie: "session_id=a1b2c3d4..."
├── 设置 Cookie 过期时间:24 小时
└── 返回给浏览器:"登录成功!"
7:authenticate()------鉴权
cpp
// ============================================================
// 鉴权:根据 Session ID 获取用户信息
//
// 每次用户访问需要登录的页面时都会调用这个方法
// 比如:查看题目列表、提交代码等
//
// 做了 3 件事:
// 1. 读 Session 文件
// 2. 检查是否过期
// 3. 返回用户信息
//
// 如果 Session 无效或过期,返回一个 id=0 的空 SessionInfo
// ============================================================
SessionInfo AuthService::authenticate(const std::string& session_id) {
SessionInfo info; // 默认 id=0,表示"未登录"
if (session_id.empty()) return info;
// 构造文件路径:/var/oj/sessions/{session_id}.json
std::string filepath = session_path(session_id);
// 打开 Session 文件
std::ifstream file(filepath);
if (!file.is_open()) return info; // 文件不存在 = 未登录
// 解析 JSON
nlohmann::json j;
try {
file >> j;
} catch (...) {
return info; // JSON 解析失败
}
// 检查是否过期
auto expires_at = j.value("expires_at", 0LL);
auto now = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now());
if (expires_at < now) {
// 已过期:删除 Session 文件
unlink(filepath.c_str());
return info; // 返回空 SessionInfo
}
// 返回用户信息
info.user_id = j.value("user_id", 0);
info.username = j.value("username", "");
info.role = j.value("role", "user");
return info;
}
鉴权在Handler中如何使用
cpp
// 在 auth_handler.cc 中
void handle_me(const Request& req, Response& res) {
// 1. 从 Cookie 中获取 session_id
std::string session_id = get_session_id(req);
// 2. 调用鉴权
SessionInfo info = AuthService::instance().authenticate(session_id);
// 3. 如果未登录,返回 401
if (info.user_id == 0) {
res.status = 401;
res.set_content("{\"error\":\"Unauthorized\"}", "application/json");
return;
}
// 4. 已登录,返回用户信息
json j;
j["user_id"] = info.user_id;
j["username"] = info.username;
j["role"] = info.role;
res.set_content(j.dump(), "application/json");
}
Cookie鉴权流程图
cpp
浏览器请求 /api/me
│
├── 请求头中携带 Cookie: "session_id=a1b2c3d4..."
│
▼
Handler → get_session_id(req)
│
▼
authenticate("a1b2c3d4...")
│
├── 打开文件 /var/oj/sessions/a1b2c3d4....json
│ │
│ ├── 文件存在 → 继续
│ └── 文件不存在 → 返回空 SessionInfo → 401
│
├── 解析 JSON → {"user_id":5, "username":"alice", "role":"user", "expires_at":...}
│
├── 检查过期时间
│ │
│ ├── 未过期 → 返回用户信息 → 200 OK
│ └── 已过期 → 删除文件 → 返回空 SessionInfo → 401
│
└── 完成
8:logout()------退出
cpp
// ============================================================
// 登出:删除 Session 文件
// unlink() 是 Linux 系统调用,删除文件
// ============================================================
bool AuthService::logout(const std::string& session_id) {
if (session_id.empty()) return false;
std::string filepath = session_path(session_id);
if (unlink(filepath.c_str()) == 0) { // 删除成功返回 0
Logger::instance().info("Session deleted: " + session_id);
return true;
}
return false; // 文件不存在或删除失败
}
9:check_rate_limit()------限流
cpp
// ============================================================
// 限流检查:同用户 10 秒内只能提交 1 次
//
// 为什么需要限流?
// 防止用户恶意频繁提交(比如 1 秒提交 100 次)
// 判题是很消耗资源的(要编译、运行、比对)
// 如果不限流,服务器会被压垮
//
// 实现原理:
// 用 unordered_map 记录每个用户最后提交的时间
// 如果距离上次提交不到 submit_window_sec_ 秒,拒绝
//
// 返回值:
// true --- 可以提交(没有超过限制)
// false --- 限流了,请稍后再试
// ============================================================
bool AuthService::check_rate_limit(int user_id) {
auto now = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> lock(rate_limit_mutex_); // 线程安全
// 在哈希表中查找这个用户
auto it = last_submit_.find(user_id);
if (it != last_submit_.end()) {
// 计算距离上次提交过去了多少秒
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - it->second).count();
if (elapsed < submit_window_sec_) {
return false; // 不到 10 秒,拒绝!
}
}
// 更新最后提交时间
last_submit_[user_id] = now;
return true; // 可以提交
}
限流工作流程
cpp
时间线: 用户 ID=5 的提交记录
───────────────────────────────────────────────
t=0s 用户提交第 1 次 → check_rate_limit(5) → true
last_submit_[5] = t=0s
t=3s 用户想提交第 2 次 → check_rate_limit(5)
elapsed = 3 - 0 = 3s < 10s → false "请稍后"
t=8s 用户想提交第 3 次 → check_rate_limit(5)
elapsed = 8 - 0 = 8s < 10s → false "请稍后"
t=12s 用户提交第 4 次 → check_rate_limit(5)
elapsed = 12 - 0 = 12s ≥ 10s → true
last_submit_[5] = t=12s (更新)
t=15s 用户想提交第 5 次 → check_rate_limit(5)
elapsed = 15 - 12 = 3s < 10s → false
10:Session清理机制
cpp
// ============================================================
// 后台清理线程的主循环
// 每 cleanup_interval_min_ 分钟扫描一次 Session 目录
// 删除所有过期的 Session 文件
// ============================================================
void AuthService::cleanup_loop() {
// 启动时先清理一次
cleanup_expired();
// 循环等待 → 清理 → 再等待 → 再清理
std::unique_lock<std::mutex> lock(cleanup_mutex_);
while (!stop_cleanup_) {
// 等待 cleanup_interval_min_ 分钟或被 notify
// wait_for 的好处:既能定时醒来,也能被提前叫醒
cleanup_cv_.wait_for(lock,
std::chrono::minutes(cleanup_interval_min_));
if (stop_cleanup_) break; // 收到停止信号
lock.unlock(); // 清理时不需要锁
cleanup_expired(); // 执行清理
lock.lock(); // 重新加锁继续循环
}
}
// ============================================================
// 执行一次清理:扫描 Session 目录,删除过期文件
// ============================================================
void AuthService::cleanup_expired() {
DIR* dir = opendir(session_dir_.c_str()); // 打开目录
if (!dir) return;
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) { // 遍历目录中的每个文件
std::string name = entry->d_name;
// 只处理 .json 结尾的文件
if (name.size() <= 5 ||
name.substr(name.size() - 5) != ".json") continue;
std::string filepath = session_dir_ + "/" + name;
// 读取 Session 文件,检查是否过期
std::ifstream file(filepath);
if (!file.is_open()) continue;
nlohmann::json j;
try {
file >> j;
} catch (...) {
// JSON 解析失败 → 文件损坏 → 删除
file.close();
unlink(filepath.c_str());
continue;
}
file.close();
// 检查过期时间
auto expires_at = j.value("expires_at", 0LL);
auto now = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now());
if (expires_at < now) {
unlink(filepath.c_str()); // 删除过期文件
Logger::instance().debug("Cleaned up expired session: " + name);
}
}
closedir(dir);
}
11:总结:AuthService架构图
cpp
AuthService
┌────────────────────────────────────┐
│ │
注册请求 ──────────→ │ register_user() │
│ ├── 参数校验 │
│ ├── SQL 注入防护 │
│ ├── 密码哈希 (bcrypt/SHA-256) │
│ └── INSERT 数据库 │
│ │
登录请求 ──────────→ │ login() │
│ ├── 查数据库 │
│ ├── 验证密码 │
│ ├── 生成 Session ID │
│ └── 写 Session 文件 │
│ │
鉴权请求 ──────────→ │ authenticate() │
│ ├── 读 Session 文件 │
│ ├── 检查过期 │
│ └── 返回用户信息 │
│ │
登出请求 ──────────→ │ logout() │
│ └── 删除 Session 文件 │
│ │
提交代码 ──────────→ │ check_rate_limit() │
│ ├── 查上次提交时间 │
│ ├── 如果 <10秒 → 拒绝 │
│ └── 如果 ≥10秒 → 允许 │
│ │
│ 后台线程: │
│ cleanup_loop() ← 每 30 分钟 │
│ └── cleanup_expired() │
│ └── 删过期 Session 文件 │
└────────────────────────────────────┘
设计思路总结
| 设计 | 为什么 |
|---|---|
| 单例模式 | 整个系统只有一个认证服务,Session 目录和限流表全局共享 |
| 文件存储 Session | 不依赖 Redis,部署简单;20 人场景性能足够 |
| bcrypt 密码哈希 | 不能解密、暴力破解极慢、自带盐值 |
| SHA-256 降级 | 某些系统没有 bcrypt 时能自动切换,保证可用性 |
| SQL 注入防护 | mysql_real_escape_string() 转义所有用户输入 |
| 统一错误信息 | 登录失败不说 "用户名不存在",防止黑客试探 |
| 后台清理线程 | 自动删除过期 Session,防止磁盘被占满 |
| 限流表用哈希表 | O (1) 查找速度,即使 10000 个用户也不慢 |
| 线程安全 | mutex 保护限流表,多线程安全 |