用户登录事件
登录使用的协议格式
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"}
需要验证的操作
- 我的文件
- 秒传
- 分享文件
- 删除文件
用户登录实现代码
整体流程
客户端
- 获取用户输入的登录数据
- 数据校验——使用正则表达式
- 将数据打包为json格式数据
- 将数据以post格式发送给server
- 接受服务器返回的数据,解析判断登录是否成功
服务端
- FCGI_Accept() 阻塞等待用户连接 - fastCGI函数接口
- 读取用户登录数据 - post/get
- 用户登录——将用户信息从json包中解析出来、拿到mysql数据库的用户名,密码、连接mysql数据库来查询用户是否存在
- 给客户端发送响应数据
客户端用户登录代码
// 用户登录操作
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,进行跳转;
- 一部分拦截你的数据,然后钓鱼网站去连接服务器