上位机与后端传递消息最基础的一个就是登录界面的实现,上位机这边输入用户名和密码传递给服务器检测用户名和密码的准确性。最常见的用法是使用HTTPS,但是本文使用的是HTTP(不要问为什么,问就是业务文档让用HTTP)。
1.HTTP与HTTPS
1)安全性差异
特性 | HTTP | HTTPS |
---|---|---|
加密 | ❌ 明文传输,数据可被窃听/篡改 | ✅ 使用SSL/TLS加密,防止中间人攻击 |
数据完整性 | ❌ 无校验机制 | ✅ 通过哈希算法验证数据完整性 |
身份认证 | ❌ 无法验证服务器身份 | ✅ 通过CA证书验证服务器真实性 |
2)关键组件对比
组件 | HTTP | HTTPS |
---|---|---|
默认端口 | 80 |
443 |
协议处理器 | QNetworkAccessManager |
同左,但依赖QSslSocket |
证书验证 | 不需要 | 需CA证书(否则需手动忽略错误) |
依赖库 | Qt Network模块 | Qt Network + OpenSSL库(需额外安装) |
3)部署要求
HTTP:无需额外配置,但禁止传输敏感数据。
HTTPS:
服务器必须配置有效SSL证书(如Let's Encrypt)。
客户端需链接OpenSSL库(编译Qt时启用
-openssl
选项)。处理证书错误(开发环境可能需要临时忽略)
总结建议
场景 | 推荐协议 |
---|---|
公开信息(如新闻) | HTTP |
登录/支付/API数据传输 | HTTPS |
本地测试 | HTTP |
生产环境 | 必须HTTPS |
所以“登录”这一需求需要用的协议应该是HTTPS,可惜没有这个条件,希望下次可以使用到这个协议。因为很多协议光看网上说如何实现可能会有点复杂繁琐,但是实际使用过程中会发现其实蛮简单的。
2.QT中使用
用到的类:
QNetworkAccessManager* m_pNetworkManager;
QNetworkReply* m_pReply = nullptr;
实现流程:构建API端点URL——》构建json请求——》设置HTTP请求——》设置连接超时处理——》发送请求——》处理回复消息
1)初始化
void LoginController::loginUser(const QString& strUserName, QString strPwd, JMLConfig::eUserRole role)
{
//输入检查
QString strRole = roleToString(role);
if (!validateInput(strUserName, strPwd, strRole))
{
emit loginFailed("Invalid input");
return;
}
//构建API端点URL
QUrl apiEndpoint;
apiEndpoint.setScheme("http");
apiEndpoint.setHost(m_serverIP);
apiEndpoint.setPort(m_serverPort);
apiEndpoint.setPath("/login");
//使用base64加密
QString base64Pwd = QString(QByteArray(strPwd.toUtf8()).toBase64());
//清除原始密码内存
secureClearPassword(strPwd);
//JSON请求
QJsonObject jsonPayload;
jsonPayload["loginName"] = sanitizeInput(strUserName);
jsonPayload["password"] = base64Pwd;
QJsonDocument doc(jsonPayload);
QByteArray data = doc.toJson();
//设置HTTP请求
QNetworkRequest request(apiEndpoint);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("User-Agent", "Industrial-Secure-Client/2.0");
//设置超时处理
QTimer* timeoutTimer = new QTimer(this);
timeoutTimer->setSingleShot(true);
timeoutTimer->start(15000);
connect(timeoutTimer, &QTimer::timeout, this, [=]() {
if (m_pReply && m_pReply->isRunning()) {
m_pReply->abort();
emit loginFailed("连接超时 (15秒)");
timeoutTimer->deleteLater();
}
});
//发送请求
m_pReply = m_pNetworkManager->post(request, data);
connect(m_pReply, &QNetworkReply::finished, this, [this]() {
handleLoginResponse(m_pReply);
});
connect(m_pReply, &QNetworkReply::finished, timeoutTimer, &QObject::deleteLater);
}
这个api需要与后端协商好,一般都是后端做好接口之后主动提供接口文档,可以对照文档来写接收信息的内容。
一开始想要判定IP地址和端口的可达性,可以使用QTcpSocket来判定,或者WIN+R输入cmd,输入telent IP地址 端口号来判定这个ip和端口是否能通信。
QTcpSocket testSocket;
testSocket.connectToHost("127.0.0.0", 8080);
if (testSocket.waitForConnected(3000)) {
//qDebug() << "服务器可达";
emit loginSuccess("0", "服务器可达");
}
else {
//qDebug() << "无法连接服务器:" << testSocket.errorString();
emit loginFailed(testSocket.errorString());
return;
}
2)处理回复
void LoginController::handleLoginResponse(QNetworkReply* reply)
{
m_pReply = nullptr;
//处理网络错误
if (reply->error() != QNetworkReply::NoError)
{
if (reply->error() == QNetworkReply::OperationCanceledError)
{
emit loginFailed("连接超时");
}
else
{
emit loginFailed("网络错误: " + reply->errorString());
}
reply->deleteLater();
return;
}
//处理HTTP状态码
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode != 200)
{
emit loginFailed(QString("服务器错误: HTTP %1").arg(statusCode));
reply->deleteLater();
return;
}
//安全读取响应
QByteArray response = reply->readAll();
if (response.size() > 4096)
{ //防止过大响应导致内存耗尽
emit loginFailed("服务器响应异常");
reply->deleteLater();
return;
}
//解析JSON响应
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError)
{
emit loginFailed("服务器响应格式错误");
reply->deleteLater();
return;
}
QJsonObject json = doc.object();
//验证响应结构
if (!json.contains("success") || !json.contains("result"))
{
emit loginFailed("服务器响应格式错误");
reply->deleteLater();
return;
}
//处理登录结果
if (json["success"].toBool() == true)
{
//获取用户信息用于展示
QJsonObject result = json["result"].toObject();
if (result.isEmpty())
{
emit loginFailed(QStringLiteral("没有用户对应信息"));
reply->deleteLater();
return;
}
QString userType = result["userType"].toString();
QString userName = result["userName"].toString();
emit loginSuccess(userType, userName);
}
else
{
QString errorMsg = json["message"].toString();
emit loginFailed(errorMsg);
}
reply->deleteLater();
}
3)密码加密
这个应该蛮简单的,我这里用的是base64加密,最简单的加密方式。之前还用过hash加密,产生随机盐值然后进行加密。稍微记录一下,毕竟真的是太久不用就忘了。
QString LoginController::generateRandomSalt() const
{
//盐值的目的:
//使相同密码产生不同哈希值
//确保不同用户的相同密码哈希不同
//强制攻击者为每个用户单独计算
QByteArray salt;
salt.resize(16);
QRandomGenerator* generator = QRandomGenerator::system();
for (auto i = 0; i < salt.size(); ++i)
{
salt[i] = generator->generate() % 256;
}
return salt.toBase64();
}
QString LoginController::hashPassword(const QString& strPwd, const QString& strSalt)
{
QByteArray passwordData = strPwd.toUtf8();
QByteArray saltData= strSalt.toUtf8();
//参数:密码,盐,迭代次数,输出长度,哈希算法
QByteArray hashed = QCryptographicHash::hash(
passwordData + saltData,
QCryptographicHash::Sha256);
return hashed.toHex();
}