目录
一、系统架构与技术栈
本文实现一个完整的用户认证系统,包含注册、登录功能,技术栈如下:
- 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 位,同上增强复杂度
- 使用 C++11 正则表达式库
- 返回值:格式正确返回
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 数据库
- 核心步骤:
- 先调用
validateCredentials
进行格式校验 - 通过单例模式读取
db_config.json
数据库配置 - 使用自定义
mysqlconn
类封装的连接接口 - 执行 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 快速查询,减少数据库压力
- 支持 Docker 部署:通过环境变量
三、登录功能实现详解
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 缓存中验证用户名密码
- 核心逻辑:
- 复用注册时的 Redis 连接逻辑
- 使用
hget
命令获取哈希表中对应字段值 - 存在且密码匹配时返回
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 + 数据库双重校验)
- 执行流程:
- 先尝试 Redis 快速验证(缓存穿透处理)
- 未命中时查询 MySQL(使用 COUNT (*) 轻量查询)
- 数据库命中则更新 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 返回客户端
- 会话 ID 生成:使用
四、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 格式,包含
username
和password
字段 - 响应: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\"}";
}
}