项目(9)(用户登录事件、单例模式、token验证码、关于传输数据的安全问题)

用户登录事件

登录使用的协议格式

1、客户端

  • post url:http://127.0.0.1:80/login
  • post数据:
{

     user:xxxx,

     pwd:xxx

}

2、服务器端

成功:

{

    "code": "000",

    "token": "xxx"

}

失败:

{

    "code": "001",

    "token": "faild"

}

3、token验证

  • token验证成功:{"token":"110"}
  • token验证失败:{"token":"111"}

需要验证的操作

  • 我的文件
  • 秒传
  • 分享文件
  • 删除文件

用户登录实现代码

整体流程

客户端

  1. 获取用户输入的登录数据
  2. 数据校验——使用正则表达式
  3. 将数据打包为json格式数据
  4. 将数据以post格式发送给server
  5. 接受服务器返回的数据,解析判断登录是否成功

服务端

  1. FCGI_Accept() 阻塞等待用户连接 - fastCGI函数接口
  2. 读取用户登录数据 - post/get
  3. 用户登录——将用户信息从json包中解析出来、拿到mysql数据库的用户名,密码、连接mysql数据库来查询用户是否存在
  4. 给客户端发送响应数据

客户端用户登录代码

// 用户登录操作
void Login::on_login_btn_clicked()
{
    // 获取用户登录信息
    QString user = ui->log_usr->text();
    QString pwd = ui->log_pwd->text();
    QString address = ui->address_server->text();
    QString port = ui->port_server->text();

    // 数据校验
    QRegExp regexp(USER_REG);
    if(!regexp.exactMatch(user))
    {
        QMessageBox::warning(this, "警告", "用户名格式不正确");
        ui->log_usr->clear();
        ui->log_usr->setFocus();
        return;
    }
    //setPattern修改正则规则
    regexp.setPattern(PASSWD_REG);
    if(!regexp.exactMatch(pwd))
    {
        QMessageBox::warning(this, "warning", "password formate is not suitable");
        ui->log_pwd->clear();
        ui->log_pwd->setFocus();
        return;
    }

    //判断记住密码选项是否被选中
    bool remember=ui->rember_pwd->isChecked();
    
    // 登录信息写入配置文件cfg.json
    // 登陆信息加密
    m_cm.writeLoginInfo(user, pwd,remember);
    
    // 设置登陆信息json包, 密码经过md5加密, getStrMd5()
    QByteArray array = setLoginJson(user, m_cm.getStrMd5(pwd));
    
    // 设置登录的url。request做东发送的头
    QNetworkRequest request;
    QString url = QString("http://%1:%2/login").arg(address).arg(port);
    request.setUrl(QUrl(url));
    
    // 请求头信息
    request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));
    request.setHeader(QNetworkRequest::ContentLengthHeader, QVariant(array.size()));
   
    // 向服务器发送post请求
    QNetworkReply* reply = m_manager->post(request, array);
    cout << "post url:" << url << "post data: " << array;

    // 接收服务器发回的http响应消息,也可以检测QNetworkReply::readyRead信号,这里检测的是finished信号
    connect(reply, &QNetworkReply::finished, [=]()
    {
        // 出错了
        if (reply->error() != QNetworkReply::NoError)
        {
            cout << reply->errorString();
            //释放资源
            reply->deleteLater();
            return;
        }

        // 将server回写的数据读出
        QByteArray json = reply->readAll();
        /*
            登陆 - 服务器回写的json数据包格式:
                成功:{
                       "code": "000",
                       "token": "xxx"
                     }
                失败:{
                       "code": "001",
                    	"token": "faild"
                    }
        */
        cout << "server return value: " << json;
        //解析服务器返回的json
        QStringList tmpList = getLoginStatus(json); //common.h
        if( tmpList.at(0) == "000" )
        {
            cout << "登陆成功";

            // 存储当前用户信息
            LoginInfoInstance *p = LoginInfoInstance::getInstance(); //获取单例
            p->setLoginInfo(user, address, port, tmpList.at(1));
            cout << p->getUser().toUtf8().data() << ", " << p->getIp() << ", " << p->getPort() << tmpList.at(1);

            // 当前窗口隐藏
            this->hide();
            // 主界面窗口显示
            m_mainWin->showMainWindow();
        }
        else
        {
            QMessageBox::warning(this, "登录失败", "用户名或密码不正确!!!");
        }

        reply->deleteLater(); //释放资源
    });
}

功能函数一—— 登陆用户需要使用的json数据包

