이제 클라이언트에도 TLS를 적용시켰다.

 

이제 남은것은 클라이언트를 만들고, 컨텐츠를 구현하는 것 뿐이다.

게임서버 강의(2) 에서는 이 뒤로 spawn, 이동 동기화를 진행하는 순서였는데 게임 컨텐츠 개발이야 정해진 순서가 있는건 아니니까 회원가입과 로그인부터 구현해보려고 한다.


회원가입과 로그인을 만들려면 언리얼 클라이언트, 인증서버, 게임서버 모두 다뤄야 해서 좀 복잡했다.

 

Proto 수정

클라이언트와 인증서버와의 TLS연결까지는 해봤지만 protobuf로 직렬화한 통신을 하려면 일단 같은 proto파일로 message를 맞춰야한다. 그 작업부터 해주자.

기존에는 message 이름에 서버가 만들어 보내는건 S_ 를 붙이고 클라가 만들어 보내는건 C_ 를 붙였는데, 지금은 서버가 게임서버와 인증서버로 두가지인 상황이다. 그러니까 네이밍 컨벤션을 게임서버 - 클라 통신은 GS_, GC_ 로 인증서버 - 클라 통신은 AS_, AC_ 로 구분하자.

 

인증서버에서 쓰던 Protocol.proto를 그대로 덧붙이고 message의 이름만 변경해줬다.

GameServer에 있는 Protocol.proto

syntax = "proto3";
package Protocol;

import "Enum.proto";
import "Struct.proto";

// Game Server용 프로토콜

message GC_LOGIN {
	string jwt = 1;
}

message GS_LOGIN {
	bool success = 1;
	uint32 user_id = 2;
}
/*...*/

message GS_CHAT {
  int32 user_id = 1;
  string msg = 2;
}

// Auth Server용 프로토콜

// 1. 회원가입 요청
message AC_SIGNUP {
    string nickname = 1;
    string password = 2;
    string email = 3;
    bool skip_email = 4; 
}
// 1. ID/Email중복 여부
message AS_SIGNUP {
    bool success = 1;
    bool skip_email = 2;
    string temp_id = 3;
    string reason = 4;
}
/*...*/

// 4. 로그인 요청
message AC_LOGIN {
    string nickname = 1;
    string password = 2;
}
// 4. 로그인 성공 여부
message AS_LOGIN {
    bool success = 1;
    bool is_block = 2;
    int32 fail_count = 3;
    string token = 4;
    string reason = 5;
}

 

그리고 Protocol.proto 파일과 탬플릿 패킷핸들러를 기반으로 파이썬프로그램을 사용해 패킷핸들러를 만들어줬었는데 기존에 C_랑 S_만 받아서 썼던걸 AC_, GC_, AS_, GS_ 이렇게 더 써야하기 때문에 이 파이썬프로그램도 수정해줘야한다.

1차 수정 이후에 사용하다가 패킷 id가 일치하지 않는 문제가 생겨서 인증서버용 패킷은 id를 20000번대로, 게임서버용은 10000번대로 설정되도록 수정했다. (이 부분은 AI가 거의 다 짜줬다.) 아래 파이썬 코드는 2차수정본이다.

 

PacketGenerator.py

import argparse
import jinja2
import ProtoParser

def main():

    arg_parser = argparse.ArgumentParser(description = 'PacketGenerator')
    arg_parser.add_argument('--path', type=str, default='/home/server/Protocol', help='proto path')
    arg_parser.add_argument('--output', type=str, default='TestPacketHandler', help='output file')
    arg_parser.add_argument('--recv', type=str, nargs='+', default=['C_'], help='recv convention')
    arg_parser.add_argument('--send', type=str, nargs='+', default=['S_'], help='send convention')
    args = arg_parser.parse_args()

    parser = ProtoParser.ProtoParser(args.recv, args.send)
    parser.parse_proto(args.path)

    file_loader = jinja2.FileSystemLoader('Templates', encoding='cp949')
    env = jinja2.Environment(loader=file_loader)
    
    template = env.get_template('PacketHandler.h')
    output = template.render(parser=parser, output=args.output)
    
    f = open(args.output+'.h', "w+")
    f.write(output)
    f.close()

    return

if __name__ == '__main__':
    main()

 

