1. 用户登录验证流程图:
/*
登录流程:
client ---> msg_server ---> db_proxy_server route_server
【client】 ---> 【CMsgConn --> (msg_server) --> CDBServConn】 ---> 【CProxyConn ---> (db_proxy_server)】 |
| | | | |
| | | | |
IM::Login::IMLoginReq
pdu.CID = CID_LOGIN_REQ_USERLOGIN;
CMsgConn::HandlePdu()
case CID_LOGIN_REQ_USERLOGIN:
_HandleLoginRequest(pPdu);
IM::Server::IMValidateReq |
pdu.CID = CID_OTHER_VALIDATE_REQ; -----------------------> |
CProxyConn::HandlePdu()
DB_PROXY::doLogin();
| IM::Server::IMValidateRsp
| <--------------- pdu.CID = CID_OTHER_VALIDATE_RSP;
CDBServConn::HandlePdu()
case CID_OTHER_VALIDATE_RSP:
_HandleValidateResponse(pPdu);
if(UserValidateFail)
| IM::Login::IMLoginRes
| <------------------------------------------- pdu.CID = CID_LOGIN_RES_USERLOGIN;
if(UserValidateSucc)
IM::Server::IMServerKickUser |
pdu.CID = CID_OTHER_SERVER_KICK_USER ----------------------------------------> |
*/
2. 详细过程:
2.1 msg_server:(CMsgConn)
void CMsgConn::_HandleLoginRequest(CImPdu* pPdu) {
//step 1. 从所有的db_proxy_server列表中找出一个可用的连接:
CDBServConn* pDbConn = get_db_serv_conn_for_login();
if(!pDbConn) {
result = IM::BaseDefine::REFUSE_REASON_NO_DB_SERVER;
reslt_string = "服务器异常";
}
if(result) { //如果服务器异常,例如没有可用的 msg_server到proxy_server的连接,则直接给客户端返回异常
IM::Login::IMLoginRes msg;
SendPdu(&pdu);
Close();
return;
}
//step 2. 正常找到连接,对收到的msg反序列化,准备给proxy_server发登录请求消息:
IM::Login::IMLoginReq msg;
msg.ParseFromArray(pdu->GetBodyData(), pdu->GetBodyLength()); //反序列化
m_login_name = msg.user_name(); //CMsgConn对应一个登录的端,CMsgConn.m_login_name 保存登录的用户名。(一个 msg_server 上会同时维护无数个 CMsgConn)
string password = msg.password();
m_online_status = IM::BaseDefine::USER_STATUS_ONLINE; //连接状态为在线
//step 3. CImUser: 根据用户名去找CImUser实例
CImUser* pImUser = CImUserManager::getInstance()->GetImUserByLoginName(m_login_name);
if(!pImUser) { //如果没有找到,则是首个端登录,新创建一个实例:
pImUser = new CImUser(m_login_name);
CImUserManager::getInstance()->AddImUserByLoginName(m_login_name, pImUser); //将新建的CImUser实例加入到CImUserManager中:map<string(user_name), CImUser*> ImUserMapByName_t;
}
pImUser->AddUnValidateMsgConn(this); //加入到 m_unvalidate_conn_set 未鉴权set中
//step 4. 构造消息发给 db_proxy_server:
CDBAttachData attach_data(ATTACH_TYPE_HANDLE, m_handle, 0); //附加上connfd
IM::Server::IMValidateReq msg2;
msg2.set_user_name();
msg2.set_password();
msg2.set_attach_data();
CImPdu pdu;
pdu.SetPBMsg(&msg2);
pdu.SetServiceId(SID_OTHER);
pdu.SetCommandId(CID_OTHER_VALIDATE_REQ);
pdu.SetSeqNum();
pDbConn->SendPdu(&pdu);
}
//关键点: CImUserManager ---> CImUser ---> CMsgConn :
//msg_server上维护一个 CImUserManager, 用于维护此msg_server上登录的所有用户(CImUser),
//CImUserManager的实现也比较简单,数据成员是两个map,分别对应 user_id 和 user_name到 CImUser实例指针的索引,成员函数是向其中添加成员和删除成员
//每个 CImUser 对应一个登录用户,CMsgConn 对应一个端的登录,CImUser和CMsgConn是 1:n 的关系
//对应的包含关系如下:
// CImUserManager --> CImUser --> CMsgConn
class CImUserManager {
private:
ImUserMap_t m_im_user_map; //map<uint32_t(user_id), CImUser*>
ImUserMapByName_t m_im_user_map_by_name; //map<string(login_name), CImUser*>
static CImUserManager* s_manager;
};
class CImUser {
private:
uint32_t m_user_id;
string m_login_name;
uint32_t m_pc_login_status;
bool m_bValidate;
map<uint32_t, CMsgConn*> m_conn_map; //<connfd, CMsgConn*>
set<CMsgConn*> m_unvalidate_conn_set; //刚刚连接到msg_server上,还未经过proxy_server密码验证的CMsgConn。等验证通过后就会转入到上面的m_conn_map中。
//我认为这里用map代替set会更好,因为后面的是在for循环遍历整个set找到匹配的CMsgConn,不如直接connfd--->CMsgConn索引更快
};
class CMsgConn : public CImConn {
private:
string m_login_name; //用于标识本连接属于哪个user
uint32_t m_user_id;
uint32_t m_client_type; //端类型(mobile/pc)
uint32_t m_online_status;//连接状态(OFFLINE/ONLINE)
//属于 CImConn中的:
net_handle_t m_handle;
};
static CDBServConn* get_db_serv_conn_for_login(uint32_t start_pos, uint32_t stop_pos) {
uint32_t i = 0;
CDBServConn* pDbConn = NULL;
//determine if there is a valid DB server connection:
for(i = start_pos; i < stop_pos; i++) {
pDbConn = (CDBServConn*)g_db_server_list[i].serv_conn; //g_db_server_list 是从原始conf配置文件中界消息出来的所有db_server列表
if(pDbConn && pDbConn->IsOpen()) {
break;
}
}
if(i == stop_pos) {
return NULL;
}
while(true) {
int i = rand() % (stop_pos - start_pos) + start_pos;
pDbConn = (CDBServConn*)g_db_server_list[i].serv_conn;
if(pDbConn && pDbConn->IsOpen()) {
break;
}
}
return pDbConn;
}
2.2 db_proxy_server:(CProxyConn)
namespace DB_PROXY {
void doLogin(CImPdu* pPdu, uint32_t conn_uuid) { //conn_uuid是一个保持单调递增的uint32_t整型值,在CProxyConn新建时赋值 m_uuid
CImPdu *pPduResp = new CImPdu; //用于给msg_server回复消息的pdu
IM::Server::IMValidateRsp msgResp; //用于给msg_server回复消息的pb
//step 0. 反序列化msg:
IM::Server::IMValidateReq msg;
if(!msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength())) {
//如果反序列化失败,则直接回复错误:
msgResp.set_result_code(2);
msgResp.ser_result_string("服务器内部错误");
} else {
//正常情况,对收到的msg_server的pb消息反序列化成功后:
//step 1. 检验是否已在30分钟内连续密码错误10次,如是,则禁止登录,直接给msg_server回复异常响应并return返回:
if(!VerifyFailTimesIn30min()) {
return ;
}
//step 2. 进行登录验证:
IM::BaseDefine::UserInfo cUser; //如果检验成功,则将用户的各个信息存储在cUser中,返回给msg_server
int ret = g_loginStrategy.doLogin(msg.user_name(), msg.password(), cUser);
if(ret == fail) {
//如果验证失败,则记录一次:
lsErrorTime.push_front(time(NULL));
msgResp.set_result_code(1);
msgResp.ser_result_string("用户名/密码错误");
} else {
//登录验证成功:
IM::BaseDefine::UserInfo* pUser = msgResp.mutable_user_info(); //填充pdu中的pb内容
pUser->set_user_id(); pUser->set_user_name(); pUser->set_dapartment_id(); ......
msgResp->set_result_code(0);
msgResp->set_result_string("成功");
lsErrorTime.clear(); //已经登录成功了,就清空失败记录
}
}
//step 3. 无论成功失败,统一给msg_server回复结果:(注意这里只是加入到发送列表)
pPduResp->SetPBMsg(&msgResp);
pPduResp->SetSeqNum(pPdu->GetSeqNum());
pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER);
pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP);
CProxyConn::AddResponsePdu(conn_uuid, pPduResp);
}
};
//校验是否在30分钟内失败了10次,如是,则禁止登录,即使这次带的密码是正确的:
int VerifyFailTimesIn30min() {
uint32_t tmNow = time(NULL);
auto itTime = lsErrorTime.begin(); //list<uint32_t>& lsErrorTime = g_hmLimits[msg.user_name()]; //g_hmLimits = unordered_map<string user_name, list<uint32_t> error_time>; 用于记录该user_names上历次的密码失败时刻
for(; itTime != lsErrorTime.end(); ++itTime) {
if(tmNow - *itTime > 30*60) {
break;
}
}
if(itTime != lsErrorTime.end()) {
lsErrorTime.erase(itTime, lsErrorTime.end()); //lsErrorTime链表中存储的格式是 (60,50,40,30,20,10...) 新元素会push_front插入到链表头部,删除时则删除后面的老时间
}
if(lsErrorTime.size() > 30) {
msgResp.set_result_code(6);
msgResp.ser_result_string("用户名/密码错误次数过多");
pPduResp->SetPBMsg(msgResp);
pPduResp->SetSeqNum();
pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER);
pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP);
CProxyConn::AddResponsePdu(conn_uuid, pPduResp);
return 1; //fail
}
return 0; //succ
}
//查询MySQL数据库,进行用户名密码验证:
bool CInterLoginStrategy::doLogin(const std::string &strName, const std::string &strPass, IM::BaseDefine::UserInfo& user) {
CDBConn* pDbConn = CDBManager::getInstance()->GetDBConn("teamtalk_slave");
if(pDbConn) {
string strSql = "select * from IMUser where name = '" + strName + "' and status = 0";
CResultSet* pResultSet = pDbConn->ExcueteQuery(strSql.c_str);
if(pResultSet) {
while(pResultSet->Next()) {
}
if(strOutPass == strResult) {
user.set...... //user是引用&类型,密码验证通过后,把查询到的用户信息都拷贝出去
}
}
}
}
//ResponseList中的消息的发送:
//关键点在于 db_proxy_server 上会维护一个map,保存 uuid 到 CProxyConn 之间的索引,这样需要从ResponseList中统一发送消息时,就可以通过uuid知道发送给哪个msg_server(一个CProxyConn对应一个连接的msg_server)
//另外注意CProxyConn中的两个static静态成员的写法,所有CProxyConn实例都共享同一个 s_uuid_allocator 和 s_response_list
tpepdef unordered_map<uint32_t, CImConn*> UserMap_t; //map<uuid, CProxyConn*>
static UserMap_t g_uuid_conn_map;
class CProxyConn : public CImConn {
public:
CProxyConn();
static void AddResponsePdu(uint32_t conn_uuid, CImPdu* pPdu);
static void SendResponsePduList();
private:
static uint32_t s_uuid_allocator; //注意s_uuid_allocator是static静态对象,所有CProxyConn实例共享一个,保证单调递增
uint32_t m_uuid; //这个才是每个CProxyConn对应的专属 conn_uuid
static list<ResponsePdu_t*> s_response_pdu_list; //同样是static静态对象,所有CProxyConn的Response消息都会插入到这里list链表中,因此会产生互斥,需要对其加锁访问
static CLock s_list_clock;
};
CProxyConn::CProxyConn() {
m_uuid = ++CProxyConn::s_uuid_allocator;
if(m_uuid == 0) {
m_uuid = ++CProxyConn::s_uuid_allocator;
}
g_uuid_conn_map.insert( make_pair(m_uuid, this) ); //新建的CProxyConn及其对应的m_uuid 插入到map中
}
void CProxyConn::AddResponsePdu(uint32_t conn_uuid, CImPdu* pPdu) {
ResponsePdu_t* pPduResp = new ResponsePdu_t(conn_uuid, pPdu);
s_list_clock.lock();
s_response_pdu_list.push_back(pPduResp);
s_list_clock.unlock();
}
void CProxyConn::SendResponsePduList() {
s_list_clock.lock();
while(!s_response_pdu_list.empty()) {
ResponsePdu_t* pResp = s_response_list.font();
s_response_list.pop_font();
s_list_clock.unlock();
CProxyConn* pConn = get_proxy_conn_by_uuid(pResp->conn_uuid); //g_uuid_conn_map.find();
if(pConn) {
if(pResp->pPdu) {
pConn->SendPdu(pResp->pPdu);
}
}
s_response_pdu_list.lock();
}
s_list_clock.unlock();
}
2.3 msg_server:(CDBServConn)
void CDBServConn::_HandleValidateResponse(CImPdu* pPdu) {
//step 0. 对收到的返回结果消息反序列化:
IM::Server::IMValidateRsp msg;
msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength());
CImUser* pImUser = CImUserManager::GetInstance()->GetImUserByLoginName(msg.user_name());
if(pImUser) {
CMsgConn* pConn = pImUser->GetUnvalidateMsgConn(msg.attach_data().GetHandle()); //根据attach_data中保存的用户的connfd,从CIMUser::m_unvalidate_conn_set中找到未鉴权的CMsgConn
}
//step 1. 检查密码校验的结果是成功还是失败:
if(result != 0) {
//step 1.1 : 如果密码校验失败,则只需要返回结果给客户端,无需做其他处理
result = IM::BaseDefine::REFUSE_REASON_DB_VALIDATE_FAILED;
IM::Login::IMLoginRes msg4;
msg4.set_server_time();
msg4.set_result_code();
msg4.set_result_string();
CImPdu pdu3;
pdu3.SetPBMsg(&msg4);
pdu3.SetServiceId(SID_LOGIN);
pdu3.SetCommandId(CID_LOGIN_RES_USERLOGIN);
pdu3.SetSeqNum();
pMsgConn->SendPdu(pdu3);
pMsgConn->Close(); //注意发送会失败的校验结果后要close关闭连接,因为后面不会再有聊天报文上来
} else { //result = 0, 说明密码校验成功
//step 1.2 : 如果登录成功,则需要发送route_server去踢掉此用户的其他同类型的登录端:
CImUserManager::GetInstance()->AddImUserById(user_id, pUser); //维护user_id到CImUser的映射关系
//step 2. 在本msg_server上踢掉同类型登录端 + 通知route_server踢掉其他msg_server上的同类型登录端:
pUser->KickOutSameClientType(pMsgConn->GetClientType(), IM::BaseDefine::KICK_REASON_DUPLICATE_USER, pMsgConn);
CRouteServConn* pRouteConn = get_route_serv_conn();
if(pRouteConn) {
IM::Server::IMServerKickUser msg2;
msg2.set_user_id();
CImPdu pdu;
pdu.SetCommandId(CID_OTHER_SERVER_KICK_USER);
pRouteConn->SendPdu(&pdu);
}
//step 3. 通知所有 route_server 更新用户的登录状态:
pMsgConn->SendUserStatusUpdate(IM::BaseDefine::USER_STATUS_ONLINE);
pUser->ValidateMsgConn(); //将这个CMsgConn 从CImUser的未完成鉴权set集合中转移到 已完成鉴权map中
//step 4. 给客户端回复成功登录响应:
IM::Login::IMLoginRes msg3;
CImPdu pdu2;
pdu2.SetCommandId(CID_LOGIN_RES_USERLOGIN);
pMsgConn->SendPdu(&pdu2);
}
}
3. 登录过程涉及到的MySQL表:
3.1 IMUser :
功能:用户表
字段说明:
id : 用户ID,自增,唯一 (主键)
password : 密码,规则: ( md5(password) + salt )
salt : 密码混淆
sex : 性别
name : 用户名
domain : 拼音
nick : 昵称
phone : 电话号码
email : 邮箱
avatar : 头像
departId : 部门
status : 状态
create : 创建时间
update : 更新时间