Program.cs
// WebApplication 클래스의 CreateBuilder() 메서드를 호출하여 builder 인스턴스 생성
// 이 객체는 애플리케이션의 구성을 빌드하는데 사용
var builder = WebApplication.CreateBuilder(args);
// 애플리케이션에 컨트롤러를 추가한다
// 웹 애플리케이션에서 컨트롤러를 사용하기 위한 서비스를 등록하는 역할
// 컨트롤러는 HTTP 요청에 대한 응답을 생성하는데 사용되며,
// 애플리케이션에서 사용할 수 있는 엔드포인트를 정의
builder.Services.AddControllers();
// WebApplicationBuilder 객체를 사용하여 애플리케이션을 빌드하고
// WebApplication 객체를 반환
var app = builder.Build();
// ASP.NET Core의 라우팅 기능을 사용하기 위해 UseRouting() 메서드를 호출
app.UseRouting();
// 라우팅이 완료되면, 애플리케이션의 엔드포인트를 정의하기 위해 UseEndpoints() 메서드를 호출한다.
// 여기에서는 컨트롤러를 매핑하기 위해 MapControllers() 메서드를 호출한다.
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
// WebApplication 객체의 Configuration 속성을 사용하여
// appsettings.json 파일과 같은 구성 파일에서 설정을 로드한다.
IConfiguration configuration = app.Configuration;
// DBManager 클래스의 Init() 메서드를 호출하여 데이터베이스 연결을 초기화한다.
DBManager.Init(configuration);
// WebApplication 객체의 Run() 메서드를 호출하여 애플리케이션을 실행
// 구성 파일에서 읽은 서버 주소를 사용한다.
app.Run(configuration["ServerAddress"]);
DBManager.cs
using CloudStructures; // CloudStructures는 Redis서비스를 사용하기 위한 라이브러리
using MySqlConnector; // MySqlConnector는 MySQl데이터베이스에 접근하기 위한 라이브러리
using SqlKata.Execution; // SqlKata는 SQL 쿼리 빌더와 실행 라이브러리
public class DBManager
{
static string GameDBConnectString; //MySQL데이터베이스 연결 문자열을 저장하는 정적 변서
static string RedisAddress; //Redis 서버 주소를 저장하는 정적 변수
// Redis 서버와 연결을 가져오거나 설정할수 있는 RedisConnection 객체를 정적 프로퍼티로 선언
public static RedisConnection RedisConn { get; set; }
// 애플리케이션의 구성 파일에서 데이터베이스와 Redis 연결 정보를 읽어들이고
// RedisConnection 객체를 초기화하는 메서드
public static void Init(IConfiguration configuration)
{
// appsettings.json 파일에서 DBConnection 섹션의 MySqlGame 값을 읽어서
// GameDBConnectString 변수에 저장
GameDBConnectString = configuration.GetSection("DBConnection")["MySqlGame"];
// appsettings.json 파일에서 DBConnection 섹션의 Redis 값을 읽어서
// RedisAddress 변수에 저장
RedisAddress = configuration.GetSection("DBConnection")["Redis"];
// RedisConfig 객체를 생성
// 이 객체는 Redis 서버 연결 정보를 설정하는데 사용
var config = new RedisConfig("basic", RedisAddress);
// RedisConfig 객체를 사용하여 RedisConnection 객체를 초기화
RedisConn = new RedisConnection(config);
}
// MySQL 데이터베이스와 연결하여 SqlKata 라이브러리의 QueryFactory 객체를 반환하는 비동기 함수
public static async Task<QueryFactory> GetGameDBQuery()
{
// MySqlConnection 객체를 생성하고
// GameDBConnectString 변수의 값으로 MySQL 데이터베이스에 연결
var connection = new MySqlConnection(GameDBConnectString);
// 데이터베이스 연결을 비동기적으로 연다.
await connection.OpenAsync();
// MySQL 데이터베이스에서 실행할 SQL 쿼리를 컴파일할 MySqlCompiler 객체를 생성
var compiler = new SqlKata.Compilers.MySqlCompiler();
// MySqlConnection 객체와 MySqlCompiler 객체를 사용하여
// QueryFactory 객체를 생성
var queryFactory = new SqlKata.Execution.QueryFactory(connection, compiler);
// QueryFactory 객체를 반환
return queryFactory;
}
Security.cs
using System.Security.Cryptography;
using System.Text;
public class Security
{
// "abcdefghijklmnopqrstuvwxyz0123456789" 문자열로 초기화되는 상수
// 이 문자열은 salt 값과 인증 토큰을 생성할 때 사용
private const String AllowableCharacters = "abcdefghijklmnopqrstuvwxyz0123456789";
// alt 값과 비밀번호를 받아들이고, 이를 SHA-256 알고리즘을 사용하여 해싱된 비밀번호를 반환
public static String MakeHashingPassWord(String saltValue, String pw)
{
// HA256.Create()을 사용하여 SHA-256 해싱 객체 생성
var sha = SHA256.Create();
// sha.ComputeHash(Encoding.ASCII.GetBytes(saltValue + pw))는
// 입력 문자열의 해시 값을 계산한다
var hash = sha.ComputeHash(Encoding.ASCII.GetBytes(saltValue + pw));
// stringBuilder 객체를 사용하여 해시 된 바이트를 문자열로 변환하고 반환
var stringBuilder = new StringBuilder();
// hash 배열의 각 요소를 16진수 문자열로 변환하여 stringBuilder에 추가하는 반복문
// {0:x2} 문자열 형식 지정자는 현재 반복에서 처리중인 바이트를 16진수로 표현
// 2자리로 패딩하여 나타내도록 지정
// 0xAB 바이트를 처리 할 때, 결과 문자열은 "ab"
// 반복문의 결과로 hash 배열의 모든 요소를 16진수 문자열로 표현한 stringBuilder 객체가 반환
foreach (var b in hash)
{
stringBuilder.AppendFormat("{0:x2}", b);
}
return stringBuilder.ToString();
}
// 64 바이트의 난수 배열을 생성
// 이 배열을 사용하여 salt 문자열 만든다
public static String SaltString()
{
var bytes = new Byte[64];
// RandomNumberGenerator.Create()을 사용하여 임의의 바이트 시퀀스를 생성
using (var random = RandomNumberGenerator.Create())
{
random.GetBytes(bytes);
}
// AllowableCharacters의 문자 중에서 랜덤으로 선택하여 인증 토큰 문자열을 만듬
return new String(bytes.Select(x => AllowableCharacters[x % AllowableCharacters.Length]).ToArray());
}
// 길이가 25 바이트인 임의의 바이트 배열을 생성하고,
// 이를 AllowableCharacters 문자열에서 선택된 문자로 변환하여 반환
public static String CreateAuthToken()
{
var bytes = new Byte[25];
// RandomNumberGenerator 클래스를 사용하여
// 무작위로 생성 된 바이트 값을 bytes 배열에 채운다
using (var random = RandomNumberGenerator.Create())
{
// 세 번째 줄에서는 bytes 배열의 각 바이트 값을 AllowableCharacters 문자열의 인덱스로 변환
// x % AllowableCharacters.Length는 AllowableCharacters 문자열의 길이로 나눈 나머지를 구하여,
// 이 값을 AllowableCharacters 문자열에서의 인덱스로 사용
// 이를 통해 bytes 배열의 각 요소는 AllowableCharacters 문자열에서 유효한 인덱스를 가진다
random.GetBytes(bytes);
}
// Select 메서드와 ToArray 메서드를 사용하여 각 바이트 값을 AllowableCharacters 문자열에서 선택된 문자로 변환
// 하나의 문자열로 결합하여 반환
return new String(bytes.Select(x => AllowableCharacters[x % AllowableCharacters.Length]).ToArray());
}
}
ErrorCode.cs
// ErrorCode라는 이름의 열거형 정의
// 연관된 상수들을 하나의 자료형으로 정의하는 방법
// 에러 코드를 열거형으로 정의하면 에러를 쉽게 구분하고,
// 코드 작성시 실수를 방지 할 수 있다.
// 이러한 에러 코드를 반환하는 것은 API의 에러 응답 등에 활용될 수 있다
public enum ErrorCode : int
{
None = 0,
Create_Account_Fail_Duplicate = 11,
Create_Account_Fail_Exception = 12,
Login_Fail_NotUser = 16,
Login_Fail_PW = 17,
}
LoginController.cs
using CloudStructures.Structures;
using Microsoft.AspNetCore.Mvc;
using SqlKata.Execution;
[ApiController]
[Route("[controller]")]
public class Login : ControllerBase
{
// HTTP POST 요청에 대한 핸들러 선언
// PkLoginRequest 객체가 전송되며, 핸들러는 PkLoginREsponse객체를 반환한다.
[HttpPost]
public async Task<PkLoginResponse> Post(PkLoginRequest request)
{
// 새로운 PkLoginResponse 객체 생성
var response = new PkLoginResponse();
// ErrorCode.None으로 결과 속성을 초기화
response.Result = ErrorCode.None;
// DBManager.GetGameDBQuery()메서드를 사용하여 데이터 베이스 커넥션을 가져온다
// account 테이블에서 이메일 주소가 request.Email인 사용자 정보를 가져온다.
// 가져온 사용자 정보에 대해 해싱된 비밀번호를 만들고,
// 이 비밀번호가 사용자 정보와 일치하는지 확인한다.
// 사용자 정보나 해싱된 비밀번호가 없는 경우,
// 로그인 실패 코드를 반환함
// 마지막으로, 데이터베이스 커넥션을 폐기한다.
using (var db = await DBManager.GetGameDBQuery())
{
var userInfo = await db.Query("account").Where("Email", request.Email).FirstOrDefaultAsync<DBUserInfo>();
if (userInfo == null || string.IsNullOrEmpty(userInfo.HashedPassword))
{
response.Result = ErrorCode.Login_Fail_NotUser;
return response;
}
var hashingPassword = Security.MakeHashingPassWord(userInfo.SaltValue, request.Password);
Console.WriteLine($"[Request Login] Email:{request.Email}, request.Password:{request.Password}, saltValue:{userInfo.SaltValue}, hashingPassword:{hashingPassword}");
if (userInfo.HashedPassword != hashingPassword)
{
response.Result = ErrorCode.Login_Fail_PW;
return response;
}
db.Dispose();
}
// ecurity.CreateAuthToken() 메소드를 사용하여 인증 토큰(tokenValue)을 생성한 후,
// Redis 서버에 해당 토큰 값을 저장
// Security.CreateAuthToken() 메서드를 호출하여, tokenValue 변수에 저장
string tokenValue = Security.CreateAuthToken();
var idDefaultExpiry = TimeSpan.FromDays(1);
// Redis 서버에 해당 이메일 주소(request.Email)를 키로 사용하여 RedisString<string> 객체를 생성
// 이 객체는 Redis 서버에서 해당 키에 대한 값을 문자열로 저장하고,
// 만료 시간(idDefaultExpiry)을 1일로 지정하여 만료 시간이 지난 후 해당 값이 자동으로 삭제되도록 설정
var redisId = new RedisString<string>(DBManager.RedisConn, request.Email, idDefaultExpiry);
// SetAsync() 메소드를 사용해 Redis 서버에 해당 이메일 주소를 키로 사용하여 인증 토큰 값을 저장
await redisId.SetAsync(tokenValue);
// response.Authtoken 속성에 tokenValue 값을 할당
response.Authtoken = tokenValue;
// response 객체를 반환
return response;
// 인증 토큰이 생성되고, 해당 토큰 값이 Redis 서버에 저장되며,
// 클라이언트에게도 해당 토큰 값이 반환
}
}
public class PkLoginRequest
{
public string? Email { get; set; }
public string? Password { get; set; }
}
public class PkLoginResponse
{
public ErrorCode Result { get; set; }
public string Authtoken { get; set; }
}
class DBUserInfo
{
public string Email { get; set; }
public string HashedPassword { get; set; }
public string SaltValue { get; set; }
}