ProtoParser.py

class ProtoParser(object):
    def __init__(self, recv_prefixes, send_prefixes):
        self.recv_pkt = []
        self.send_pkt = []
        self.total_pkt = []
        self.recv_prefixes = recv_prefixes if isinstance(recv_prefixes, list) else [recv_prefixes]
        self.send_prefixes = send_prefixes if isinstance(send_prefixes, list) else [send_prefixes]

        # prefix별 시작 ID 계산 (첫번째 1만번대, 두번째 2만번대 ...)
        all_prefixes = self.recv_prefixes + self.send_prefixes
        unique_first_chars = []
        for p in all_prefixes:
            first_char = p[0]
            if first_char not in unique_first_chars:
                unique_first_chars.append(first_char)

        self.prefix_id_map = {}
        for i, char in enumerate(unique_first_chars):
            self.prefix_id_map[char] = (i + 1) * 10000

    def get_next_id(self, pkt_name):
        first_char = pkt_name[0]
        id = self.prefix_id_map[first_char]
        self.prefix_id_map[first_char] += 1
        return id

    def parse_proto(self, path):
        f = open(path, 'r', encoding='utf-8')
        lines = f.readlines()

        for line in lines:
            if line.startswith('message') == False:
                continue

            pkt_name = line.split()[1].upper()
            if any(pkt_name.startswith(p.upper()) for p in self.recv_prefixes):
                id = self.get_next_id(pkt_name)
                self.recv_pkt.append(Packet(pkt_name, id))
                self.total_pkt.append(Packet(pkt_name, id))
            elif any(pkt_name.startswith(p.upper()) for p in self.send_prefixes):
                id = self.get_next_id(pkt_name)
                self.send_pkt.append(Packet(pkt_name, id))
                self.total_pkt.append(Packet(pkt_name, id))
        f.close()

class Packet:
    def __init__(self, name, id):
        self.name = name
        self.id = id

 

GenPacket.bat

파이썬 프로그램으로 패킷핸들러를 만드는 부분도 아래와 같이 수정해준다.

GenPackets.exe --path=./Protocol.proto --output=ClientPacketHandler --recv=GC_ --send=GS_
GenPackets.exe --path=./Protocol.proto --output=ServerPacketHandler --recv GS_ AS_ --send GC_ AC_

 

이제 GenPacket.bat을 실행해서 패킷핸들러를 만들면 패킷 클래스 이름이 바뀌었고 추가된 함수들도 많기 때문에 기존 코드도  이에 맞춰서 수정해준다. 컴파일 시도하면서 없는 클래스 이름이다 하는 부분을 다 고쳐준다.


인증세션, 게임세션 추가

지금은 TLSSession 뿐인데, 인증과정에서의 연결과 처리를 담당할 인증세션과 로그인 성공 이후를 담당할 게임 세션을 만들어줬다. 내용이 많으니 git을 참조하는게 나을듯 하다.

#pragma once
#include "PacketSession.h"

class CLIENT_API AuthSession : public TLSSession
{
	/*...*/
};

jwt-cpp 추가

인증서버에서 발급한 jwt로 게임서버에서 신원을 인증받아야하니까 jwt-cpp가 게임서버쪽에도 필요하다.

인증서버에서 추가했던 것 처럼 jwt-cpp를 gitclone 하고, 받은 파일에서 include 폴더만 쏙 뽑아서 게임서버에 넣어준다.

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

Include 폴더를 서버 솔루션 폴더/Libraies/ 로 옮겨줬다.

서버코어의 Utils에서 JWT 검증함수를 만들어 쓸거니까 ServerCore 프로젝트 속성에서 포함디렉터리도 추가해준다.

 

Utils 클래스에 VerifyAccessToken 함수 추가

인증서버에 있던걸 그대로 가져왔다.

컴파일이 안되는 문제가 생겨서 확인해보니 jwt 헤더와 pch에 포함된 다른 헤더가 충돌이 생겨서 컴파일이 안됐다.

Utils.cpp 에서 include pch를 제거하고, Utils.cpp의 속성에서 미리컴파일된 헤더 사용하지 않음으로 수정하니까 해결됐다.

#pragma once

#include <mutex>
#include <random>
#include <string>
#include <iostream>
#include "Types.h"

