Identity, Session & Heartbeat

02. 身份认证与会话安全

围绕登录请求、token/session_id 注入、单账号会话覆盖与心跳保活,形成统一认证基线。

6 个核心函数4 个重点文件基于当前工程源码

模块职责

围绕登录请求、token/session_id 注入、单账号会话覆盖与心跳保活,形成统一认证基线。

重点文件

调用链

  1. 1客户端提交凭据
  2. 2服务端校验账号
  3. 3创建 token 与 session_id
  4. 4客户端启动心跳
  5. 5业务请求统一认证
客户端认证封装

ClientLogic::makeAuthedRequest

创建统一认证请求对象,自动写入 cmd、from、token 和 session_id。

clientlogic.cpp · L63–L71
原型QJsonObject ClientLogic::makeAuthedRequest(const QString& cmd) const

调用时机

所有登录后业务请求构造时调用。

返回说明

返回带认证字段的 QJsonObject。

参数

参数说明
cmd业务命令名称

执行流程

  1. 创建 JSON 对象
  2. 写入命令和用户标识
  3. 写入 token
  4. 写入 session_id

工程说明

减少各业务函数重复拼接认证字段。

关联接口

查看完整实现
clientlogic.cpp
QJsonObject ClientLogic::makeAuthedRequest(const QString& cmd) const
{
    QJsonObject obj;
    obj["cmd"] = cmd;
    obj["from"] = currentUser;
    obj["token"] = authToken;
    obj["session_id"] = sessionId;
    return obj;
}
客户端登录请求

ClientLogic::requestLogin

校验重复请求状态,构造登录 JSON 并发送至中心服务。

clientlogic.cpp · L293–L320
原型void ClientLogic::requestLogin(const QString& user, const QString& pwd)

调用时机

用户提交登录表单时调用。

返回说明

无返回值;结果通过 sigLoginResult 信号返回。

参数

参数说明
user登录账号
pwd登录凭据

执行流程

  1. 检查 pending 状态
  2. 校验输入
  3. 设置登录请求标记
  4. 构造 login 命令
  5. 发送请求

工程说明

异步结果通过 Qt 信号槽回到登录界面。

关联接口

查看完整实现
clientlogic.cpp
void ClientLogic::requestLogin(const QString& user, const QString& pwd)
{

    stopHeartbeat();
    currentUser.clear();
    authToken.clear();
    sessionId.clear();

    if (user.isEmpty() || pwd.isEmpty()) {
        emit sigLoginResult(false, "账号或密码不能为空");
        return;
    }

    if (m_loginPending) {
        emitUiLog("⚠️ 登录请求正在处理中,请勿重复点击");
        return;
    }

    m_loginPending = true;

    QJsonObject req;
    req["cmd"] = "login";
    req["user"] = user;
    req["pwd"] = pwd;

    netManager->sendJsonToServer(req);
}
客户端会话保活

ClientLogic::startHeartbeat

按服务端下发间隔启动心跳定时器,并重置失联计数。

clientlogic.cpp · L206–L224
原型void ClientLogic::startHeartbeat(int intervalSeconds)

调用时机

登录成功并获取 session_id 后调用。

返回说明

无返回值。

参数

参数说明
intervalSeconds心跳间隔秒数

执行流程

  1. 规范心跳间隔
  2. 创建或复用 QTimer
  3. 连接 timeout 信号
  4. 重置漏报计数
  5. 启动定时器

工程说明

心跳仅维护会话活性,不承载业务数据。

关联接口

查看完整实现
clientlogic.cpp
void ClientLogic::startHeartbeat(int intervalSeconds)
{
    if (!m_heartbeatTimer) {
        return;
    }

    if (intervalSeconds <= 0) {
        intervalSeconds = 10;
    }

    m_heartbeatIntervalMs = intervalSeconds * 1000;
    m_missedHeartbeatCount = 0;

    m_heartbeatTimer->stop();
    m_heartbeatTimer->setInterval(m_heartbeatIntervalMs);
    m_heartbeatTimer->start();

    emitUiLog(QString("✅ 登录态心跳已启动,间隔 %1 秒").arg(intervalSeconds));
}
服务端会话管理

