RedisConnectionPool- 회원가입, 로그인 로직 구체적으로 정리 및 구현
- SQL쿼리 바인더
현재는 sql인젝션에 취약한 상태임 - 비밀번호 해싱과 관리
- 타이머 관리
타이머를 만들긴 했는데 사용이 불편하고, 적절한 해제방법은 구현하지 않았음 - 이메일 API 사용
- 너무 느린 빌드 문제 해결하기
Client/Server PacketHandler.h 코드 자동생성 기능 만들기게임서버강의에서 배운 Protocol.proto 파일을 파싱해서 패킷핸들러 헤더를 자동으로 만들어주는 프로그램을 만들자.
회원가입을 확실하게 구현해보자! email API파트 빼고.
회원가입 시퀀스 다이어그램

임시ID 발급에 대해 생각만 하고있었는데 구체적으로 정리해봤다.
만들다보니 왜 Redis가 필요한지 좀 알게된게 인증서버에서는 state를 저장하지 않고, 클라이언트에게 임시로 발급한 ID를 해당 유저의 키값으로 활용해서 상대가 누구였는지 기억할 필요 없이 원활하게 회원가입 절차를 처리할 수 있어지는게 좋은 것 같다.
이렇게 처리하기 위해서 일단 proto파일부터 변경했다.
1. 회원가입 요청
클라가 회원가입란에 정보를 적고 확인을 누르면 서버로 보낼 패킷에 대한 처리
Protocol.proto
닉네임, 비밀번호, 이메일을 받는다. skip_email은 테스트할 때 실제로 이메일을 보내긴 좀 그러니 추후에 EmailAPI를 추가하면 스킵할 수 있도록 추가했다.
// 1. 회원가입 요청
message C_SIGNUP {
string nickname = 1;
string password = 2;
string email = 3;
bool skip_email = 4;
}
// 1. ID/Email중복 여부
message S_SIGNUP {
bool success = 1;
bool skip_email = 2;
string temp_id = 3;
}
ClientPacketHandler.cpp
길고 복잡해보이지만 아래 네단계로 이루어져있다.
- 중복확인 : postgres에 id, email중복 있는지 확인
- 준비 : 비밀번호 암호화, 클라이언트용 임시 ID 생성
- 저장 : 임시ID를 key로 닉네임, 암호화된 비밀번호, 이메일을 Redis에 저장.
- 전송 : 성공/실패 여부와 클라가 다음에 보낼 임시ID를 Send
void Handle_C_SIGNUP(const PacketSessionRef& session, const Protocol::C_SIGNUP& pkt)
{
Protocol::S_SIGNUP response;
std::string nickname = pkt.nickname();
std::string email = pkt.email();
bool skip_email = pkt.skip_email();
std::string pgsql = "SELECT user_id FROM auth.users WHERE nickname = '" + nickname + "' OR email = '" + email + "'";
//TODO 커넥션풀에 남은거 없을 때 처리
PGConnection* pg = GDBConnectionPool->PopPG();
PGresult* pgResult = pg->ExecuteSQL(pgsql.c_str());
GDBConnectionPool->Push(pg);
int num = PQntuples(pgResult);
if (num == 0)// ID,email 중복 없음 생성가능
{
std::string hashedPassword = BCrypt::generateHash(pkt.password());
std::string temp_id = GetTempId(32);
std::string base = "SET " + temp_id;
std::string q1, q2, q3, a1, a2, a3;
q1 = base + ":nickname " + nickname;
q2 = base + ":password " + hashedPassword;
q3 = base + ":email " + email;
redisReply *reply;
RedisConnection* redis = GDBConnectionPool->PopRedis();
//TODO 커넥션풀에 남은거 없을 때 처리
reply = redis->Execute(q1.c_str());//set 닉네임
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a1 = reply->str;
freeReplyObject(reply);
reply = redis->Execute(q2.c_str());//set password
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a2 = reply->str;
freeReplyObject(reply);
reply = redis->Execute(q3.c_str());//set email
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a3 = reply->str;
freeReplyObject(reply);
GDBConnectionPool->Push(redis);
redis = nullptr;
if (a1 != "OK" || a2 != "OK" || a3 != "OK")
{//실패 있을경우 에러처리
return ;
}
response.set_success(true);
response.set_skip_email(skip_email);
response.set_temp_id(temp_id);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
}
else// ID,email 중복 있음 생성 불가능
{
response.set_success(false);
response.set_skip_email(skip_email);
response.set_temp_id("");
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
}
}
실제로 테스트를 해보니 BCrypt::generateHash(pkt.password()); 이게 정말 매우매우 느리다. 하나에 0.4초라고 한다.
BCrypt::generateHash(pkt.password(), 10); 이렇게 함수 오버로딩으로 뒤에 숫자넣은 버전이 있는데 내부에서 연산횟수를 2의 n승회로 설정하는것이라고 한다. 이걸 높이면 연산이 느려지기 때문에 브루트포스에 강해지는 대신 속도가 느려지는거고, 낮추면 브루트포스에 취약해지지만 속도가 빨라진다. 기본값은 12
일단 너무 느린 것 같아서 10으로 낮춰봤다.
2. 이메일 인증 요청
클라가 id생성 가능을 확인받으면 자동으로 보내는 패킷에 대한 처리.
Protocol.proto
아까 적은 이메일로 인증번호를 보내달라는 요청. 만약 인증실패해도 "인증번호 다시 발송" 버튼으로 다시 보낼 수 있도록 만들 예정이다.
// 2. 이메일 인증 코드 요청
message C_VERIFY_MAIL_REQ {
string temp_id = 1;
}
// 2. 인증코드 발송 성공여부
message S_VERIFY_MAIL_REQ {
bool success = 1;
}
ClientPacketHandler.cpp
8자리 인증코드를 생성한다.
임시ID기반으로 email을 redis에서 가져오고 (그냥 클라가 보내게 해도 될 것 같다)
그email로 인증코드 발송한다. 그리고 발송 성공 여부를 클라이언트에 Send.
void Handle_C_VERIFY_MAIL_REQ(const PacketSessionRef& session, const Protocol::C_VERIFY_MAIL_REQ& pkt)
{
Protocol::S_VERIFY_MAIL_REQ response;
std::string temp_id = pkt.temp_id();
//TODO 임시id 유효성 검사
std::string verfiy_code = "12341234";//GetTempId(8); 더미클라이언트 테스트용 고정코드
std::string q1, q2;
q1 = "GET " + temp_id + ":email";
q2 = "SET " + temp_id + ":verify_code " + verfiy_code;
std::string a1_email, a2_status;
redisReply *reply;
RedisConnection* redis = GDBConnectionPool->PopRedis();
reply = redis->Execute(q1);//아까 등록한 이메일 레디스에서 가져오기
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a1_email = reply->str;
freeReplyObject(reply);
reply = redis->Execute(q2);//인증코드 레디스에 저장
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a2_status = reply->str;
freeReplyObject(reply);
GDBConnectionPool->Push(redis);
//TODO EmailAPI로 이메일 보내기, 실패시 처리
response.set_success(true);
response.set_temp_id(temp_id);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
}
3. 이메일 인증 확인 요청
클라가 이메일 인증 "확인" 을 누르면 서버로 오는 패킷에 대한 처리.
Protocol.proto
// 3. 이메일 인증 확인 요청
message C_VERIFY_EMAIL_CODE {
string temp_id = 1;
string verify_code = 2;
}
// 3. 이메일 인증 성공 여부
message S_VERIFY_EMAIL_CODE {
bool success = 1;
bool expired = 2;
}
ClientPacketHandler.cpp
길고 복잡한데 레디스에서 인증코드가 맞는지 확인하고, 맞으면 레디스에 저장해놨던 id, email, pw을 다 꺼내서 postgres에 저장하는게 전부다. 성공여부를 클라이언트에 Send한다.
void Handle_C_VERIFY_EMAIL_CODE(const PacketSessionRef& session, const Protocol::C_VERIFY_EMAIL_CODE& pkt)
{
Protocol::S_VERIFY_EMAIL_CODE response;
std::string temp_id = pkt.temp_id();
//TODO 임시id 유효성 검사
std::string verify_code = pkt.verify_code();
std::string getBase = "GET " + temp_id;
std::string q1_verify_code, q2_nickname, q3_password, q4_email;
std::string a1_verify_code, a2_nickname, a3_password, a4_email;
q1_verify_code = getBase + ":verify_code";
q2_nickname = getBase + ":nickname";
q3_password = getBase + ":password";
q4_email = getBase + ":email";
redisReply *reply;
RedisConnection* redis = GDBConnectionPool->PopRedis();
reply = redis->Execute(q1_verify_code);//get 확인용 인증코드
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
if (reply->type == REDIS_REPLY_NIL)
{//인증코드가 만료됐을 경우
freeReplyObject(reply);
GDBConnectionPool->Push(redis);
response.set_success(false);
response.set_expired(true);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
return;
}
a1_verify_code = reply->str;
freeReplyObject(reply);
if (verify_code != a1_verify_code)
{//인증코드가 틀렸을 경우
GDBConnectionPool->Push(redis);
response.set_success(false);
response.set_expired(true);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
return;
}
/* 인증코드가 맞았을 경우 */
reply = redis->Execute(q2_nickname);//get 레디스에 저장해둔 닉네임
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a2_nickname = reply->str;
freeReplyObject(reply);
reply = redis->Execute(q3_password);//get 레디스에 저장해둔 비밀번호
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a3_password = reply->str;
freeReplyObject(reply);
reply = redis->Execute(q4_email);//get 레디스에 저장해둔 비밀번호
if (RedisConnection::isReplyError(reply))
{//에러면 함수안에서 free해줌.
GDBConnectionPool->Push(redis);
return;
}
a4_email = reply->str;
freeReplyObject(reply);
GDBConnectionPool->Push(redis);
std::string pgsql = "INSERT INTO auth.users (nickname, password, email) VALUES \
('" + a2_nickname + "', '" + a3_password + "', '" + a4_email + "')";
PGConnection* pg = GDBConnectionPool->PopPG();
PGresult* pgResult = pg->ExecuteSQL(pgsql.c_str());
GDBConnectionPool->Push(pg);
PQclear(pgResult);
response.set_success(true);
response.set_expired(false);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
}
아직 완성은 아니고 나중에 EmailAPI, 쿼리 바인딩을 구현하면 수정될 예정이다.
더미 클라이언트쪽에서는 별게없다. db작업이 없기때문에 그냥 패킷만 채워서 보냈다.
void Handle_S_SIGNUP(const PacketSessionRef& session, const Protocol::S_SIGNUP& pkt)
{
Protocol::C_VERIFY_MAIL_REQ response;
bool success = pkt.success();
std::string temp_id = pkt.temp_id();
if (success)
{
response.set_temp_id(temp_id);
session->Send(ServerPacketHandler::MakeWriteBuffer(response));
}
else
{
session->Disconnect();
return;
}
}
void Handle_S_VERIFY_MAIL_REQ(const PacketSessionRef& session, const Protocol::S_VERIFY_MAIL_REQ& pkt)
{
Protocol::C_VERIFY_EMAIL_CODE response;
bool success = pkt.success();
std::string temp_id = pkt.temp_id();
if (success)
{
response.set_temp_id(temp_id);
response.set_verify_code("12341234");
session->Send(ServerPacketHandler::MakeWriteBuffer(response));
}
else
{
session->Disconnect();
return;
}
}
void Handle_S_VERIFY_EMAIL_CODE(const PacketSessionRef& session, const Protocol::S_VERIFY_EMAIL_CODE& pkt)
{
bool success = pkt.success();
if (success)
{
std::cout << "Successfully signed up!" << std::endl;
}
else
{
std::cout << "Failed to sign up!" << std::endl;
}
session->Disconnect();
}
postgres에서 보면 계정이 등록되어있다!

'프로젝트 > Project_Island' 카테고리의 다른 글
| 20. DBConnection 리팩토링 (0) | 2026.03.11 |
|---|---|
| 19. 로그인 (0) | 2026.03.11 |
| 17. Session, EpollEvent 포인터 참조 문제 해결 (0) | 2026.03.09 |
| 16. RedisConnection (0) | 2026.03.08 |
| 15. 패킷 자동화 (2) (0) | 2026.03.08 |