【Qt加密播放器】登录窗口

LoginForm的UI设计

【Qt加密播放器】登录窗口_第1张图片

关于背景渐变色的说明:

background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 
	rgba(255, 255, 0, 255), stop:1 rgba(255,255,202, 255));

【Qt加密播放器】登录窗口_第2张图片
登录按钮圆角矩形的实现:

QPushButton{
    color: rgb(0, 0, 0);   //设置文本颜色为黑色
    border:0px groove gray;  //设置边框样式为0像素宽的灰色凹槽边框
    border-radius:5px;    //设置圆角矩形的圆角的旋转半径为5个像素(半径越小越尖锐
    padding:2px 4px;    //设置按钮的内边距为2像素上下和4像素左右的空白。
    border-style: outset;   //设置边框样式为凸起的效果,使按钮的边框呈现立体效果。
	background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(255, 226, 78), stop:1 rgba(255, 236, 125, 255));  //渐变背景色
}

【Qt加密播放器】登录窗口_第3张图片

窗口的丝滑移动

【Qt加密播放器】登录窗口_第4张图片
通过重写鼠标移动相关函数来实现,即在loginForm.h中重新声明这些函数:

virtual void mouseMoveEvent(QMouseEvent* event);
virtual void mousePressEvent(QMouseEvent* event);
virtual void mouseReleaseEvent(QMouseEvent* event);

loginForm.cpp中的实现如下:

void LoginForm::mousePressEvent(QMouseEvent *event)
{
    position = event->globalPos() - this->pos();
}

positionLoginForm类中的私有成员。
使用 event->globalPos() 获取了当前鼠标按下的全局坐标位置,即鼠标相对于整个屏幕的位置。然后减去this->pos(),即当前窗口的位置,可以得到鼠标相对于窗口的位置。

void LoginForm::mouseMoveEvent(QMouseEvent *event)
{
    move(event->globalPos() - position);
}

使用 event->globalPos() 获取了鼠标相对于整个屏幕的位置。减去之前保存的 position 变量,即鼠标相对于窗口最初按下位置的偏移量,得到了鼠标的新位置。
调用 move 函数,将窗口移动到新的位置。

void LoginForm::mouseReleaseEvent(QMouseEvent *event)
{

}

release函数不需要多余的操作。

启动和登录时序图

  1. 先构造登录窗口LoginForm和主窗口Widget,然后将Widget隐藏,LoginForm显示。
  2. 加载配置文件RecordFile
  3. 当按下登录按钮时,向服务器确认用户名和密码,如果正确,则显示播放器窗口Widget,如果错误则弹出提示窗口InfoForm

【Qt加密播放器】登录窗口_第5张图片

main函数

Widget w;//主窗口
LoginForm login;//登录窗口
w.hide();//一开始,主窗口隐藏
login.show();//登录窗口显示
login.connect(&login, SIGNAL(login(const QString&, const QByteArray&)),
              &w, SLOT(on_show(const QString&, const QByteArray&)));
              //若登录成功,则展示主窗口

login信号和主窗口的on_show槽函数connect在一起,若成功登录,则显示主窗口。

RecordFile类

提供了使用 RSA 加密和解密技术从文件中读取和写入 JSON 数据的功能。主要表现为,根据文件路径读取文件内容,进行解密和解析操作,将解析结果存储到成员变量中。在解析成功时直接返回,否则设置默认值后返回。

//配置和信息记录文件类
class RecordFile
{
public:
    RecordFile(const QString& path);
    ~RecordFile();
    QJsonObject& config();
    bool save();
private:
    QJsonObject m_config;
    QString m_path;
    SslTool tool;
};

在构造函数中,传入QString类型的path,使用do…while(false),当出错时及时收手,并保证无论如何都有初始值

RecordFile::RecordFile(const QString& path)
{
    QFile file(path);
    m_path = path;
    do
    {
        ...
    }
    while(false);
    //读取失败,则设置默认值
    ...
    return;
}

以只读方式打开文件,注意判错:

QByteArray data = file.readAll();
if(data.size() <= 0)break;

使用 RSA 解码函数 (tool.rsaDecode()) 对 data 进行解码,然后进行分析。

