基于 C++ 的用户认证系统开发:从注册登录到Redis 缓存优化

发布于:2025-05-01 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

一、系统架构与技术栈

二、注册功能实现详解

1. 输入格式验证:validateCredentials

2. 数据库注册:RegisterInfo 

3. Redis 缓存更新:AddToRedis 

三、登录功能实现详解

1. Redis 快速验证:FindInRedis

 2. 数据库校验与缓存更新:authenticate

3. 会话管理:AddSessionToRedis 

四、HTTP 接口实现

1. 注册接口:handle_register

2. 登录接口:handle_login 

五、完整代码 

 Login.hpp

Register.hpp 

 


一、系统架构与技术栈

本文实现一个完整的用户认证系统,包含注册、登录功能,技术栈如下:

  • HTTP 服务:使用httplib轻量级库处理 HTTP 请求
  • 数据存储:MySQL 存储用户持久化数据,Redis 实现缓存加速
  • 数据解析nlohmann/json处理 JSON 格式数据
  • 连接管理:自定义mysqlconn封装数据库连接
  • 工具支持:正则表达式验证输入格式,UUID 生成会话 ID

 系统流程图:

注册流程:客户端请求 -> 格式验证 -> 数据库写入 -> Redis缓存
登录流程:客户端请求 -> Redis快速验证 -> 数据库校验 -> 会话管理

二、注册功能实现详解

1. 输入格式验证:validateCredentials

inline bool validateCredentials(const string& username, const string& password) {
    regex usernamePattern("^[a-zA-Z0-9._-]{3,}$");  // 3位以上字母数字下划线
    regex passwordPattern("^[a-zA-Z0-9._-]{6,}$");  // 6位以上复杂密码
    return regex_match(username, usernamePattern) && 
           regex_match(password, passwordPattern);
}
  • 功能:验证用户名密码是否符合安全规范
  • 核心逻辑
    • 使用 C++11 正则表达式库regex
    • 用户名要求:3-64 位,包含字母、数字、._-
    • 密码要求:6-64 位,同上增强复杂度
  • 返回值:格式正确返回true,否则false

2. 数据库注册:RegisterInfo 

inline bool RegisterInfo(const char *username, const char *password) {
    if(!validateCredentials(username, password)) return false;
    
    // 读取数据库配置(单例模式)
    DataBaseConf::DatabaseConfig& config = 
        DataBaseConf::getDatabaseConfig("./conf/db_config.json");
    
    // 数据库连接
    mysqlconn conn(config.host, config.username, config.password, config.dbName);
    
    // 拼接SQL(生产环境需用预处理防止注入)
    string query = "INSERT INTO users (username, password) VALUES ('" + 
                   string(username) + "', '" + string(password) + "')";
    
    conn.executeSql(query);  // 执行SQL语句
    return true;
}

  • 功能:将用户信息写入 MySQL 数据库
  • 核心步骤
    1. 先调用validateCredentials进行格式校验
    2. 通过单例模式读取db_config.json数据库配置
    3. 使用自定义mysqlconn类封装的连接接口
    4. 执行 INSERT 语句(注意:实际需防范 SQL 注入)

3. Redis 缓存更新:AddToRedis 

inline void AddToRedis(const string& username, const string& password) {
    string redisconn = "tcp://" + 
        (getenv("REDIS_NAME") ? getenv("REDIS_NAME") : "127.0.0.1") + ":6379";
    
    sw::redis::Redis redis(redisconn);  // 创建Redis连接
    // 使用哈希表存储用户信息:键=user:username:password,字段=用户名,值=密码
    redis.hset("user:username:password", username, password);
}
  • 功能:将新注册用户信息缓存到 Redis
  • 技术细节
    • 支持 Docker 部署:通过环境变量REDIS_NAME获取容器名
    • 使用sw/redis++客户端操作 Redis
    • 存储结构:哈希表(Hash),适合存储键值对集合
    • 优势:后续登录可直接从 Redis 快速查询,减少数据库压力

三、登录功能实现详解

1. Redis 快速验证:FindInRedis