class Utils
{
public:
	static int ErrorExit(const char* errstr);
	static int32 GetRandNum(int32 start, int32 end);
	static  bool VerifyAccessToken(const std::string& token, std::string& out_user_id, std::string& out_nickname);
	/*...*/

private:
	static std::mutex m;
};
#include "Utils.h"
#include <jwt-cpp/jwt.h>
#include <random>

using namespace std;

/*...*/

bool Utils::VerifyAccessToken(const string& token, string& out_user_id, string& out_nickname)
{
	// TODO 비밀키 환경변수에 저장하거나 다른방법으로 가져와야함.
	const string SECRET_KEY = "cb1c63a81ccd9488c37de67a6028996ca0d994f1f22d05a84818a8a770e028ab";

	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();
		out_nickname = decoded.get_payload_claim("nickname").as_string();
		return true;
	}
	catch (const exception& e)
	{
		cout << "exeption" << endl;
		// 서명 불일치, 만료, 형식 오류 전부 여기로 떨어짐
		return false;
	}
}

 

위 함수는 게임서버에 있는 Handle_GC_LOGIN에서 아래와 같이 사용됐다.

void Handle_GC_LOGIN(const PacketSessionRef& session, const Protocol::GC_LOGIN& pkt)
{
	/*...*/
	if (false == Utils::VerifyAccessToken(jwt, OUT userId, OUT nickname))
	{//jwt 검증 실패 시
		response.set_success(false);
		session->Send(ClientPacketHandler::MakeSendBuffer(response));
		return;
	}
    //jwt 검증 성공 시
	/*...*/
}

회원가입

회원가입을 처리하기 위해 언리얼 클라이언트 위젯을 사용해서 간단하게 UI를 만들어봤다.

아래 영상을 보고 따라해 UI부분을 만들었고, 로직은 일단 회원가입 로직을 돌릴 수 있는 정도로만 만들어봤다. (이건 레퍼런스 없이 직접 빨리 만들어본거라 굉장히 주먹구구)

 

 

인증코드를 성공적으로 입력하면 메인 메뉴로 돌아온다.

 

회원가입 과정의 에러처리

  • 잘못된 형식의 이메일을 적거나 ID에 문제가 있으면 인증서버에서 실패 패킷을 보내고 실패 사유를 띄웠다. 이 과정에서 언리얼의 DELEGATE 라는 매우 유용한 기능을 배웠다.
  • 마찬가지로 인증번호가 틀리면 실패사유를 띄우게 만들어봤다.

당장 필수적인 기능들은 아니지만 DELEGATE 라는건 알아둬야 할 것 같아서 여기서 먼저 적용해봤다.

로그인

인증서버에 등록되어있는 ID와 PW를 제대로 입력하면 인증서버로부터 성공 패킷과 JWT를 받는다. JWT는 세션에 저장해놓고, 게임서버에 TCP, TLS 접속을 한다. 게임서버에 접속을 성공하면 즉시 GC_LOGIN 패킷에 JWT를 담아서 게임서버로 보낸다.

게임서버는 인증서버와 공유하고있는 비밀키를 사용해 JWT가 유효한지 확인하고, 유효하다면 클라에게 GS_LOGIN 성공패킷을 보내준다.


현재까지의 git 버전

게임서버

 

Feat: jwt-cpp추가, Handle_GC_LOGIN에서 jwt 검증 추가, 프로토콜 변경 등 · Dodontak/Project_Island_GameServer@3b7b

jwt-cpp 추가. Utils의 VerifyAccessToken 함수 추가. 기존에는 Handle_C_LOGIN에서 jwt가 pass인지 아닌지만 확인했는데, VerifyAccessToken 함수로 검증 과정 추가됨. 프로토콜이 1000번대로만 있었는데, 10000번대는

github.com

언리얼 클라이언트

 

Feat: 메인메뉴, 로그인, 회원가입 기능 추가 · Dodontak/Project_Island_Client@b5eaefa

메인메뉴, 로그인, 회원가입을 추가함

github.com

인증서버

 

Refactor: PacketId, Packet Prefix · Dodontak/AuthServer@7057bc3

Protocol PacketId changed 1000~ to 20000~

github.com

 

+ Recent posts