본문 바로가기
개인공부/Web API 게임 서버 공부

Web API 서버 예시

by 하고싶은건많은놈 2023. 4. 24.

Main DB(MySQL)과 Redis를 사용하며, 간단한 로그인 기능정도가 구현되어있는 Web API 서버를 구현

기본 구성은 다음과 같음


Program.cs

프로그램의 진입점(entry point) 역할

using APIServer.Services;
using ZLogger;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IAccountDb, AccountDb>();
builder.Services.AddSingleton<IRedisDb, RedisDb>();
builder.Services.AddControllers();

builder.Logging.ClearProviders();
builder.Logging.AddZLoggerConsole();

var app = builder.Build();

IConfiguration configuration = app.Configuration;

app.UseRouting();
app.MapControllers();

app.Run(configuration["ServerAddress"]);
  • WebApplication.CreateBuilder(args)
    웹 어플리케이션의 빌더 객체 생성
  • builder.Services.AddTransient<IAccountDb, AccountDb>()
    IAccountDb 인터페이스와 AccountDb간의 종속성 주입
    서비스 생명주기를 Transient로 설정하였기 때문에 매 요청마다 새로운 인스턴스를 생성하여 제공
  • builder.Services.AddSingleton<IRedisDb, RedisDb>()
    IRedisDb 인터페이스와 RedisDb간의 종속성주입
    서비스 생명주기를 Singleton으로 설정하였기 때문에 애플리케이션의 수명주기 동안 하나의 인스턴스만을 생성하여 제공
  • builder.Services.AddControllers()
    웹 API 컨트롤러 등록
  • builder.Logging.ClearProviders(), builder.LoggingAddZLoggerConsole()
    기존 로깅 공급자를 제거하고 ZLoggerConsole 공급자를 사용하도록 변경
  • builder.Build()
    애플리케이션을 빌드 - 소스 코드를 컴파일하고 실행 가능한 웹 애플리케이션 객체를 생성
  • IConfiguration configuration = app.Configuration
    애플리케이션의 구성 객체를 가져옴 - 일반적으로 appsettings.json 파일을 통해 설정
  • app.UseRouting()
    애플리케이션이 라우팅 기능을 사용하도록 설정
  • app.MapControllers()
    등록된 컨트롤러를 매핑하여 요청을 처리할 수 있도록 설정
  • app.Run(configuration["ServerAddress"])
    ServerAddress이라는 키에 해당하는 구성 정보를 사용하여 애플리케이션을 실행

 


AccountDb

계정정보는 메인 DB인 MySQL을 통해 관리되도록 설정

public class AccountDb : IAccountDb
{
    readonly ILogger<AccountDb> _logger;

    IDbConnection _dbConn;
    QueryFactory _queryFactory;

    public AccountDb(ILogger<AccountDb> logger, IConfiguration configuration)
    {
        _logger = logger;

        var AccountDbConnectString = configuration.GetSection("DBConnection")["AccountDb"];
        _dbConn = new MySqlConnection(AccountDbConnectString);

        var compiler = new SqlKata.Compilers.MySqlCompiler();
        _queryFactory = new SqlKata.Execution.QueryFactory(_dbConn, compiler);

        _logger.ZLogInformation("MySQL Db Connected");
    }
  • 생성자를 통해 SqlKata로 쿼리를 처리하기 위한 QueryFactory 변수를 설정
  • ASP.NET Core에서 제공하는 ILogger 인터페이스를 사용하여 로그를 기록
    단, 실제 로그 기록에는 ZLogger를 사용하여 성능을 향상

 

    public async Task<ErrorCode> CreateAccount(string email, string password)
    {
        try
        {
            await _queryFactory.Query("account").InsertAsync(new
            {
                Email = email,
                Password = password
            });

            return ErrorCode.None;
        }
        catch (MySqlException ex)
        {
            if (ex.Number == 1062)
            {
                _logger.ZLogError(ex, $"[CreateAccount] ErrorCode: {ErrorCode.CreateAccountFailDuplicate}, Email: {email}, ErrorNum : {ex.Number}");
                return ErrorCode.CreateAccountFailDuplicate;
            }
            else
            {
                _logger.ZLogError(ex, $"[CreateAccount] ErrorCode: {ErrorCode.CreateAccountFailException}, Email: {email}, ErrorNum : {ex.Number}");
                return ErrorCode.CreateAccountFailException;
            }
        }
    }
  • Account 생성시 사용되는 함수
  • InsertAsync() 메서드를 사용해 DB에 실제로 데이터를 저장, 이상이 없을시 에러코드 None 반환
  • try-catch로 MySqlException 발생을 확인한 경우 헤당 오류에 맞게 처리하고 적절한 에러코드 반환
    1062번은 이미 해당 키가 존재함을 뜻함

 