// 登陆用户需要使用的json数据包
QByteArray Login::setLoginJson(QString user, QString pwd)
{
    QMap<QString, QVariant> login;
    login.insert("user", user);
    login.insert("pwd", pwd);

    /*json数据如下
        {
            user:xxxx,
            pwd:xxx
        }
    */

    QJsonDocument jsonDocument = QJsonDocument::fromVariant(login);
    if ( jsonDocument.isNull() )
    {
        cout << " jsonDocument.isNull() ";
        return "";
    }

    return jsonDocument.toJson();
}

功能函数二——登录信息,写入配置文件(如果记住密码被勾选,则调用该函数)

如果记住用户名密码,则需要先对用户名和密码进行两次加密之后,再保存到json文件中(为了安全)

// 登录信息,写入配置文件
void Common::writeLoginInfo(QString user, QString pwd, bool isRemeber, QString path)
{
    // web_server信息
    QString ip = getCfgValue("web_server", "ip");
    QString port = getCfgValue("web_server", "port");

    QMap<QString, QVariant> web_server;
    web_server.insert("ip", ip);
    web_server.insert("port", port);

    // type_path信息
    QMap<QString, QVariant> type_path;
    type_path.insert("path", m_typePath);

    // login信息
    QMap<QString, QVariant> login;

    // 登陆信息加密
    int ret = 0;

    // 登陆用户加密
    unsigned char encUsr[1024] = {0};
    int encUsrLen;
    // toLocal8Bit(), 转换为本地字符集,如果windows则为gbk编码,如果linux则为utf-8编码
    //先通过.toLocal8Bit()将QString转成QByteArray,在通过data()将QByteArray转成const char*
    //因为DesEnc的传入参数类型要求是const char*,所以需要进行转换
    //encUsr代表转完的数据放在哪,encUsrLen是转完之后的实际长度
    ret = DesEnc((unsigned char *)user.toLocal8Bit().data(), user.toLocal8Bit().size(), encUsr, &encUsrLen);
    if(ret != 0)//加密失败
    {
        cout << "DesEnc err";
        return;
    }

    // 用户密码加密
    unsigned char encPwd[512] = {0};
    int encPwdLen;
    // toLocal8Bit()——可以将QString转成QByteArray.QByteArray调用data函数转成char*
    //DesEnc是一个类,里面有两个功能,加密和解密(自己实现的类)
    ret = DesEnc((unsigned char *)pwd.toLocal8Bit().data(), pwd.toLocal8Bit().size(), encPwd, &encPwdLen);
    if(ret != 0)
    {
        cout << "DesEnc err";
        return;
    }

    // 再次加密
    // base64转码加密,目的将加密后的二进制转换为base64字符串
    login.insert("user",  QByteArray((char *)encUsr, encUsrLen).toBase64());
    login.insert("pwd", QByteArray((char *)encPwd, encPwdLen).toBase64() );
    if(isRemeber == true)
    {
         login.insert("remember", "yes");
    }
    else
    {
        login.insert("remember", "no");
    }

    // QVariant类作为一个最为普遍的Qt数据类型的联合
    // QVariant为一个万能的数据类型--可以作为许多类型互相之间进行自动转换。
    QMap<QString, QVariant> json;
    json.insert("web_server", web_server);
    json.insert("type_path", type_path);
    json.insert("login", login);


    QJsonDocument jsonDocument = QJsonDocument::fromVariant(json);
    if ( jsonDocument.isNull() == true)
    {
        cout << " QJsonDocument::fromVariant(json) err";
        return;
    }

    //打开配置文件
    QFile file(path);

    if( false == file.open(QIODevice::WriteOnly) )
    {
        cout << "file open err";
        return;
    }

    //json内容写入文件,覆盖原来的内容
    file.write(jsonDocument.toJson());
    file.close();
}

功能函数三——设置登陆信息json包, 密码经过md5加密, getStrMd5()

打包的json中,密码是加密的,这样的好处就是,即使发送post请求被拦截了,黑客也无法获知密码是什么。

md5是不可逆的。server端存储的也是md5,虽然密码找不回来了,但是可以重新设置密码,安全。

// 登陆用户需要使用的json数据包
QByteArray Login::setLoginJson(QString user, QString pwd)
{
    QMap<QString, QVariant> login;
    login.insert("user", user);
    login.insert("pwd", pwd);

    /*json数据如下
        {
            user:xxxx,
            pwd:xxx
        }
    */

    QJsonDocument jsonDocument = QJsonDocument::fromVariant(login);
    if ( jsonDocument.isNull() )
    {
        cout << " jsonDocument.isNull() ";
        return "";
    }

    //将json字符串返回
    return jsonDocument.toJson();
}

功能函数四——将某个字符串加密成md5码

