在实现Token令牌登录前,首先需要思考Token的存储形式
基于用户ID唯一,以及一个Token 值对应一个用户ID和用户姓名的情况下
决定在哪里存储 token 值取决于多种因素,包括安全性、可扩展性、持久性和可用性等
还有一种结合使用的最推荐方案
使用缓存技术: 如 Redis 或 Memcached 等缓存系统,用来缓存最近使用的 token 信息。这样可以显著减少数据库的访问次数,提高性能。
这种方法结合了数据库的安全性和持久性以及缓存的高性能。当客户端发送请求时,首先从缓存中查询 token 信息,如果缓存中存在则冷加载数据,如果缓存中不存在,则从数据库中查询并将结果缓存起来。当 token 过期或者被注销时,从缓存和数据库中删除相应的记录。
综上所述由于只是一个简单的令牌Token登陆验证模拟实现,忽略Token加密和Token验证等环节,并且采用第二种方法
由于简单实现的原因只需要一个管理器类,基于单例模式在服务端全局使用
单例模式的讲解可以参考这篇文章
Token管理器类出于用户ID的唯一性采用了双向映射:
一个是从令牌到用户信息(已经存在),另一个是从用户ID到令牌。这样,当需要更新用户的令牌时,可以直接通过用户ID找到旧的令牌并删除它,而无需遍历整个 _tokenMap
。
综上所述实现基本的增删改查
TokenMgr.hpp
#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时,例如,在生成新令牌时,如果有多个线程同时执行,则可能会生成相同的令牌,这时就需要上锁了。
略去服务端客户端的搭建,这里仅以交互逻辑为例
一个Token登录流程
客户端进行登陆请求,并发送相应的用户名和密码
服务端验证登陆无误后,生成一个 Token 并将用户信息存储在服务端(如 Redis,数据库,临时数据结构)以便快速验证和获取用户信息,旧的Token进行删除,服务端向客户端返回Token。
客户端将 Token 存储在本地(存储可加密)
在每个后续的登陆或非登录的请求中,客户端可以通过请求头(内含Token)发送给服务端。
服务端从请求头中获取 Token,解密验证其合法性后,完成访问受保护的功能
略去加密验证等繁琐步骤后,遵循客户端的一切行动逻辑都尽量基于服务端的情况下。
CS简单实现一个Token登陆交互
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());
}
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());
}
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);
}
void GameClient::SendTokenLoginRequest(const char* token)
{
cJSON* root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "cmd", "CS_TokenLogin");
cJSON_AddStringToObject(root, "token", token);
cJSONSendMsg(root);
}
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());
}
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 删除。