지난번에는 회원가입을 만들었다.

이번엔 로그인을 구현해보자.


구체적인 로그인 과정

  1. 클라이언트가 인증서버로 nickname, password를 보낸다.
  2. 인증서버는 postgres에서 nickname에 해당하는 hashed_pw와 user_id를 SELECT한다.
  3. BCrypt::validatePassword 함수와 hashed_pw를 사용해 클라가 보낸 pw가 유효한지 확인한다.
  4. 유효하지 않다면 해당 nickname의 로그인 실패 횟수를 redis에 만료시간 10분으로 업데이트하고, 5회가 넘어가면 차단한다.
  5. 유효하다면 user_id, nickname을 jwt에 담고, 게임서버와 공유하는 secret_key로 서명한다.
  6. 클라에게 success와 jwt를 보낸다.
  7. 클라가 게임서버로 jwt를 보내고, 게임서버는 그것을 인증서버와 공유하는 secret_key를 사용해 유효성 검사를 한다.

여기서 7번은 클라-게임서버 에서 구현할 부분이니 6번까지만 구현해보았다.

void	Handle_C_LOGIN(const PacketSessionRef& session, const Protocol::C_LOGIN& pkt)
{
	Protocol::S_LOGIN	response;
	std::string	nickname = pkt.nickname();
	std::string	password = pkt.password();

	std::string	q1_password = "SELECT user_id, password FROM auth.users WHERE nickname = '" + nickname + "'";
	PGConnection*	pg = GDBConnectionPool->PopPG();
	PGresult*	pgResult = pg->ExecuteSQL(q1_password);
	GDBConnectionPool->Push(pg);
	if (PQntuples(pgResult) != 1)
	{//존재하지 않는 nickname
		PQclear(pgResult);
		response.set_success(false);
		session->Send(ClientPacketHandler::MakeWriteBuffer(response));
		return;
	}
	std::string	a_user_id = PQgetvalue(pgResult, 0, 0);
	std::string	a_password = PQgetvalue(pgResult, 0, 1);
	PQclear(pgResult);

	if (BCrypt::validatePassword(password, a_password))
	{//로그인 성공
		std::string	token = CreateAccessToken(a_user_id, nickname);
		response.set_success(true);
		response.set_token(token);
		std::cout << "token : " << token << std::endl;
		session->Send(ClientPacketHandler::MakeWriteBuffer(response));
	}
	else
	{//비밀번호 틀림
		std::string	query = "INCR " + nickname + ":fail_count EX 600";
		RedisConnection*	redis = GDBConnectionPool->PopRedis();
		redisReply*	reply = redis->Execute(query);
		int	fail_count = reply->integer;
		freeReplyObject(reply);
		GDBConnectionPool->Push(redis);
		if (fail_count >= 5)
		{
			//TODO 차단
		}
		response.set_success(false);
		session->Send(ClientPacketHandler::MakeWriteBuffer(response));
	}
}

 

CreateAccessToken

jwt-cpp를 사용하려면 깃허브에서 받아야한다.

https://github.com/Thalhammer/jwt-cpp

 

GitHub - Thalhammer/jwt-cpp: A header only library for creating and validating json web tokens in c++

A header only library for creating and validating json web tokens in c++ - Thalhammer/jwt-cpp

github.com

git clone https://github.com/Thalhammer/jwt-cpp.git

jwt-cpp는 헤더만으로 동작하기때문에 헤더만 include해주면 된다.

Makefile에서 include항목에 -Ijwt-cpp/include 를 추가해주자

INC = -I$(SERVER_CORE_DIR) -Ijwt-cpp/include

 

#include <jwt-cpp/jwt.h>

std::string CreateAccessToken(const std::string& user_id, const std::string& nickname)
{
    const std::string SECRET_KEY = std::getenv("JWT_SECRET_KEY");

    auto token = jwt::create()
        .set_issuer("auth_server")          // 발급자
        .set_type("JWT")
        .set_payload_claim("user_id",  jwt::claim(user_id))
        .set_payload_claim("nickname", jwt::claim(nickname))
        .set_issued_at(std::chrono::system_clock::now())
        .set_expires_at(std::chrono::system_clock::now() 
                        + std::chrono::hours(1))    // 1시간
        .sign(jwt::algorithm::hs256{SECRET_KEY});   // 비밀키로 서명

    return token;
}

더미클라이언트

더미클라이언트에서 로그인을 성공해서 토큰을 받은 뒤에, secret_key를 사용해 유효성을 검사해봤다.

void	Handle_S_LOGIN(const PacketSessionRef& session, const Protocol::S_LOGIN& pkt)
{
	bool	success = pkt.success();
	if (success)
	{
		std::string	token = pkt.token();
		std::string	user_id;
		if (VerifyAccessToken(token, user_id))
		{
			std::cout << "login suceess! user : " << user_id << std::endl;
		}
	}
	else
	{
		std::cout << "login fail!" << std::endl;
		session->Disconnect();
	}
}

bool VerifyAccessToken(const std::string& token, std::string& out_user_id)
{
    const std::string SECRET_KEY = std::getenv("JWT_SECRET_KEY");
    try
    {
        auto verifier = jwt::verify()
            .allow_algorithm(jwt::algorithm::hs256{SECRET_KEY})
            .with_issuer("auth_server");    // 발급자 확인
        
        auto decoded = jwt::decode(token);
        verifier.verify(decoded);           // 서명 + 만료시간 자동 검증
        
        out_user_id = decoded.get_payload_claim("user_id").as_string();
        return true;
    }
    catch (const std::exception& e)
    {
		std::cout << "exeption" << std::endl;
        // 서명 불일치, 만료, 형식 오류 전부 여기로 떨어짐
        return false;
    }
}

회원가입 부터 로그인까지 다 잘 되는 모습이다.

'프로젝트 > Project_Island' 카테고리의 다른 글

21. EmailAPI - SMTP  (0) 2026.03.13
20. DBConnection 리팩토링  (0) 2026.03.11
18. 회원가입  (0) 2026.03.10
17. Session, EpollEvent 포인터 참조 문제 해결  (0) 2026.03.09
16. RedisConnection  (0) 2026.03.08

+ Recent posts