TeamTalk源码之用户登录验证流程

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  	: 	更新时间

上一篇:mysql8 sql优化相关(持续更新...)


下一篇:大润发优鲜APP-协议参数paramsMD5分析