data = tool.rsaDecode(data);
int i = 0;
for(; i < data.size(); i++)
{
    if((int)data[i] >= (int)0x7F || (int)data[i] < (int)0x0A)
    {
        data.resize(i);
        break;
    }
}

使用 QJsonDocument::fromJson() 函数将处理后的 data 解析为 JSON 文档。如果解析成功且文档非空,则将该对象赋给成员变量 m_config,然后返回。
否则输出错误信息。

QJsonParseError json_error;
QJsonDocument doucment = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError)
{
    if (doucment.isObject())
    {
        m_config = doucment.object();
        return;//成功了,直接返回
    }
}
else
{
    qDebug().nospace() << __FILE__ << "(" << __LINE__ << "):" << json_error.errorString() << json_error.error;
}

当读取失败时,设置默认值。

//读取失败,则设置默认值
file.close();
QJsonValue value = QJsonValue();
m_config.insert("user", value);
m_config.insert("password", value);
m_config.insert("date", "2024-01-01 10:10:10");
return;

LoginForm

初始化

  • 配置和记录信息文件
record = new RecordFile("xxxxxxxx");//path
  • 设置无头窗口
this->setWindowFlag(Qt::FramelessWindowHint);
  • 设置输入框的默认占位文本,并将边框设为不可见(这样可以使输入框看起来没有边框)。
  • 设置为密码输入模式。在密码输入模式下,输入的文本会被隐藏显示为密码掩码,通常是星号或圆点,以保护用户输入的敏感信息。
ui->nameEdit->setPlaceholderText(u8"用户名/手机号/邮箱");
ui->nameEdit->setFrame(false);
ui->pwdEdit->setPlaceholderText(u8"填写密码");
ui->pwdEdit->setFrame(false);
ui->pwdEdit->setEchoMode(QLineEdit::Password);
  • 创建一个QNetworkAccessManager对象net,用于发送HTTP请求。
  • 连接net对象的finished信号和LoginForm的slots_login_request_finshed槽函数。
net = new QNetworkAccessManager(this);
connect(net, SIGNAL(finished(QNetworkReply*)),
        this, SLOT(slots_login_request_finshed(QNetworkReply*)));
  • 设置提示窗口info
  • 确保在弹出的反馈窗口关闭之前,其他窗口不能被点击——ApplicationModal
info.setWindowFlag(Qt::FramelessWindowHint);//无头窗口
info.setWindowModality(Qt::ApplicationModal);
QSize sz = size();
info.move((sz.width() - info.width()) / 2, (sz.height() - info.height()) / 2);//调整大小

登录

登录逻辑
【Qt加密播放器】登录窗口_第6张图片

当登录按钮被按下

void LoginForm::on_logoButton_released()
{
    QString user = ui->nameEdit->text();
    //检查用户名的有效性
    if(user.size() == 0 || user == u8"用户名/手机号/邮箱")
    {
        info.set_text(u8"用户不能为空\r\n请输入用户名", u8"确认").show();
        ui->nameEdit->setFocus();
        return;
    }
    //检查密码的有效性
    QString pwd = ui->pwdEdit->text();
    if(pwd.size() == 0 || pwd == u8"填写密码")
    {
        info.set_text(u8"密码不能为空\r\n请输入密码", u8"确认").show();
        ui->pwdEdit->setFocus();
        return;
    }
    check_login(user, pwd);
}

检查用户名和密码的有效性,如果用户名或密码输入为空,则对InfoForm设置对应的文字,并将其show()。
比如:当为填写用户名
【Qt加密播放器】登录窗口_第7张图片

检查登录

  • 问题1:不能将密码放到请求信息里,因为公共网络不安全
  • 解决方法:
    • 用本地的用户名和密码计算一个MD5,将用户名和MD5发送到服务器
    • 服务器拿到用户名,从数据库里取密码,也计算MD5
    • 若两个MD5相同,则密码正确
  • 问题2:用户名和密码一般不变,计算出的MD5总是相同。漏洞:可以将这个MD5保存起来,用这个MD5去请求服务器
  • 解决方法:
    • 将时间精确到秒,加入到MD5的计算中
    • 加入噪声点salt,提高MD5丰富度,提高破解难度
  • 总结:时间+用户名+密码+随机插入噪声点 = MD5

随机输入一个md5_key

