Advertisement

How to check if the user is already logged in?

Started by January 26, 2018 06:40 PM
1 comment, last by hplus0603 6 years, 9 months ago

I need to implement a verification if the user is already logged in. At the moment my game doesn't verify this, so an account can log-in several times and I want to avoid this.

This is how I'm doing log-in authentication on game server:


void Session::authLogin(uint32_t id, std::string username, std::string token)
{
	// Return a valid alive connection, if doesn't exist, return nullptr
	auto dbcon = DatabaseManager::getFreeConnection();
	
	// Wait for a free connection
	if(!dbcon || dbcon->is_locked())
	{
		dbWait->expires_from_now(
		boost::posix_time::microseconds(
		DatabaseManager::DB_WAIT_TIME));
		
		dbWait->async_wait(boost::bind(
			&awaitAuthLogin, 
			boost::asio::placeholders::error, 
			weak_from_this(), 
			id, username, token));
		
		return;
	}
	
	// Lock connection
	DatabaseManager::Locker lock(dbcon);
	
	// Get Account Data
	auto pstmt = dbcon->prepare_statement(
			"SELECT `username`,`storage_gold`,`coins`,`ctl_code`,`vip_level`,"
			"unix_timestamp(`vip_expire_date`) FROM `account_data` WHERE `id`=? AND `username`=?");
	
	pstmt->push_uint(id);
	pstmt->push_string(username);
	
	uint32_t storage_gold = 0;
	uint32_t coins = 0;
	int32_t ctl_code = 0;
	uint32_t vip_level = 0;
	uint32_t vip_expire_date = 0;
		
	auto result = pstmt->execute_query();

	if(result->next())
	{	
		//Check token
		pstmt = dbcon->prepare_statement(
			"SELECT COUNT(*) FROM `token` WHERE `account_id`=? AND `token`=?");
		
		pstmt->push_uint(id);
		pstmt->push_string(token);
		
		auto token_result = pstmt->execute_query();
		
		if(!token_result->next())
		{
			// Invalid Token
			sendMessage(strdef::INVALID_TOKEN);
			closeSession();
			
			return;
		}
		
		storage_gold = result.get_int("storage_gold"),
		coins = result.get_int("coins"),
		ctl_code = result.get_int("ctl_code"),
		vip_level = result.get_int("vip_level");
		vip_expire_date = result.get_uint("unix_timestamp(`vip_expire_date`)");
	}
	else
	{
		// Invalid Login
		sendMessage(strdef::INVALID_USERNAME);
		closeSession();
		
		return;
	}

	// TODO: Check if account is logged in

	// *** I was thinking of doing an INSERT and 
	// if I get an ER_DUP_ENTRY error  it's because the account is logged in.
	
	try {
		// Insert account_stat
		pstmt = dbcon->prepare_statement(
			"INSERT INTO `account_stat` (`account_id`,`server_id`,`endpoint`,`mac`) VALUES(?,?,?,?)");
			
		pstmt->push_uint(id);
		pstmt->push_uint(getServerId());
		pstmt->push_string(getEndpoint());
		pstmt->push_string(getMacAddress());

		pstmt->execute();
	}
	catch(const mysql::exception& e) {
		// Already logged in
		if(e.error_code() == ER_DUP_ENTRY){
			sendMessage(strdef::ALREADY_LOGGED_IN);
			closeSession();
			
			return;
		}
	}
	
	// ***
	
	authLoginTimeout->cancel();
	
	// Make AccountInfo
	accountInfo = std::make_unique<AccountInfo>(id,
		username,
		token,
		storage_gold,
		coins,
		ctl_code,
		vip_level;
		vip_expire_date);
	
	// Load account storage
	loadStorage(id);
	
	// Send character list
	sendToCharacterLobby(id);
}

Can I have a problem doing this? Is there another way? I need it to be safe for multiple gameservers/dataservers.

If you have any tips to optimize it I accept :-)

Thanks in advance for helping me with this.

The way this is generally done, is that each time you log in, a login cookie is generated. (Doesn't matter if it's for HTTP, HTTPS, TCP, or UDP protocols) The cookie is either just a long, strong cryptographic random string (256 bits or more, encoded as base64 or whatever) or it's a signed hash of some data, such as userid,logintime,sha256(userid+logintime+secret)

You have some kind of database (which could be something memory-only, such as Redis or even memcached) which maps userID to cookie. You can additionally also map cookie to session state; this is convenient for web pages, but less necessary for games with persistent session connections.

When you generate the cookie, you update the mapping from user to cookie value. If your cookie doesn't allow you to recover the user ID, you also update the mapping from cookie value to session information (which should include login time and user id)

Now, each time the user makes a web request, include this cookie value. You can then verify both that 1) the cookie value is one you issued, AND 2) that mapping from the user ID to cookie value, you get this cookie, and not some other cookie. (If the cookie for the user ID is not the provided one, then this is an earlier login that should now be invalid.)

Or, if the user uses TCP, this value should be stored in the server that keeps the TCP connection alive, and every 10 seconds or so, it should re-verify with the cookie database that the cookie value for the user is still the same. If the cookie value changes, it means the user logged in somewhere else, and thus this connection should be terminated.

If you have a robust, fast, message bus between all servers you could also broadcast an event saying "this user logged in" and have all connections listen to those events, and kick off connections with the same user id. There may be some race conditions in that approach, depending on how you implement it. so I wouldn't rely solely on that mechanism, but it might be convenient if you want faster-than-10-seconds kicks.

You can use a SQL database instead of a memory database; these cookies don't need to live longer than a session, but you can use some script in the database to clean up old values.

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement