一个OJ系统的诞生(五)Service层上--AuthService用户认证系统

前面我们讲了工具层、数据库层和模型层,现在终于进入真正的业务逻辑层!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 保护限流表,多线程安全