前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++简单实现一个令牌(Token)验证登录基于Windows平台下的CS交互

C++简单实现一个令牌(Token)验证登录基于Windows平台下的CS交互

原创
作者头像
晨星成焰
发布2024-08-06 01:08:14
1510
发布2024-08-06 01:08:14
举报
文章被收录于专栏:网络编程C++入门基础知识

Token值的存储

在实现Token令牌登录前,首先需要思考Token的存储形式

基于用户ID唯一,以及一个Token 值对应一个用户ID和用户姓名的情况下

决定在哪里存储 token 值取决于多种因素,包括安全性、可扩展性、持久性和可用性等

1. 存储在数据库中

  • 优点:
    • 安全性: 数据库通常提供了更好的安全性措施,如加密、备份和恢复机制等。
    • 持久性: 数据库中的数据可以在服务器重启后依然存在。
    • 高可用性: 可以使用多节点数据库集群实现高可用性。
    • 事务支持: 支持事务处理,这对于需要保证数据一致性的情况非常重要。
  • 缺点:
    • 性能: 对于高并发的应用来说,每次访问都需要查询数据库可能会成为瓶颈。
    • 复杂性: 需要维护数据库的索引、分区等,增加了系统的复杂性。
    • 成本: 高性能的数据库服务器可能需要较高的硬件和维护成本。

2. 存储在服务端的一个数据结构中

  • 优点:
    • 性能: 数据存储在内存中,读写速度非常快。
    • 简单: 实现起来相对简单,无需复杂的数据库配置。
    • 成本: 无需额外的数据库服务器,减少了成本。
  • 缺点:
    • 持久性: 服务端重启后,数据会丢失。
    • 安全性: 相比于数据库,内存中的数据更容易受到攻击。
    • 可扩展性: 当服务端负载增加时,单个服务端的内存可能不足以支撑更多的用户会话。
    • 故障恢复: 一旦服务端出现故障,所有的 token 信息都会丢失。

还有一种结合使用的最推荐方案

3. 使用数据库作为主存储: 存储所有 token 和相关的用户信息。

使用缓存技术: 如 Redis 或 Memcached 等缓存系统,用来缓存最近使用的 token 信息。这样可以显著减少数据库的访问次数,提高性能。

这种方法结合了数据库的安全性和持久性以及缓存的高性能。当客户端发送请求时,首先从缓存中查询 token 信息,如果缓存中存在则冷加载数据,如果缓存中不存在,则从数据库中查询并将结果缓存起来。当 token 过期或者被注销时,从缓存和数据库中删除相应的记录。


Token验证登录实现

综上所述由于只是一个简单的令牌Token登陆验证模拟实现,忽略Token加密和Token验证等环节,并且采用第二种方法

实现一个Token管理器类

由于简单实现的原因只需要一个管理器类,基于单例模式在服务端全局使用

单例模式的讲解可以参考这篇文章

C++设计模式-单例模式讲解

Token管理器类出于用户ID的唯一性采用了双向映射:

一个是从令牌到用户信息(已经存在),另一个是从用户ID到令牌。这样,当需要更新用户的令牌时,可以直接通过用户ID找到旧的令牌并删除它,而无需遍历整个 _tokenMap

综上所述实现基本的增删改查

TokenMgr.hpp

代码语言:cpp
复制
#pragma once
#include <map>
#include <string>
#include <iostream>
#include <random>
#include <chrono>

class TokenMgr
{
private:
	struct TokenUserInfo
	{
		int userId;
		std::string userName;
	};

	std::map<std::string, TokenUserInfo> _tokenMap; // 从令牌到用户信息的映射
	std::map<int, std::string> _userIdToToken; // 从用户ID到令牌的映射
	int _tokenLength = 8; // 令牌长度

private:
	TokenMgr()
	{
		_tokenLength = 8;
	}
	TokenMgr(const TokenMgr&) = delete;
	TokenMgr& operator=(const TokenMgr&) = delete;

	// 生成指定长度的随机令牌
	std::string GenerateRandomToken(int length)
	{
		static const char alphanum[] =
			"0123456789"
			"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
			"abcdefghijklmnopqrstuvwxyz";

		static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());
		static std::uniform_int_distribution<int> distribution(0, sizeof(alphanum) - 2);

		std::string token;
		for (int i = 0; i < length; ++i)
		{
			token += alphanum[distribution(generator)];
		}
		return token;
	}