const char* MD5_KEY = "*&^%$#@b.v+h-b*g/h@n!h#n$d^ssx,.kl;
bool LoginForm::check_login(const QString& user, const QString& pwd)
{
    QCryptographicHash md5(QCryptographicHash::Md5);
    QNetworkRequest request;
    QString url = QString(HOST) + "/login?";
    QString salt = QString::number(QRandomGenerator::global()->bounded(10000, 99999));
    QString time = getTime();

    md5.addData((time + MD5_KEY + pwd + salt).toUtf8());
    QString sign = md5.result().toHex();
    url += "time=" + time + "&";
    url += "salt=" + salt + "&";
    url += "user=" + user + "&";
    url += "sign=" + sign;

    request.setUrl(QUrl(url));
    record->config()["password"] = ui->pwdEdit->text();
    record->config()["user"] = ui->nameEdit->text();
    this->setEnabled(false);//防止用户狂点登录,将窗口禁用
    net->get(request);// ---->slots_login_request_finshed
    return true;
}

调用 net->get(request) 发起网络请求,进入异步的网络请求流程,请求完成后将会触发 slots_login_request_finshed 槽函数

处理登录请求完成后的相应

void LoginForm::slots_login_request_finshed(QNetworkReply* reply)
  • 首先调用 this->setEnabled(true) 将登录窗体重新启用。
 this->setEnabled(true);
  • 检查网络响应是否发生错误。如果有错误发生,则通过调用 reply->errorString() 获取错误信息,并使用 info.set_text() 设置一个提示框的文本,显示登录发生错误的提示信息。
    if(reply->error() != QNetworkReply::NoError)
    {
        info.set_text(u8"登录发生错误\r\n" + reply->errorString(), u8"确认").show();
        return;
    }
  • 如果网络响应没有发生错误,则读取响应的数据通过 reply->readAll() 返回的 QByteArray 对象中。
 QByteArray data = reply->readAll();
  • 使用 QJsonDocument::fromJson() 解析响应数据为 QJsonDocument 对象,并检查解析错误的信息,如果解析错误码为 QJsonParseError::NoError,则表示解析成功。
 QJsonParseError json_error;
 QJsonDocument doucment = QJsonDocument::fromJson(data, &json_error);
  • 在成功解析的情况下,通过 obj.contains() 判断响应对象中是否包含特定的键和值。如果包含 statusmessage 键,则进一步获取对应的值,并检查登录状态是否为成功状态(status.toInt(-1) == 0)。如果登录成功,则将 LOGIN_STATUS 设置为 true,发射 login() 信号,隐藏登录窗体,并标记登录成功,然后更新登录时间,最后保存记录对象的配置信息。
if (json_error.error == QJsonParseError::NoError)
{
    if (doucment.isObject())
    {
        const QJsonObject obj = doucment.object();
        if (obj.contains("status") && obj.contains("message"))
        {
            QJsonValue status = obj.value("status");
            QJsonValue message = obj.value("message");
            if(status.toInt(-1) == 0) //登录成功
            {
                LOGIN_STATUS = status.toInt(-1) == 0;
                emit login(record->config()["user"].toString(), QByteArray());
                hide();
                login_success = true;
                char tm[64] = "";
                time_t t;
                time(&t);
                strftime(tm, sizeof(tm), "%Y-%m-%d %H:%M:%S", localtime(&t));
                record->config()["date"] = QString(tm);//更新登录时间
                record->save();
            }
        }
    }
}
  • 如果解析错误码不为 QJsonParseError::NoError,则表示解析失败,通过 info.set_text() 设置提示框的文本,显示登录失败的提示信息。
info.set_text(u8"登录失败\r\n无法解析服务器应答!", u8"确认").show();
  • 如果登录不成功,则通过 info.set_text() 设置提示框的文本,显示用户名或密码错误的提示信息。
if(!login_success)
{
    info.set_text(u8"登录失败\r\n用户名或者密码错误!", u8"确认").show();
}
  • 最后调用 reply->deleteLater() 删除响应对象。
reply->deleteLater();

当登陆成功,将会emit login信号。在构造函数中已将loginWidget::on_show()conncet在一起,所以登录成功则会显示主窗口。

你可能感兴趣的:(Qt加密播放器,qt,开发语言)