NetworkServer::createLoginSessionForUser

为账号生成新 token 和 session_id,覆盖旧会话并记录客户端地址。

network_server.cpp · L245–L312
原型LoginSessionResult NetworkServer::createLoginSessionForUser(const std::string& username, const sockaddr_in& client_addr)

调用时机

账号凭据校验成功后调用。

返回说明

返回 LoginSessionResult,包含 token、session_id 与是否替换旧会话。

参数

参数说明
username用户标识
clientAddr本次登录来源地址

执行流程

  1. 生成随机 token/session
  2. 锁定会话表
  3. 判断旧会话
  4. 写入新会话与地址
  5. 返回会话结果

工程说明

同一账号的新会话会使旧 session 失效。

关联接口

查看完整实现
network_server.cpp
LoginSessionResult NetworkServer::createLoginSessionForUser(const std::string& username,
                                                            const sockaddr_in& client_addr)
{
    LoginSessionResult result;

    if (!isValidUserId(username)) {
        return result;
    }

    Session oldSession;
    bool hasOldSession = false;

    {
        std::lock_guard<std::mutex> lock(online_mutex);

        auto it = online_users.find(username);
        if (it != online_users.end()) {
            oldSession = it->second;
            hasOldSession = !oldSession.ip.empty() && oldSession.port > 0;
        }
    }

    result.token = makeRandomHexToken();
    result.sessionId = "sess_" + makeRandomHexToken();
    result.kickedOldSession = hasOldSession;

    {
        std::lock_guard<std::mutex> lock(token_mutex);

        auto oldTokenIt = user_to_token.find(username);
        if (oldTokenIt != user_to_token.end()) {
            token_to_user.erase(oldTokenIt->second);
        }

        user_to_token[username] = result.token;
        token_to_user[result.token] = username;
    }

    Session newSession = sessionFromAddr(client_addr);
    newSession.token = result.token;
    newSession.sessionId = result.sessionId;
    newSession.loginTime = nowSeconds();
    newSession.lastHeartbeat = newSession.loginTime;

    {
        std::lock_guard<std::mutex> lock(online_mutex);
        online_users[username] = newSession;
    }

    if (hasOldSession) {
        sockaddr_in oldAddr{};
        oldAddr.sin_family = AF_INET;

        if (inet_pton(AF_INET, oldSession.ip.c_str(), &oldAddr.sin_addr) == 1 &&
            oldSession.port > 0 && oldSession.port <= 65535) {
            oldAddr.sin_port = htons(static_cast<uint16_t>(oldSession.port));

            std::string reason = "账号已在其他客户端登录,当前客户端已下线";
            std::ostringstream oss;
            oss << "{\"cmd\":\"force_logout\",\"reason\":\"" << reason << "\"}";
            sendData(oss.str(), oldAddr);
        }
    }

    return result;
}
服务端登录业务

BusinessHandler::handleLogin

解析登录请求、校验数据库凭据、建立会话并返回认证信息。

business_handler.cpp · L426–L483
原型void BusinessHandler::handleLogin(const std::string& json_str, struct sockaddr_in client_addr)

调用时机

dispatchTask 识别 login 命令后调用。

返回说明

无直接返回;通过 NetworkServer 回包。

参数

参数说明
json_str客户端登录 JSON
client_addr客户端 UDP 地址

执行流程

  1. 解析账号密码
  2. 调用 DBManager 校验
  3. 创建登录会话
  4. 组织 login_resp
  5. 发送响应

工程说明

认证信息只在登录成功响应中下发。

关联接口