// 将某个字符串加密成md5码
QString Common::getStrMd5(QString str)
{
    QByteArray array;
    //md5加密
    array = QCryptographicHash::hash ( str.toLocal8Bit(), QCryptographicHash::Md5 );

    //md5是16进制的,因此需要进行转化
    return array.toHex();
}

功能函数五——解析服务器返回的json

QStringList Login::getLoginStatus(QByteArray json)
{
    //判断是否出错
    QJsonParseError error;
    //QStringList是一个字符串数组容器,是可扩展的——QList<QString>
    QStringList list;

    /*
        登陆 - 服务器回写的json数据包格式:
            成功:{
                   "code": "000",
                   "token": "xxx"
                 }
            失败:{
                   "code": "001",
                    "token": "faild"
                }
    */
    
    // 将来源数据json转化为JsonDocument
    // 由QByteArray对象构造一个QJsonDocument对象,用于我们的读写操作
    QJsonDocument doc = QJsonDocument::fromJson(json, &error);
    if (error.error == QJsonParseError::NoError)
    {
        if (doc.isNull() || doc.isEmpty())
        {
            cout << "doc.isNull() || doc.isEmpty()";
            return list;
        }

        if( doc.isObject() )
        {
            //取得最外层这个大对象
            QJsonObject obj = doc.object();
            cout << "服务器返回的数据" << obj;
            //状态码
            list.append( obj.value( "code" ).toString() );
            //登陆token
            list.append( obj.value( "token" ).toString() );
        }
    }
    else
    {
        cout << "err = " << error.errorString();
    }

    return list;
}

功能函数六——单例模式,主要保存当前登陆用户,服务器信息

类的声明

//单例模式,主要保存当前登陆用户,服务器信息
class LoginInfoInstance
{
public:
     static LoginInfoInstance *getInstance(); //保证唯一一个实例
     static void destroy(); //释放堆区空间

     void setLoginInfo( QString tmpUser, QString tmpIp, QString tmpPort,  QString token="");//设置登陆信息
     QString getUser() const;   //获取登陆用户
     QString getIp() const;     //获取服务器ip
     QString getPort() const;   //获取服务器端口
     QString getToken() const;  //获取登陆token

private:
    //构造和析构函数为私有的
    LoginInfoInstance();
    ~LoginInfoInstance();
    //把复制构造函数和=操作符也设为私有,防止被复制
    LoginInfoInstance(const LoginInfoInstance&);
    LoginInfoInstance& operator=(const LoginInfoInstance&);

    //它的唯一工作就是在析构函数中删除Singleton的实例
    class Garbo
    {
    public:
        ~Garbo()
        {
            //释放堆区空间
            LoginInfoInstance::destroy();
        }
    };

    //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数
    //static类的析构函数在main()退出后调用
    //静态变量被析构的时候,调用类Garbo里面的destory函数
    static Garbo tmp; //静态数据成员,类中声明,类外定义


    //静态数据成员,类中声明,类外必须定义
    static LoginInfoInstance *instance;


    QString user;   //当前登陆用户
    QString token;  //登陆token
    QString ip;     //web服务器ip
    QString port;   //web服务器端口
};

类的定义

//static类的析构函数在main()退出后调用
//静态数据成员,类中声明,类外定义
LoginInfoInstance::Garbo LoginInfoInstance::tmp;

//静态变量动态分配空间
//静态数据成员,类中声明,类外必须定义
LoginInfoInstance* LoginInfoInstance::instance = new LoginInfoInstance;

LoginInfoInstance::LoginInfoInstance(){}

LoginInfoInstance::~LoginInfoInstance(){}

//把复制构造函数和=操作符也设为私有,防止被复制
LoginInfoInstance::LoginInfoInstance(const LoginInfoInstance& ){}

LoginInfoInstance& LoginInfoInstance::operator=(const LoginInfoInstance&)
{
    return *this;
}

//获取唯一的实例
LoginInfoInstance *LoginInfoInstance::getInstance()
{
    return instance;
}

//释放堆区空间
void LoginInfoInstance::destroy()
{
    if(NULL != LoginInfoInstance::instance)
    {
        delete LoginInfoInstance::instance;
        LoginInfoInstance::instance = NULL;
        cout << "instance is detele";
    }
}

//设置登陆信息
void LoginInfoInstance::setLoginInfo( QString tmpUser, QString tmpIp, QString tmpPort, QString token)
{
    user = tmpUser;
    ip = tmpIp;
    port = tmpPort;
    this->token = token;
}

//获取登陆用户
QString LoginInfoInstance::getUser() const
{
    return user;
}

//获取服务器ip
QString LoginInfoInstance::getIp() const
{
    return ip;
}

//获取服务器端口
QString LoginInfoInstance::getPort() const
{
    return port;
}

