하고싶은건많은놈 2023. 4. 24. 14:37

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을 확인하여 인증 과정을 거치도록 추가