查看完整实现
business_handler.cpp
void BusinessHandler::handleLogin(const std::string& json_str, struct sockaddr_in client_addr) {
    try {
        json j = json::parse(json_str);
        std::string user = getStringField(j, "user");
        std::string pwd = getStringField(j, "pwd");

        json resp;
        resp["cmd"] = "login_resp";

        if (user.empty() || pwd.empty()) {
            resp["result"] = "fail";
            resp["msg"] = "账号或密码不能为空";
            net_server->sendData(resp.dump(), client_addr);
            return;
        }

        if (DBManager::getInstance().verifyLogin(user, pwd)) {
            resp["result"] = "success";
            resp["user"] = user;

            std::string ip = inet_ntoa(client_addr.sin_addr);
            int port = ntohs(client_addr.sin_port);

            LoginSessionResult session =
                net_server->createLoginSessionForUser(user, client_addr);

            if (session.token.empty() || session.sessionId.empty()) {
                resp["result"] = "fail";
                resp["msg"] = "创建登录会话失败,请稍后重试";
                net_server->sendData(resp.dump(), client_addr);
                return;
            }

            resp["token"] = session.token;
            resp["session_id"] = session.sessionId;
            resp["heartbeat_interval"] = 10;
            resp["session_timeout"] = 35;
            resp["kicked_old_session"] = session.kickedOldSession;

            std::cout << "✅ [用户登录] " << user
                      << " 登录成功,当前地址: " << ip << ":" << port
                      << ",session_id=" << session.sessionId
                      << ",旧会话踢出=" << (session.kickedOldSession ? "yes" : "no")
                      << "\n";
        } else {
            resp["result"] = "fail";
            resp["msg"] = "账号不存在或密码错误";
            std::cout << "❌ [登录拒绝] " << user << " 尝试登录失败\n";
        }

        net_server->sendData(resp.dump(), client_addr);
    } catch (std::exception& e) {
        std::cerr << "登录逻辑处理异常: " << e.what() << "\n";
    }
}
服务端统一鉴权

BusinessHandler::checkAuth

校验请求中的用户、token 与 session_id,失败时返回标准认证错误。

business_handler.cpp · L1049–L1095
原型bool BusinessHandler::checkAuth(const json& j, struct sockaddr_in client_addr, std::string& out_user)

调用时机

绝大多数登录后业务处理函数入口调用。

返回说明

认证通过返回 true,否则返回 false。

参数

参数说明
j业务 JSON 对象
client_addr请求来源地址
out_user认证成功后输出用户标识

执行流程

  1. 读取认证字段
  2. 校验字段完整性
  3. 验证 token/session
  4. 输出认证用户
  5. 必要时回传错误

工程说明

业务函数以该函数作为统一安全前置条件。

关联接口

查看完整实现
business_handler.cpp
bool BusinessHandler::checkAuth(const json& j,
                                struct sockaddr_in client_addr,
                                std::string& out_user)
{
    out_user.clear();

    json resp;
    resp["cmd"] = "auth_failed";

    std::string from = getStringField(j, "from");
    std::string token = getStringField(j, "token");
    std::string sessionId = getStringField(j, "session_id");

    if (token.empty()) {
        resp["reason"] = "缺少 token,请重新登录";
        net_server->sendData(resp.dump(), client_addr);
        return false;
    }

    if (sessionId.empty()) {
        resp["reason"] = "缺少 session_id,请重新登录";
        net_server->sendData(resp.dump(), client_addr);
        return false;
    }

    if (from.empty()) {
        resp["reason"] = "缺少 from 字段,请重新登录";
        net_server->sendData(resp.dump(), client_addr);
        return false;
    }

    if (!net_server || !net_server->verifySession(from, token, sessionId)) {
        resp["reason"] = "登录状态已失效,请重新登录";
        net_server->sendData(resp.dump(), client_addr);
        return false;
    }

    net_server->refreshHeartbeat(from, token, sessionId, client_addr);

    out_user = from;
    return true;
}