//获取登陆token
QString LoginInfoInstance::getToken() const
{
    return token;
}

token验证码

客户端连接服务器登陆成功之后,服务器会随机生成一个身份的标志。以后再和服务器发送消息的时候,需要把token返回给服务器,服务器会对token进行验证,看看用户是不是伪造的。

由于token需要频繁的验证,因此我们将其存入redis数据库中。

那么在redis中如何对token进行存储?

键——用户名+验证码(token)

值——数据库中存储的验证码(token)

server端生成token验证码

/* -------------------------------------------*/
/**
 * @brief  生成token字符串, 保存redis数据库
 *
 * @param user 		用户名
 * @param token     生成的token字符串
 *
 * @returns
 *      成功: 0
 *      失败:-1
 */
 /* -------------------------------------------*/
int set_token(char *user, char *token)
{
    int ret = 0;
    redisContext * redis_conn = NULL;

    //redis 服务器ip、端口
    char redis_ip[30] = {0};
    char redis_port[10] = {0};

    //读取redis配置信息
    get_cfg_value(CFG_PATH, "redis", "ip", redis_ip);
    get_cfg_value(CFG_PATH, "redis", "port", redis_port);
    LOG(LOGIN_LOG_MODULE, LOGIN_LOG_PROC, "redis:[ip=%s,port=%s]\n", redis_ip, redis_port);

    //连接redis数据库
    redis_conn = rop_connectdb_nopwd(redis_ip, redis_port);
    if (redis_conn == NULL)
    {
        LOG(LOGIN_LOG_MODULE, LOGIN_LOG_PROC, "redis connected error\n");
        ret = -1;
        goto END;
    }

    //产生4个1000以内的随机数
    int rand_num[4] = {0};
    int i = 0;

    //设置随机种子
    srand((unsigned int)time(NULL));
    for(i = 0; i < 4; ++i)
    {
        rand_num[i] = rand()%1000;//随机数
    }

    char tmp[1024] = {0};
	//将用户名和四个随机数拼成一个字符串
    sprintf(tmp, "%s%d%d%d%d", user, rand_num[0], rand_num[1], rand_num[2], rand_num[3]);
    LOG(LOGIN_LOG_MODULE, LOGIN_LOG_PROC, "tmp = %s\n", tmp);

    //加密
    char enc_tmp[1024*2] = {0};
    int enc_len = 0;
    ret = DesEnc((unsigned char *)tmp, strlen(tmp), (unsigned char *)enc_tmp, &enc_len);
    if(ret != 0)
    {
        LOG(LOGIN_LOG_MODULE, LOGIN_LOG_PROC, "DesEnc error\n");
        ret = -1;
        goto END;
    }

    // base64编码
    char base64[1024*3] = {0};
    base64_encode((const unsigned char *)enc_tmp, enc_len, base64); //base64编码
    LOG(LOGIN_LOG_MODULE, LOGIN_LOG_PROC, "base64 = %s\n", base64);

    //token变成md5——为了让每个token长度相同
    MD5_CTX md5;
    MD5Init(&md5);
    unsigned char decrypt[16];
    MD5Update(&md5, (unsigned char *)base64, strlen(base64) );
    MD5Final(&md5, decrypt);


    char str[100] = { 0 };
	//将串变成16进制,如此得到最终的md5串
    for (i = 0; i < 16; i++)
    {
        sprintf(str, "%02x", decrypt[i]);
        strcat(token, str);
    }

    // redis保存此字符串,用户名:token, 有效时间为24小时
    //设置key值的生命周期,24小时之后,该键值对被销毁
    //每次登陆都会生成一个新的token,所以token不删除也没有影响
    ret = rop_setex_string(redis_conn, user, 86400, token);
	//或者断开连接的时候调用函数将键值对删除
    //ret = rop_setex_string(redis_conn, user, 30, token); //30秒

END:
    if(redis_conn != NULL)
    {
        rop_disconnect(redis_conn);
    }

    return ret;

}

关于传输数据的安全问题(http和https)

客户端将用户名和md5编码之后的密码发送给服务器,假设在发送的过程中http请求被拦截,别人拿到你的数据去连接服务器是可以连接上的。

假设别人仅仅拿到了md5密码,则通过客户端无法登陆,因为客户端会对密码进行加密,即被md5加密过的密码再一次被md5加密。

http协议发送的数据是明文传输,不加密,不安全,可以换成https加密。

https在建立连接时,先建立ssl连接通道,再进行数据通信。

钓鱼网站

  • 一部分模拟url,进行跳转;
  • 一部分拦截你的数据,然后钓鱼网站去连接服务器

猜你喜欢

转载自blog.csdn.net/qq_29996285/article/details/87899745
今日推荐