public:
	// 全局访问点
	static TokenMgr& GetInstance()
	{
		static TokenMgr instance;
		return instance;
	}
	// 添加一个新令牌,并关联到用户ID和用户名
	void AddToken(int userId, const std::string& userName)
	{
		std::string token = GenerateRandomToken(this->_tokenLength); // 生成8个字符的令牌
		_tokenMap[token] = TokenUserInfo{ userId, userName };
		_userIdToToken[userId] = token;
	}

	std::string GetTokenByUserId(int userId)
	{
		auto it = _userIdToToken.find(userId);
		if (it != _userIdToToken.end())
		{
			return it->second;
		}
		return ""; // 如果未找到,则返回空字符串
	}

	// 根据令牌获取用户信息
	bool GetUserInfo(const std::string& token, int& userId, std::string& userName)
	{
		auto it = _tokenMap.find(token);
		if (it != _tokenMap.end())
		{
			userId = it->second.userId;
			userName = it->second.userName;
			return true;
		}
		return false;
	}

	void ByTokenUpdateIdName(const std::string& token, int userId, const std::string& userName)
	{
		auto it = _tokenMap.find(token);
		if (it != _tokenMap.end())
		{
			it->second.userId = userId;
			it->second.userName = userName;
			_userIdToToken[userId] = token;
		}
	}

	void ByIdUpdateNewToken(int userId)
	{
		// 生成一个新的唯一令牌。
		std::string newToken;
		do
		{
			newToken = GenerateRandomToken(8); // 生成8个字符的令牌
		} while (_tokenMap.find(newToken) != _tokenMap.end()); // 确保令牌是唯一的。

		// 直接通过用户ID找到旧的令牌并删除它
		auto it = _userIdToToken.find(userId);
		if (it != _userIdToToken.end())
		{
            //因为map键具有唯一性,必须删除再添加,
			_tokenMap.erase(it->second); // 从_tokenMap中删除旧的令牌
			_userIdToToken.erase(it); // 从_userIdToToken中删除旧的映射
		}

		// 插入带有给定用户ID的新令牌。
		TokenUserInfo info;
		info.userId = userId;
		info.userName = GetUserNameByUserId(userId);
		_tokenMap[newToken] = info;
		_userIdToToken[userId] = newToken;
	}

	// 检查令牌是否存在
	bool HasToken(const std::string& token)
	{
		return _tokenMap.find(token) != _tokenMap.end();
	}

	// 移除令牌
	void RemoveToken(const std::string& token)
	{
		int userId;
		std::string userName;
		if (GetUserInfo(token, userId, userName))
		{
			_tokenMap.erase(token);
			_userIdToToken.erase(userId);
		}
	}

private:
	std::string GetUserNameByUserId(int userId)
	{
		auto it = _userIdToToken.find(userId);
		if (it != _userIdToToken.end())
		{
			auto tokenIt = _tokenMap.find(it->second);
			if (tokenIt != _tokenMap.end())
			{
				return tokenIt->second.userName;
			}
		}
		return ""; // 如果未找到,则返回空字符串
	}
};

如果有多线程需求,在多个线程同时访问 _tokenMap时,例如,在生成新令牌时,如果有多个线程同时执行,则可能会生成相同的令牌,这时就需要上锁了。

CS交互演示

略去服务端客户端的搭建,这里仅以交互逻辑为例

一个Token登录流程

客户端进行登陆请求,并发送相应的用户名和密码

服务端验证登陆无误后,生成一个 Token 并将用户信息存储在服务端(如 Redis,数据库,临时数据结构)以便快速验证和获取用户信息,旧的Token进行删除,服务端向客户端返回Token。

客户端将 Token 存储在本地(存储可加密)

在每个后续的登陆或非登录的请求中,客户端可以通过请求头(内含Token)发送给服务端。

服务端从请求头中获取 Token,解密验证其合法性后,完成访问受保护的功能


略去加密验证等繁琐步骤后,遵循客户端的一切行动逻辑都尽量基于服务端的情况下。

CS简单实现一个Token登陆交互

服务端的Login信息处理

登陆回复

代码语言:cpp
复制
void TcpSocket::SC_LoginRespond(cJSON* root)
{
	cJSON* username = cJSON_GetObjectItem(root, "username");
	cJSON* password = cJSON_GetObjectItem(root, "password");
	if (username == nullptr || username->type != cJSON_String && password == nullptr || password->type != cJSON_String)
	{
		Close();
		return;
	}

	cJSON* SC_Login = cJSON_CreateObject();

	std::string strUsername = username->valuestring;
	std::string strUserToken;
	std::string strPassword = password->valuestring;

	bool loginFlag = DBOperate::GetInstance().DBLogin(strUsername.c_str(), strPassword.c_str());

	if (loginFlag)
	{
		// 用户名密码正确,登录成功
		
		//避免旧的Token值存在
		std::string findToken = TokenMgr::GetInstance().GetTokenByUserId(findUID);
		if (findToken != "" && TokenMgr::GetInstance().HasToken(findToken))TokenMgr::GetInstance().RemoveToken(findToken);

		TokenMgr::GetInstance().AddToken(findUID, strUsername);
		strUserToken = TokenMgr::GetInstance().GetTokenByUserId(findUID);

		cJSON_AddStringToObject(SC_Login, "cmd", "SC_Login");
		cJSON_AddStringToObject(SC_Login, "token", strUserToken.c_str());
		cJSON_AddNumberToObject(SC_Login, "result", 1);
		cJSON_AddStringToObject(SC_Login, "msg", "Login Success");
	}
	else
	{
		// 用户名或者密码错误
		cJSON_AddStringToObject(SC_Login, "cmd", "SC_Login");
		cJSON_AddNumberToObject(SC_Login, "result", 0);
		cJSON_AddStringToObject(SC_Login, "msg", "User Or Password Failed");
	}


	std::string msg = cJSON_Print(SC_Login);
	cJSON_Delete(SC_Login);

	SendData(msg.c_str(), msg.size());
}

