目录
一、交互逻辑
二、请求验证码
三、帐号注册
四、帐号/验证码登录
五、重置密码
本项目的交流QQ群:701889554
物联网实战--入门篇https://blog.****.net/ypp240124016/category_12609773.html
物联网实战--驱动篇https://blog.****.net/ypp240124016/category_12631333.html
本项目资源文件https://download.****.net/download/ypp240124016/89280540
一、交互逻辑
对于账户的注册、登录等流程的交互逻辑基本上是这样的:
1、用户端APP提交相关信息;
2、服务器接收解析信息,同时对信息的合法性进行认证;
3、根据命令类型执行对应的操作;
4、返回操作结果;
5、用户端APP显示结果并进入下一步。
两个工程都有跟账户相关的类文件,核心就是对这个流程中的不同指令进行处理,接下来根据指令详细看下处理流程。
二、请求验证码
先看下图箭头所指的三个关键信息,这是手机APP的代码,刚开始的时候会获取设备的mac地址和一个随机数,然后把他们作为订阅话题的组成部分,这样服务器就可以根据上报携带的身份信息进行针对性地回应了,这个订阅话题示例:yyy125/as/pub/account/E0:23:A3:62:63:E2/1629/#
这样基本上具备唯一性了。第三个箭头是账户相关的前后端交互接口,QML文件中可以直接用theAccountMan调用AccountMan类中的后端函数。
接下来进入用户端APP的AccountMan类中,核心是下图框框内的几个函数,分别是请求验证码、请求注册、请求账户登录、请求验证码登录和请求重置密码,可以看出,所有流程都是以APP端请求开始的。其它是对字符串进行有效性验证的函数,比如手机号、账户名和密码的格式进行校验,便于前端检测输入的合法性。
void AccountMan::requestVerCode(QString account, QString phone)
{
QJsonObject root_obj;
QJsonDocument json_doc;
root_obj.insert("cmd_type", "req_vercode");
root_obj.insert("account", account);
root_obj.insert("phone", phone);
root_obj.insert("rand_num", m_randNum);
root_obj.insert("mac", m_macStr);
json_doc.setObject(root_obj);
QByteArray msg_ba = json_doc.toJson(QJsonDocument::Indented);
QString topic=makePubTopic("code");
emit sigMqttPushMessage(topic, msg_ba);
}
QString AccountMan::makePubTopic(QString key_str)
{
QString topic=QString(TOPIC_HEAD)+"as/sub/account/"+key_str;
return topic;
}
以上是请求验证码的代码,参数账户名和手机号由前端输入,其中账户名可以为空,保留功能,核心是要手机号,然后用json的形式组合参数,命令类型是req_vercode,随机数和mac地址是主程序中传进来的,服务器就是根据这两个信息组合返回的发布话题的,这样才能收到返回结果。在这里还有个组合发布话题的函数,现在传入的是code关键字,组合后就是yyy125/as/sub/account/code,字面上就能理解了这个话题的数据是发送给应用服务器的,内容是账户相关的,子功能为验证码,至于具体要干嘛那就交给数据包内的cmd_type字段去处理了,对于验证码功能主要就是 “请求验证码” 命令了。
数据发送到服务器后,主程序先根据话题筛选出账户类话题,然后把该类型话题的数据转发到账户线程中去进一步处理,代码如下:
void MainInterface::slotMqttReceived(const QMQTT::Message &message)
{
QJsonParseError json_error;
QJsonDocument json_doc;
QString recv_topic=message.topic();
qDebug()<<"msg topic= "<<message.topic();
qDebug()<<message.payload().data();
json_doc = QJsonDocument::fromJson(message.payload(), &json_error);//转为JSON格式
if(json_error.error != QJsonParseError::NoError)
{
// qDebug()<<"json error= "<<json_error.error;
return;
}
QJsonObject rootObj = json_doc.object();
if(recv_topic.contains("account/"))//账户类相关的话题
{
emit sigAccountThreadMessage(recv_topic, rootObj);
}
}
进入账户线程后代码如下,根据话题的子功能进行分类处理,对于验证码类型,我们刚才组合话题时候添加的关键字是code,那么话题内有/code的就进行具体的验证码数据解析,具体函数是parseVerCodeTopic(),在函数内,我们获取了命令类型、手机号、mac地址和随机数等数据,命令类型目前只有请求验证码,对于此命令的处理步骤是:
1、检查手机号;
2、获取随机验证码;
3、缓存验证码;
4、发送验证码;
5、回复用户端APP
其中,步骤3在缓存时会检查该手机号之前是否有验证码存在,如果有的话就缓存失败,同时也会回复用户端失败的信息;验证码的添加成功后会在一个列表内暂存30秒,超时后自动删除;步骤4中的发送验证码,理论上需要SMS服务器的,这个是需要企业才能办理的业务,我们当前先打印出来就好,实际的发送后面再专门出一篇。
void AccountThread::slotAccountThreadMessage(QString topic, QJsonObject root_obj)
{
if(topic.contains("/reg"))//注册相关
{
parseRegTopic(root_obj);
}
else if(topic.contains("/login"))//登录相关
{
parseLoginTopic(root_obj);
}
else if(topic.contains("/code"))//验证码相关
{
parseVerCodeTopic(root_obj);
}
else if(topic.contains("/passwd"))//密码相关
{
parsePasswdTopic(root_obj);
}
else if(topic.contains("/child"))//子账户相关
{
}
else if(topic.contains("/app"))//应用相关
{
}
else if(topic.contains("/group"))//分组相关
{
}
else if(topic.contains("/device"))//设备相关
{
}
}
//解析验证码话题
void AccountThread::parseVerCodeTopic(QJsonObject root_obj)
{
QString cmd_type="";
if(root_obj.contains("cmd_type"))//命令类型
{
QJsonValue value = root_obj.value("cmd_type");
if(value.isString())
{
cmd_type=value.toString();
}
}
QString account="";
if(root_obj.contains("account"))//账户
{
QJsonValue value = root_obj.value("account");
if(value.isString())
{
account=value.toString();
}
}
int rand_num=0;
if(root_obj.contains("rand_num"))//随机数
{
QJsonValue value = root_obj.value("rand_num");
if(value.isDouble())
{
rand_num=(int)value.toDouble();
}
}
QString mac_str="";
if(root_obj.contains("mac"))//mac
{
QJsonValue value = root_obj.value("mac");
if(value.isString())
{
mac_str=value.toString();
}
}
QString phone="";
if(root_obj.contains("phone"))//手机号码
{
QJsonValue value = root_obj.value("phone");
if(value.isString())
{
phone=value.toString();
}
}
if(cmd_type=="req_vercode")//请求验证码
{
if(phone.isEmpty())
{
qDebug()<<"phone.isEmpty()";
return;
}
QString ver_code_str=takeVerCode();
bool ok=addReqVerCodeNode(account, phone, ver_code_str);//添加进列表,进行超时检测
if(ok)
{
//向SMS服务器发送验证码
// sendSmsCheckCode(phone, ver_code_str);
ackReqVerCodeState(account, mac_str, rand_num, phone, 0, "验证码已发送!");//返回验证码已发送状态
qDebug()<<"req_vercode ok";
}
else
{
ackReqVerCodeState(account, mac_str, rand_num, phone, 1, "重复发送!");//
}
}
}
回复函数根据不同的命令类型参数略有区别,下面是请求验证码的函数,注意点就是回复的话题,需要根据上传的mac、随机数和关键字段组合,与开头就形成闭环了。
void AccountThread::ackReqVerCodeState(QString account, QString mac_str, int rand_num, QString phone, int result, QString ack_str)
{
QJsonObject root_obj;
QJsonDocument json_doc;
root_obj.insert("cmd_type", "req_vercode");
root_obj.insert("result", result);
root_obj.insert("phone", phone);
root_obj.insert("ack_str", ack_str);
json_doc.setObject(root_obj);
QByteArray msg_ba = json_doc.toJson(QJsonDocument::Indented);
QString topic=makePubTopic(account, mac_str, rand_num, "code");
emit sigMqttPushMessage(topic, msg_ba);
}
//生成发布话题
QString AccountThread::makePubTopic(QString account, QString mac_str, int rand_num, QString key_str)
{
if(account.isEmpty())
{
}
QString topic=QString(TOPIC_HEAD) + SERVER_PUB_TOPIC+QString("/account/")+mac_str+QString::asprintf("/%d/", rand_num)+key_str;
return topic;
}
回到用户端这边,主程序也是根据话题类型归类处理的,目前只进行简单的信息提示处理,就是将服务器的回复信息发送到QML前端进行显示交互。
总的来讲,整个流程就是这样了,其他注册、登录等功能也是类似的。
三、帐号注册
1、用户端发送注册信息,包含账户、密码、手机、验证码等:
void AccountMan::requestReg(QString account, QString passwd, QString phone, QString ver_code)
{
QJsonObject root_obj;
QJsonDocument json_doc;
root_obj.insert("cmd_type", "req_reg");
root_obj.insert("account", account);
root_obj.insert("pass_word", passwd);
root_obj.insert("phone", phone);
root_obj.insert("rand_num", m_randNum);
root_obj.insert("mac", m_macStr);
root_obj.insert("ver_code", ver_code);
json_doc.setObject(root_obj);
QByteArray msg_ba = json_doc.toJson(QJsonDocument::Indented);
QString topic=makePubTopic("reg");
emit sigMqttPushMessage(topic, msg_ba);
}
2、服务器接收解析,在这里要对账户和手机号的重复性进行检查,注册成功后会默认新建一个app_id。
//解析注册话题
void AccountThread::parseRegTopic(QJsonObject root_obj)
{
QString cmd_type="";
if(root_obj.contains("cmd_type"))//命令类型
{
QJsonValue value = root_obj.value("cmd_type");
if(value.isString())
{
cmd_type=value.toString();
}
}
QString account="";
if(root_obj.contains("account"))//账户
{
QJsonValue value = root_obj.value("account");
if(value.isString())
{
account=value.toString();
}
}
int rand_num=0;
if(root_obj.contains("rand_num"))//随机数
{
QJsonValue value = root_obj.value("rand_num");
if(value.isDouble())
{
rand_num=(int)value.toDouble();
}
}
QString mac_str="";
if(root_obj.contains("mac"))//mac
{
QJsonValue value = root_obj.value("mac");
if(value.isString())
{
mac_str=value.toString();
}
}
QString pass_word="";
if(root_obj.contains("pass_word"))//密码
{
QJsonValue value = root_obj.value("pass_word");
if(value.isString())
{
pass_word=value.toString();
}
}
QString phone="";
if(root_obj.contains("phone"))//手机号码
{
QJsonValue value = root_obj.value("phone");
if(value.isString())
{
phone=value.toString();
}
}
QString ver_code="";
if(root_obj.contains("ver_code"))//验证码
{
QJsonValue value = root_obj.value("ver_code");
if(value.isString())
{
ver_code=value.toString();
}
}
if(cmd_type=="req_reg")//请求注册
{
bool ok;
account.toDouble(&ok);
if(ok)
{
ackReqRegState(account, mac_str, rand_num, phone, 6, "账户不能为纯数字!");
return;
}
AccountSqlite::AccountNodeStruct tag_account;
bool ok1, ok2;
ok1=m_accountSqlite->searchAccountByName(account, tag_account);
ok2=m_accountSqlite->searchAccountByPhone(phone, tag_account);
if(ok1==false && ok2==false)//未找到重复的,可以注册
{
for(auto iter : m_reqVerCodeList)
{
if(iter.phone == phone)//根据手机号检索
{
if(iter.verCode == ver_code)//验证码相等
{
bool ok=m_accountSqlite->addAccountNode(account, pass_word, 0x00, "", phone);
if(ok)
{
ackReqRegState(account, mac_str, rand_num, phone, 0, "注册成功!");
qDebug()<<"reg ok!";
//自动创建一个应用
u32 max_app=m_accountSqlite->selectMaxAppID();
u32 new_app_id=0;
if(max_app>APP_ID_MIN)
new_app_id=max_app+1;
else
new_app_id=APP_ID_MIN+1;
qDebug()<<"new_app_id="<<new_app_id;
m_accountSqlite->addAppIDToList(new_app_id, account);
}
else
{
ackReqRegState(account, mac_str, rand_num, phone, 1, "数据库存储出错!");
qDebug()<<"reg sql error!!";
}
}
else
{
ackReqRegState(account, mac_str, rand_num, phone, 2, "验证码错误!");
qDebug()<<"reg ver_code error!";
}
return;
}
}
ackReqRegState(account, mac_str, rand_num, phone, 3, "验证码超时!");
qDebug()<<"no found code!";
}
else
{
if(ok1==true)
{
ackReqRegState(account, mac_str, rand_num, phone, 4, "账户名已存在!");
qDebug()<<"have same account="<<account;
}
else if(ok2==true)
{
ackReqRegState(account, mac_str, rand_num, phone, 5, "手机号已存在!");
qDebug()<<"have same phone="<<phone;
}
}
}
}
四、帐号/验证码登录
1、用户发送帐号登录信息,主要包括账户和密码:
void AccountMan::requestLogin(QString account, QString pass_word, int remember)
{
if(remember){}
QJsonObject root_obj;
QJsonDocument json_doc;
root_obj.insert("cmd_type", "login_pwd");
root_obj.insert("account", account);
root_obj.insert("pass_word", pass_word);
root_obj.insert("rand_num", m_randNum);
root_obj.insert("mac", m_macStr);
json_doc.setObject(root_obj);
QByteArray msg_ba = json_doc.toJson(QJsonDocument::Indented);
QString topic=makePubTopic("login");
emit sigMqttPushMessage(topic, msg_ba);
}
如果是验证码登录,那就是手机号和验证码
void AccountMan::requestLoginByVerCode(QString phone, QString ver_code)
{
QJsonObject root_obj;
QJsonDocument json_doc;
root_obj.insert("cmd_type", "login_code");
root_obj.insert("phone", phone);
root_obj.insert("ver_code", ver_code);
root_obj.insert("rand_num", m_randNum);
root_obj.insert("mac", m_macStr);
json_doc.setObject(root_obj);
QByteArray msg_ba = json_doc.toJson(QJsonDocument::Indented);
QString topic=makePubTopic("login");
emit sigMqttPushMessage(topic, msg_ba);
}
2、服务端接收解析,登录操作要仔细校验账户名和密码,账户也可以填手机号,服务端会自己判断;验证码登录的时候,如果该手机号未注册则会自动注册,用户名默认是Y+手机号,密码随机,需要自己去重置。
//解析登录话题
void AccountThread::parseLoginTopic(QJsonObject root_obj)
{
QString cmd_type="";
if(root_obj.contains("cmd_type"))//命令类型
{
QJsonValue value = root_obj.value("cmd_type");
if(value.isString())
{
cmd_type=value.toString();
}
}
QString account="";
if(root_obj.contains("account"))//账户
{
QJsonValue value = root_obj.value("account");
if(value.isString())
{
account=value.toString();
}
}
int rand_num=0;
if(root_obj.contains("rand_num"))//随机数
{
QJsonValue value = root_obj.value("rand_num");
if(value.isDouble())
{
rand_num=(int)value.toDouble();
}
}
QString mac_str="";
if(root_obj.contains("mac"))//mac
{
QJsonValue value = root_obj.value("mac");
if(value.isString())
{
mac_str=value.toString();
}
}
QString pass_word="";
if(root_obj.contains("pass_word"))//密码
{
QJsonValue value = root_obj.value("pass_word");
if(value.isString())
{
pass_word=value.toString();
}
}
QString phone="";
if(root_obj.contains("phone"))//手机号码
{
QJsonValue value = root_obj.value("phone");
if(value.isString())
{
phone=value.toString();
}
}
QString ver_code="";
if(root_obj.contains("ver_code"))//验证码
{
QJsonValue value = root_obj.value("ver_code");
if(value.isString())
{
ver_code=value.toString();
}
}
AccountSqlite::AccountNodeStruct tag_account;
if(cmd_type=="login_pwd")//密码登录
{
if(!account.isEmpty())
{
bool ok=account.toDouble();
if(ok)//手机号登录
{
QString phone=account;
if( m_accountSqlite->selectAccountByPhone(phone, tag_account)==false)
{
ackLoginState("", phone, "", mac_str, rand_num, 0, "", 1, "手机号未注册!");
return;
}
}
else
{
if( m_accountSqlite->selectAccountByName(account, tag_account)==false)
{
ackLoginState(account, "", "", mac_str, rand_num, 0, "", 2, "帐号未注册!");
return;
}
}
if((account==tag_account.account || account==tag_account.phone) && pass_word==tag_account.passWord)//再次校验用户名跟密码
{
ackLoginState(tag_account.account,tag_account.phone, tag_account.parentAccount, mac_str, rand_num, tag_account.auth, tag_account.createTime, 0, "登录成功!");
}
else
{
ackLoginState(tag_account.account,tag_account.phone, tag_account.parentAccount, mac_str, rand_num, tag_account.auth, tag_account.createTime, 3, "密码错误!");
}
}
else
{
ackLoginState(account, "", "", mac_str, rand_num, 0, "", 4, "账户不存在!");
}
}
else if(cmd_type=="login_code")//验证码登录
{
bool ok;
phone.toDouble(&ok);
if(phone.size()!=11 || !ok)
{
qDebug()<<"login phone="<<phone<<" error!";
ackReqRegState(phone, mac_str, rand_num, phone, 1, "手机号有误!");
return;
}
bool flag=false;
for(auto iter : m_reqVerCodeList)
{
if(iter.phone == phone)
{
if(iter.verCode == ver_code)
{
flag=true;
}
break;
}
}
if(flag==false)
{
ackReqRegState(phone, mac_str, rand_num, phone, 1, "验证码错误!");
return;
}
tag_account.phone.clear();
m_accountSqlite->searchAccountByPhone(phone, tag_account);
if(phone==tag_account.phone)
{
ackReqRegState(phone, mac_str, rand_num, phone, 0, "登录成功!");
}
else//新手机号,直接注册
{
account="Y"+phone;
pass_word.clear();
for(int i=0; i<8; i++)
{
pass_word+=QString::asprintf("%d", drv_com.takeRandNumber()%10);//随机密码
}
qDebug()<<account<<" pass_word="<<pass_word;
bool ok=m_accountSqlite->addAccountNode(account, pass_word, 0x00, "", phone);
if(ok)
{
ackReqRegState(phone, mac_str, rand_num, phone, 0, "登录(注册)成功!");
qDebug()<<"reg ok!";
//自动创建一个应用
u32 max_app=m_accountSqlite->selectMaxAppID();
u32 new_app_id=0;
if(max_app>APP_ID_MIN)
new_app_id=max_app+1;
else
new_app_id=APP_ID_MIN+1;
qDebug()<<"phone new_app_id="<<new_app_id;
m_accountSqlite->addAppIDToList(new_app_id, account);
}
else
{
ackReqRegState(phone, mac_str, rand_num, phone, 1, "数据库存储错误!");
qDebug()<<"reg sql error!!";
}
}
}
}
五、重置密码
1、用户发送重置信息主要包括用户名、新密码、手机号和校验码:
void AccountMan::requestResetPasswd(QString account, QString pass_word, QString phone, QString ver_code)
{
QJsonObject root_obj;
QJsonDocument json_doc;
root_obj.insert("cmd_type", "reset_pwd");
root_obj.insert("account", account);
root_obj.insert("pass_word", pass_word);
root_obj.insert("phone", phone);
root_obj.insert("rand_num", m_randNum);
root_obj.insert("mac", m_macStr);
root_obj.insert("ver_code", ver_code);
json_doc.setObject(root_obj);
QByteArray msg_ba = json_doc.toJson(QJsonDocument::Indented);
QString topic=makePubTopic("passwd");
emit sigMqttPushMessage(topic, msg_ba);
}
2、服务端接收解析,首先要检查与数据库里的账户和手机是否匹配,然后校验验证码,通过后就可以更新密码了,为了确保写入成功,需要再读取出来进行对比,新密码校验成功后才算真正完成了密码重置。
void AccountThread::parsePasswdTopic(QJsonObject root_obj)
{
QString cmd_type="";
if(root_obj.contains("cmd_type"))//命令类型
{
QJsonValue value = root_obj.value("cmd_type");
if(value.isString())
{
cmd_type=value.toString();
}
}
QString account="";
if(root_obj.contains("account"))//账户
{
QJsonValue value = root_obj.value("account");
if(value.isString())
{
account=value.toString();
}
}
QString mac_str="";
if(root_obj.contains("mac"))//mac
{
QJsonValue value = root_obj.value("mac");
if(value.isString())
{
mac_str=value.toString();
}
}
int rand_num=0;
if(root_obj.contains("rand_num"))//rand_num
{
QJsonValue value = root_obj.value("rand_num");
if(value.isDouble())
{
rand_num=(int)value.toDouble();
}
}
QString pass_word="";
if(root_obj.contains("pass_word"))//密码
{
QJsonValue value = root_obj.value("pass_word");
if(value.isString())
{
pass_word=value.toString();
}
}
QString phone="";
if(root_obj.contains("phone"))//手机号码
{
QJsonValue value = root_obj.value("phone");
if(value.isString())
{
phone=value.toString();
}
}
QString ver_code="";
if(root_obj.contains("ver_code"))//验证码
{
QJsonValue value = root_obj.value("ver_code");
if(value.isString())
{
ver_code=value.toString();
}
}
if(cmd_type=="reset_pwd")
{
if(account.isEmpty() || phone.isEmpty())
{
qDebug()<<"account.isEmpty() || phone.isEmpty()";
return;
}
AccountSqlite::AccountNodeStruct tag_account;
m_accountSqlite->selectAccountByPhone(phone, tag_account);
if(tag_account.account!=account)
{
ackResetPasswdState(account, mac_str, rand_num, 4, "账户与手机不匹配!");
return;
}
for(auto iter : m_reqVerCodeList)
{
if(iter.phone == phone)
{
if(iter.verCode == ver_code)
{
tag_account.passWord.clear();
m_accountSqlite->updateAccountPassWord(account, pass_word);//更新密码
m_accountSqlite->selectAccountByName(account, tag_account);//重新获取密码
if(tag_account.passWord==pass_word)//新密码校验
{
ackResetPasswdState(account, mac_str, rand_num, 0, "密码修改成功!");
qDebug("set new pwd ok!");
}
else
{
ackResetPasswdState(account, mac_str, rand_num, 1, "密码修改(校验)失败!");
qDebug("set new pwd failed!");
}
}
else
{
ackResetPasswdState(account, mac_str, rand_num, 2, "验证码错误!");
qDebug()<<"reg ver_code error!";
}
return;
}
}
ackResetPasswdState(account, mac_str, rand_num, 3, "验证码已过期!");
}
}
服务端经常有数据库操作步骤,这里再看下数据库的创建和打开,因为账户管理任务是独立的线程,所以我这里数据库定义为指针类型,然后在槽函数里new一个 AccountSqlite(),这样数据库操作才属于线程内部,这点很重要,这是QT的特性;然后就是打开数据库,参数就是文件名(包括路径)+连接名称,如果文件不存在就会自动创建,连接名称随意,这里一般不会重复;最后就是创建账户表和应用表了,之前数据库语句介绍过了,这样操作并不会重复创建库表。