    public async Task<Tuple<ErrorCode, Int64>> VerifyAccount(string email, string password)
    {
        try
        {
            var accountinfo = await _queryFactory.Query("account").Where("Email", email).FirstOrDefaultAsync<Account>();
            if (accountinfo is null || accountinfo.AccountId == 0)
            {
                return new Tuple<ErrorCode, Int64>(ErrorCode.LoginFailUserNotExist, 0);
            }

            if (accountinfo.Password != password)
            {
                _logger.ZLogError($"[VerifyAccount] ErrorCode: {ErrorCode.LoginFailPwNotMatch}, Email: {email}");
                return new Tuple<ErrorCode, Int64>(ErrorCode.LoginFailPwNotMatch, 0);
            }

            return new Tuple<ErrorCode, Int64>(ErrorCode.None, accountinfo.AccountId);
        }
        catch (MySqlException ex)
        {
            _logger.ZLogError(ex, $"[VerifyAccount] ErrorCode: {ErrorCode.VerifyAccountFailException}, Email: {email}, ErrorNum : {ex.Number}");
            return new Tuple<ErrorCode, Int64>(ErrorCode.VerifyAccountFailException, 0);
        }
    }
}
  • 로그인시 Account 정보 확인에 사용되는 함수
  • 계정이 존재하는지 확인 후 비밀번호가 일치하는지 확인
  • Tuple 형식으로 에러코드와 계정의 ID번호를 반환

 


RedisDb

인증과 관련한 작업은 Redis를 사용하도록 설정

public class RedisDb : IRedisDb
{
    readonly ILogger<RedisDb> _logger;

    RedisConnection _redisConn;

    public RedisDb(ILogger<RedisDb> logger, IConfiguration configuration)
    {
        _logger = logger;

        var RedisAddress = configuration.GetSection("DBConnection")["Redis"];
        var Redisconfig = new RedisConfig("basic", RedisAddress);
        _redisConn = new RedisConnection(Redisconfig);

        _logger.ZLogInformation("Redis Db Connected");
    }
  • 생성자를 통해 cloudstructures 라이브러리로 redis 관련 작업을 하기 위한 _reidsConn 변수를 설정
  • ASP.NET Core에서 제공하는 ILogger 인터페이스를 사용하여 로그를 기록
    단, 실제 로그 기록에는 ZLogger를 사용하여 성능을 향상

 

    public async Task<ErrorCode> RegistUser(string email, string authToken, Int64 accountId)
    {
        var key = "UID_" + accountId;
        var user = new AuthUser
        {
            Email = email,
            AuthToken = authToken,
            AccountId = accountId,
            State = UserState.Default.ToString()
        };

        try
        {
            var redis = new RedisString<AuthUser>(_redisConn, key, LoginTimeSpan());
            if (await redis.SetAsync(user, LoginTimeSpan()) == false)
            {
                _logger.ZLogError($"[RegistUser] ErrorCode: {ErrorCode.LoginFailAddRedis}, Email: {email}");

                return ErrorCode.LoginFailAddRedis;
            }
        }
        catch (Exception ex)
        {
            _logger.ZLogError(ex, $"[RegistUser] ErrorCode: {ErrorCode.RegistUserFailException}, Email: {email}");
            return ErrorCode.RegistUserFailException;
        }

        return ErrorCode.None;
    }

    public TimeSpan LoginTimeSpan()
    {
        return TimeSpan.FromMinutes(RediskeyExpireTime.LoginKeyExpireMin);
    }
}
  • Login에 성공한 이후의 유저 요청을 인증하기 위한 유저 등록 작업을 수행
  • RedisString<> 클래스로 String 타입의 데이터를 다루는 객체를 생성
    키는 유저의 ID에 따라 유니크하도록 설정하여 이후 인증작업에 사용
    LoginTimeSpan() 함수로 지정한 데이터 만료시간이 지나면 해당 데이터가 자동으로 삭제됨
  • SetAsync() 메서드로 미리 생성해놓은 AuthUser 객체를 Redis에 저장

 