Token登陆回复

代码语言:cpp
复制
void TcpSocket::SC_TokenLoginRespond(cJSON* root)
{
	cJSON* Token = cJSON_GetObjectItem(root, "token");
	if (Token == nullptr || Token->type != cJSON_String)
	{
		Close();
		return;
	}

	cJSON* SC_TokenLogin = cJSON_CreateObject();

	std::string strUserToken = Token->valuestring;

	bool tokenLoginFlag = TokenMgr::GetInstance().HasToken(strUserToken);

	if (tokenLoginFlag)
	{
		// Token 正确,登录成功
		cJSON_AddStringToObject(SC_TokenLogin, "cmd", "SC_TokenLogin");
		cJSON_AddNumberToObject(SC_TokenLogin, "result", 1);
		cJSON_AddStringToObject(SC_TokenLogin, "msg", "Token Login Success");
		cJSON* player = cJSON_CreateObject();
		int TokenUID = -1;
		std::string strUsername = "";
		TokenMgr::GetInstance().GetUserInfo(strUserToken, TokenUID, strUsername);
		_userId = TokenUID;

		cJSON_AddNumberToObject(player, "playerId", TokenUID);
		cJSON_AddStringToObject(player, "username", strUsername.c_str());
		cJSON_AddStringToObject(player, "token", strUserToken.c_str());

		cJSON_AddItemToObject(SC_TokenLogin, "player", player);

		//该连接存储用户属性
		_player = new Player(TokenUID, strUsername);
		_player->SetTcpClient(this);

		//全局存储连接
		g_clients[TokenUID] = this;  //存储链接
	}
	else
	{
		// Token 错误
		cJSON_AddStringToObject(SC_TokenLogin, "cmd", "SC_TokenLogin");
		cJSON_AddNumberToObject(SC_TokenLogin, "result", 0);
		cJSON_AddStringToObject(SC_TokenLogin, "msg", "Token Error");
	}


	std::string msg = cJSON_Print(SC_TokenLogin);
	cJSON_Delete(SC_TokenLogin);

	SendData(msg.c_str(), msg.size());
}

客户端的Login消息处理

登录请求

代码语言:cpp
复制
void GameClient::SendLoginRequest(const char* username, const char* password)
{
	cJSON* root = cJSON_CreateObject();
	cJSON_AddStringToObject(root, "cmd", "CS_Login");
	cJSON_AddStringToObject(root, "username", username);
	cJSON_AddStringToObject(root, "password", password);
	cJSONSendMsg(root);
}

Token登陆请求

代码语言:cpp
复制
void GameClient::SendTokenLoginRequest(const char* token)
{
	cJSON* root = cJSON_CreateObject();
	cJSON_AddStringToObject(root, "cmd", "CS_TokenLogin");
	cJSON_AddStringToObject(root, "token", token);
	cJSONSendMsg(root);
}

客户端用户名密码正确的情况

代码语言:cpp
复制
void GameClient::SC_LoginEvent(cJSON* root)
{
	std::cout << "Client SC_LoginEvent Success" << std::endl;

	cJSON* token = cJSON_GetObjectItem(root, "token");
    //客户端存储Token
	_token = token->valuestring;

	SendTokenLoginRequest(_token.c_str());
}

Token登陆成功后

代码语言:cpp
复制
void GameClient::SC_TokenLoginEvent(cJSON* root)
{
	std::cout << "Client SC_TokenLoginEvent Success" << std::endl;
	cJSON* player = cJSON_GetObjectItem(root, "player");
	cJSON* userId = cJSON_GetObjectItem(player, "playerId");
	cJSON* userName = cJSON_GetObjectItem(player, "username");
	cJSON* token = cJSON_GetObjectItem(player, "token");
	_token = token->valuestring;

	Player* clientPlayer = new Player(userId->valueint, userName->valuestring);
	_clientPlayer = clientPlayer; //客户端存储用户信息
	this->_widgetType = Game_Hall;//切换UI
}

交互截图


总结

通常使用Token进行身份验证是一种常见的安全机制,为此允许服务端在客户端与服务端之间传递一个令牌,以替代传统的用户名和密码认证。

以上均基于简单模拟实现,未实现防止SQL注入,加密解密验证,redis配合数据库缓存等功能,日后完善。

如有纰漏谬误,敬请指出。

0sJ)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Token值的存储
    • 1. 存储在数据库中
      • 2. 存储在服务端的一个数据结构中
        • 3. 使用数据库作为主存储: 存储所有 token 和相关的用户信息。
        • Token验证登录实现
          • 实现一个Token管理器类
            • CS交互演示
              • 服务端的Login信息处理
                • 登陆回复
                • Token登陆回复
              • 客户端的Login消息处理
                • 登录请求
                • Token登陆请求
                • 客户端用户名密码正确的情况
                • Token登陆成功后
              • 交互截图
              • 总结
              相关产品与服务
              云数据库 Redis
              腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档