inline bool FindInRedis(const char* username, const char* password) {
    string redisconn = getRedisConn();  // 复用连接字符串获取逻辑
    sw::redis::Redis redis(redisconn);
    // hget获取指定字段值,返回optional<string>
    auto result = redis.hget("user:username:password", username);
    return result.has_value() && result.value() == password;
}
  • 功能:从 Redis 缓存中验证用户名密码
  • 核心逻辑
    1. 复用注册时的 Redis 连接逻辑
    2. 使用hget命令获取哈希表中对应字段值
    3. 存在且密码匹配时返回true
  • 性能优势:内存级访问速度,比数据库查询快 2-3 个数量级

 2. 数据库校验与缓存更新:authenticate

inline bool authenticate(const char *username, const char *password) {
    if(FindInRedis(username, password)) return true;  // 先查Redis
    
    // 数据库连接(同注册逻辑)
    DataBaseConf::DatabaseConfig& config = DataBaseConf::getDatabaseConfig(...);
    mysqlconn conn(...);
    
    // 数据库查询
    string query = "SELECT COUNT(*) FROM users WHERE username = '" + 
                   string(username) + "' AND password = '" + string(password) + "';";
    MYSQL_RES *result = conn.executeSql(query);
    
    // 解析结果
    MYSQL_ROW row = mysql_fetch_row(result);
    int count = atoi(row[0]);
    mysql_free_result(result);
    
    // 命中数据库则更新Redis缓存
    if(count > 0) AddToRedis_login(username, password);  // 同AddToRedis
    return count > 0;
}

  • 功能:完整认证逻辑(Redis + 数据库双重校验)
  • 执行流程
    1. 先尝试 Redis 快速验证(缓存穿透处理)
    2. 未命中时查询 MySQL(使用 COUNT (*) 轻量查询)
    3. 数据库命中则更新 Redis 缓存(缓存预热)
  • 设计模式:Cache-Aside 模式(应用控制缓存与数据库一致性)

3. 会话管理:AddSessionToRedis 