CreateAccountController

CreateAccount 요청시 실행되는 컨트롤러

[ApiController]
[Route("[controller]")]
public class CreateAccount : ControllerBase
{
    readonly ILogger<CreateAccount> _logger;
    readonly IAccountDb _AccountDb;

    public CreateAccount(ILogger<CreateAccount> logger, IAccountDb AccountDb)
    {
        _logger = logger;
        _AccountDb = AccountDb;
    }

    [HttpPost]
    public async Task<PkCreateAccountResponse> Post(PkCreateAccountRequest request)
    {
        var response = new PkCreateAccountResponse();
        response.Result = ErrorCode.None;

        var errorCode = await _AccountDb.CreateAccount(request.Email, request.Password);
        if (errorCode != ErrorCode.None)
        {
            response.Result = errorCode;
            return response;
        }

        _logger.ZLogInformation($"{request.Email} Account Created");

        return response;
    }
}
  • AccountDb는 program.cs에서 등록한 서비스에서 생성된 인스턴스를 활용
  • AccountDb의 CreateAccount() 메서드르 사용하여 계정 생성 작업을 수행

 


LoginController

Login 요청시 실행되는 컨트롤러

[ApiController]
[Route("[controller]")]
public class Login : ControllerBase
{
    readonly ILogger<Login> _logger;
    readonly IAccountDb _AccountDb;
    readonly IRedisDb _redisDb;

    public Login(ILogger<Login> logger, IAccountDb AccountDb, IRedisDb redisdb)
    {
        _logger = logger;
        _AccountDb = AccountDb;
        _redisDb = redisdb;
    }

    [HttpPost]
    public async Task<PkLoginResponse> Post(PkLoginRequest request)
    {
        var response = new PkLoginResponse();
        response.Result = ErrorCode.None;

        var (errorCode, accountId) = await _AccountDb.VerifyAccount(request.Email, request.Password);
        if (errorCode != ErrorCode.None)
        {
            response.Result = errorCode;
            return response;
        }

        var authToken = CreateAuthToken();
        errorCode = await _redisDb.RegistUser(request.Email, authToken, accountId);
        if (errorCode != ErrorCode.None)
        {
            response.Result = errorCode;
            return response;
        }

        _logger.ZLogInformation($"{request.Email} Login Success");

        response.Authtoken = authToken;
        return response;
    }

    public string CreateAuthToken()
    {
        const string AllowableCharacters = "abcdefghijklmnopqrstuvwxyz0123456789";

        var bytes = new Byte[25];
        using (var random = RandomNumberGenerator.Create())
        {
            random.GetBytes(bytes);
        }
        return new string(bytes.Select(x => AllowableCharacters[x % AllowableCharacters.Length]).ToArray());
    }
}
  • AccountDb와 RedisDb는 program.cs에서 등록한 서비스에서 생성된 인스턴스를 활용
  • AccountDb의 VerifyAccount() 메서드와 RedisDb의 RegistUser() 메서드를 사용하여 로그인 작업을 진행

 


추가, 수정 및 보완이 필요한점

  • 비밀번호를 입력받은 그대로 사용하기 때문에 보안상 문제가 발생
    > hashing하여 사용하도록 구조 변경
  • 로그를 단순하게 콘솔에 출력중
    > 로그를 파일에 출력되도록 수정하고, 로그의 형식도 좀 더 정돈되도록 변경
  • 로그인 이후의 요청이 발생할시 RegistUser에서 발급받은 AuthToken을 확인하여 인증 과정을 거치도록 추가

 

'개인공부 > Web API 게임 서버 공부' 카테고리의 다른 글

배경지식 - Web 서버 구조  (0) 2023.04.23
배경지식 - ZLogger  (0) 2023.04.20
배경지식 - Redis  (0) 2023.04.20
배경지식 - ORM  (1) 2023.04.20
배경지식 - C#  (0) 2023.04.20

댓글