inline void AddSessionToRedis(const string& username, const string& sessionid) {
    // 生成时间戳(Unix时间,秒级)
    auto now = chrono::system_clock::now();
    int64_t timestamp = chrono::duration_cast<chrono::seconds>(
        now.time_since_epoch()
    ).count();
    
    // 构造会话数据(包含用户名和最后访问时间)
    json sessionData;
    sessionData["username"] = username;
    sessionData["lasttime"] = timestamp;
    
    // 存储到Redis(键=session:sessionid,字段=data,值=JSON字符串)
    sw::redis::Redis redis(getRedisConn());
    redis.hset("session:" + sessionid, "data", sessionData.dump());
}

  • 功能:生成并存储用户会话信息
  • 技术要点
    • 会话 ID 生成:使用UUIDHelper::uuid()生成唯一 ID(需自定义实现)
    • 数据结构:Redis 哈希表存储会话详情,便于后续扩展字段
    • 时间戳:用于实现会话超时机制(后续可添加过期时间EXPIRE
    • 响应头:通过Set-Cookie头将 SessionID 返回客户端

四、HTTP 接口实现

1. 注册接口:handle_register

inline void handle_register(...) {
    // 方法校验:仅允许POST
    if(request.method != "POST") {
        response.status = 405;
        return;
    }
    
    // 内容类型校验:必须为JSON
    if(contentType.find("application/json") == npos) {
        response.status = 415;
        return;
    }
    
    // 解析JSON请求体
    json j = json::parse(requestBody);
    string username = j.value("username", "");
    string password = j.value("password", "");
    
    // 核心逻辑调用
    bool isValid = RegisterInfo(username.c_str(), password.c_str());
    if(isValid) {
        AddToRedis(username, password);  // 注册成功后更新缓存
        response.set_header("Content-Type", "application/json");
        response.body = R"({"message":"Login successful"})";  // 注意转义
    }
}

  • 接口规范
    • 路径:需在服务器路由中绑定(如POST /register
    • 请求体:JSON 格式,包含usernamepassword字段
    • 响应:200 成功 / 401 格式错误 / 415 类型不支持

2. 登录接口:handle_login 

inline void handle_login(...) {
    // 与注册接口类似的请求校验逻辑
    ...
    
    // 生成SessionID(假设UUIDHelper已实现)
    string sessionid = UUIDHelper::uuid();
    
    // 核心认证逻辑
    bool isValid = authenticate(username.c_str(), password.c_str());
    if(isValid) {
        AddSessionToRedis(username, sessionid);  // 存储会话信息
        // 设置Cookie头(注意:生产环境需HTTPS和Secure标志)
        response.set_header("Set-Cookie", "SessionID=" + sessionid);
        response.body = R"({"message":"Login successful"})";
    }
}
  • 安全增强点
    • SessionID 应使用 UUID 保证唯一性
    • Cookie 应设置HttpOnly防止 XSS
    • 建议添加 CSRF 令牌保护(代码中未实现)

五、完整代码 

 Login.hpp

#include <string>
#include <stdexcept>
#include <nlohmann/json.hpp>
#include <sw/redis++/redis++.h>
#include <cstring>
#include "httplib.h"
#include "mysqlconn.hpp"
#include "utility.hpp"
using namespace std;
using namespace httplib;
using json = nlohmann::json;

inline bool FindInRedis(const char* username, const char* password)
{
    const char* redisenv = getenv("REDIS_NAME");
    string redisconn = "tcp://";
    if(redisenv == nullptr)
        redisconn = redisconn + "127.0.0.1" + ":6379";
    else   
        redisconn = redisconn + redisenv + ":6379";
    sw::redis::Redis redis(redisconn);
    auto result = redis.hget("user:username:password", username);
    if(result)
        return result.value() == password;
    return false;
}

inline void AddToRedis_login(const string& username, const string& password)
{
    const char* redisenv = getenv("REDIS_NAME");
    string redisconn = "tcp://";
    if(redisenv == nullptr)
        redisconn = redisconn + "127.0.0.1" + ":6379";
    else   
        redisconn = redisconn + redisenv + ":6379";
    sw::redis::Redis redis(redisconn);
    redis.hset("user:username:password", username, password);
}

inline void AddSessionToRedis(const string& username, const string& sessionid)
{
    auto now = chrono::system_clock::now();
    auto duration = now.time_since_epoch();
    int64_t timestamp = chrono::duration_cast<chrono::seconds>(duration).count();

    json j;
    j["username"] = username;
    j["lasttime"] = timestamp;
    string sessiondata = j.dump();

    const char* redisenv = getenv("REDIS_NAME");
    string redisconn = "tcp://";
    if(redisenv == nullptr)
        redisconn = redisconn + "127.0.0.1" + ":6379";
    else   
        redisconn = redisconn + redisenv + ":6379";
    sw::redis::Redis redis(redisconn);
    auto result = redis.hset("session:" + sessionid, "data", sessiondata);
    if (result) 
        cout << "Session data added to Redis successfully!" << endl;
    else 
        cerr << "Failed to add session data to Redis." << endl;
}

inline bool authenticate(const char *username, const char *password)
{
    const string jsonpath = "./conf/db_config.json";
    DataBaseConf::DatabaseConfig& config = DataBaseConf::getDatabaseConfig(jsonpath);

    mysqlconn conn(config.host, config.username, config.password, config.dbName);
    if(FindInRedis(username, password))
        return true;

    string query = "SELECT COUNT(*) FROM users WHERE username = '" + string(username) + "' AND password = '" + string(password) + "';";

    MYSQL_RES *result = conn.executeSql(query);

    if (result == NULL)
    {
        fprintf(stderr, "Failed to get query, sql: %s\n", query.c_str());
        return false;
    }

    MYSQL_ROW row = mysql_fetch_row(result);
    int count = atoi(row[0]);

    mysql_free_result(result);

    if(count > 0)
    {
        AddToRedis_login(username, password);
        return true;
    }
    return false;
}

inline void handle_login(const httplib::Request &request, httplib::Response &response)
{
    if (request.method != "POST")
    {
        response.status = 405;
        response.body = "Only POST requests are allowed for login.";
        return;
    }

    string contentType = request.get_header_value("Content-Type");
    if (contentType.find("application/json") == string::npos)
    {
        response.status = 415;
        response.body = "Login request must have a Content-Type of application/json.";
        return;
    }

    size_t contentLength = 0;
    if(request.has_header("Content-Length"))
        contentLength = stoi(request.get_header_value("Content-Length"));
    if (contentLength == 0)
    {
        response.status = 400;
        response.body = "Login request must have a non-empty JSON body.";
        return;
    }

    string requestBody(request.body, 0, contentLength);

    json j;
    try
    {
        j = json::parse(requestBody);
    }
    catch (const json::parse_error &e)
    {
        response.status = 400;
        response.body = "Invalid JSON in login request.";
        return;
    }

    string username = j.value("username", "");
    string password = j.value("password", "");

    string sessionid = UUIDHelper::uuid();

    bool isValid = authenticate(username.c_str(), password.c_str());

    if (isValid)
    {
        AddSessionToRedis(username, sessionid);
        response.status = 200;
        response.set_header("Content-Type", "application/json");
        response.set_header("Set-Cookie", "SessionID=" + sessionid);
        response.body = "{\"message\":\"Login successful\"}";
    }
    else
    {
        response.status = 401;
        response.set_header("Content-Type", "application/json");
        response.body = "{\"message\":\"Invalid username or password\"}";
    }
}

Register.hpp 

#include "httplib.h"
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mysql/mysql.h>
#include <string>
#include <nlohmann/json.hpp>
#include <sw/redis++/redis++.h>
#include "mysqlconn.hpp"
#include "utility.hpp"
using namespace std;
using namespace httplib;
using json = nlohmann::json;

inline bool validateCredentials(const string& username, const string& password) 
{
    regex usernamePattern("^[a-zA-Z0-9._-]{3,}$");
    regex passwordPattern("^[a-zA-Z0-9._-]{6,}$");
    return regex_match(username, usernamePattern) && regex_match(password, passwordPattern);
}

inline bool RegisterInfo(const char *username, const char *password)
{
    if(!validateCredentials(username, password))
        return false;
    const string jsonpath = "./conf/db_config.json";
    DataBaseConf::DatabaseConfig& config = DataBaseConf::getDatabaseConfig(jsonpath);
    mysqlconn conn(config.host, config.username, config.password, config.dbName);
    string query = "INSERT INTO users (username, password) VALUES ('" + string(username) + "', '" + string(password) + "')";
    conn.executeSql(query);
    return true;
}

inline void AddToRedis(const string& username, const string& password)
{
    const char* redisenv = getenv("REDIS_NAME");
    string redisconn = "tcp://";
    if(redisenv == nullptr)
        redisconn = redisconn + "127.0.0.1" + ":6379";
    else   
        redisconn = redisconn + redisenv + ":6379";
    sw::redis::Redis redis(redisconn);
    redis.hset("user:username:password", username, password);
}

inline void handle_register(const httplib::Request &request, httplib::Response &response)
{
    if (request.method != "POST")
    {
        response.status = 405;
        response.body = "Only POST requests are allowed for login.";
        return;
    }
    string contentType = request.get_header_value("Content-Type");
    if (contentType.find("application/json") == string::npos)
    {
        response.status = 415;
        response.body = "Login request must have a Content-Type of application/json.";
        return;
    }
    size_t contentLength = 0;
    if(request.has_header("Content-Length"))
        contentLength = stoi(request.get_header_value("Content-Length"));
    if (contentLength == 0)
    {
        response.status = 400;
        response.body = "Login request must have a non-empty JSON body.";
        return;
    }
    string requestBody(request.body, 0, contentLength);
    json j;
    try
    {
        j = json::parse(requestBody);
    }
    catch (const json::parse_error &e)
    {
        response.status = 400;
        response.body = "Invalid JSON in login request.";
        return;
    }
    string username = j.value("username", "");
    string password = j.value("password", "");
    bool isValid = RegisterInfo(username.c_str(), password.c_str());
    if (isValid)
    {
        AddToRedis(username, password);
        response.status = 200;
        response.set_header("Content-Type", "application/json");
        response.body = "{\"message\":\"Login successful\"}";
    }
    else
    {
        response.status = 401;
        response.set_header("Content-Type", "application/json");
        response.body = "{\"message\":\"Invalid username or password\"}";
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到