[] JWT 인증 방식 도입, 로그인, 아카데미 API 에 JWT 인증 도입해서 로직 변경

This commit is contained in:
김선규 2025-02-25 17:27:23 +09:00
parent f1a901820f
commit 0e2452207c
5 changed files with 280 additions and 178 deletions

View File

@ -75,6 +75,7 @@ builder.Services.AddControllers();
// 여기다가 API 있는 컨트롤러들 AddScoped 하면 되는건가? // 여기다가 API 있는 컨트롤러들 AddScoped 하면 되는건가?
builder.Services.AddScoped<AcaMate.Common.Token.JwtTokenService>(); builder.Services.AddScoped<AcaMate.Common.Token.JwtTokenService>();
builder.Services.AddScoped<IRepositoryService, AcaMate.V1.Services.RepositoryService>();
// builder.Services.AddScoped<UserService>(); // // builder.Services.AddScoped<UserService>(); //
// builder.Services.AddScoped<UserController>(); // builder.Services.AddScoped<UserController>();

View File

@ -17,13 +17,16 @@ namespace AcaMate.Common.Token;
public class JwtTokenService public class JwtTokenService
{ {
private readonly JwtSettings _jwtSettings; private readonly JwtSettings _jwtSettings;
private readonly ILogger<JwtTokenService> _logger;
public JwtTokenService(IOptions<JwtSettings> jwtSettings) public JwtTokenService(IOptions<JwtSettings> jwtSettings, ILogger<JwtTokenService> logger)
{ {
_jwtSettings = jwtSettings.Value; _jwtSettings = jwtSettings.Value;
_logger = logger;
} }
public string GenerateJwtToken(string uid, string role) public string GenerateJwtToken(string uid)//, string role)
{ {
// 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함 // 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함
var claims = new List<Claim> var claims = new List<Claim>
@ -33,9 +36,8 @@ public class JwtTokenService
// Jti 는 토큰 식별자로 토큰의 고유 ID 이다. // Jti 는 토큰 식별자로 토큰의 고유 ID 이다.
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
// jwt 토큰이 가지는 권한 // jwt 토큰이 가지는 권한
new Claim(ClaimTypes.Role, role), // new Claim(ClaimTypes.Role, role),
// 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin") // 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin")
new Claim(ClaimTypes.NameIdentifier, uid)
}; };
// 2. 비밀 키와 SigningCredentials 생성 // 2. 비밀 키와 SigningCredentials 생성
@ -50,7 +52,7 @@ public class JwtTokenService
expires: DateTime.Now.AddMinutes(_jwtSettings.ExpiryMinutes), expires: DateTime.Now.AddMinutes(_jwtSettings.ExpiryMinutes),
signingCredentials: credentials signingCredentials: credentials
); );
// 4. 토큰 객체를 문자열로 변환하여 반환 // 4. 토큰 객체를 문자열로 변환하여 반환
return new JwtSecurityTokenHandler().WriteToken(token); return new JwtSecurityTokenHandler().WriteToken(token);
} }
@ -102,6 +104,8 @@ public class JwtTokenService
Console.WriteLine($"검증 실패 {e}"); Console.WriteLine($"검증 실패 {e}");
return null; return null;
} }
} }

View File

@ -27,6 +27,13 @@ public class RefreshToken
// 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다. // 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다.
public DateTime? revoke_Date { get; set; } public DateTime? revoke_Date { get; set; }
} }
public class ValidateToken
{
public string token { get; set; }
public string refresh { get; set; }
public string uid { get; set; }
}
/* /*
""" """

View File

@ -1,19 +1,21 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using AcaMate.Common.Data;
using AcaMate.Common.Models;
using AcaMate.V1.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions; using System.Linq.Expressions;
using AcaMate.V1.Services;
using AcaMate.Common.Token;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using MySqlConnector; using MySqlConnector;
using System.Linq; using System.Linq;
using AcaMate.Common.Data;
using AcaMate.Common.Token;
using AcaMate.Common.Models;
using AcaMate.V1.Models;
using AcaMate.V1.Services;
namespace AcaMate.V1.Controllers; namespace AcaMate.V1.Controllers;
[ApiController] [ApiController]
@ -24,12 +26,14 @@ public class UserController : ControllerBase
private readonly AppDbContext _dbContext; private readonly AppDbContext _dbContext;
private readonly ILogger<UserController> _logger; private readonly ILogger<UserController> _logger;
private readonly JwtTokenService _jwtTokenService; private readonly JwtTokenService _jwtTokenService;
private readonly IRepositoryService _repositoryService;
public UserController(AppDbContext dbContext, ILogger<UserController> logger, JwtTokenService jwtTokenService) public UserController(AppDbContext dbContext, ILogger<UserController> logger, JwtTokenService jwtTokenService, IRepositoryService repositoryService)
{ {
_dbContext = dbContext; _dbContext = dbContext;
_logger = logger; _logger = logger;
_jwtTokenService = jwtTokenService; _jwtTokenService = jwtTokenService;
_repositoryService = repositoryService;
} }
[HttpGet] [HttpGet]
@ -73,114 +77,136 @@ public class UserController : ControllerBase
[HttpGet("login")] [HttpGet("login")]
[CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")] [CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")]
public IActionResult Login(string acctype, string sns_id) public async Task<IActionResult> Login(string acctype, string sns_id)
{ {
// API 동작 파라미터 입력 값 확인
if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id)) if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id))
return BadRequest(DefaultResponse.InvalidInputError); return BadRequest(DefaultResponse.InvalidInputError);
try try
{ {
var login = _dbContext.Login.FirstOrDefault(l => l.sns_type == acctype && l.sns_id == sns_id); var login = await _dbContext.Login
var uid = ""; .FirstOrDefaultAsync(l => l.sns_type == acctype && l.sns_id == sns_id);
List<string> bids = new List<string>();
if (login != null) if (login != null)
{ {
uid = login.uid; // 로그인 정보가 존재 하는 상황
var uid = login.uid;
var user = _dbContext.User.FirstOrDefault(user => user.uid == uid); var user = await _dbContext.User
.FirstOrDefaultAsync(u => u.uid == uid);
if (user != null) if (user != null)
{ {
// 정상적으로 User 테이블에도 있는것이 확인 됨
user.login_date = DateTime.Now; user.login_date = DateTime.Now;
_dbContext.SaveChanges(); await _dbContext.SaveChangesAsync();
}
// 토큰 생성은 로그인이 이제 되고 나서 한다.
// 토큰 생성은 로그인 부분에서 하는거지 var accessToken = _jwtTokenService.GenerateJwtToken(uid);//, "Normal");
var accessToken = _jwtTokenService.GenerateJwtToken(uid, "Normal"); var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); _logger.LogInformation($"{uid}: {accessToken}, {refreshToken}");
_dbContext.RefreshTokens.Add(refreshToken);
_dbContext.SaveChanges(); await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
var userAcademy = _dbContext.UserAcademy.Where(u => u.uid == uid).ToList(); var response = new APIResponseStatus<dynamic>
foreach (var userData in userAcademy)
{
_logger.LogInformation($"uid: {userData.uid} || bid: {userData.bid}");
bids.Add(userData.bid);
}
var response = new APIResponseStatus<dynamic>
{
status = new Status
{ {
code = "000", status = new Status
message = "정상" {
}, code = "000",
data = new message = "정상"
{ },
uid = $"{uid}", data = new
bid = bids {
} token = accessToken,
}; refresh = refreshToken.refresh_token
}
return Ok(response.JsonToString()); };
return Ok(response.JsonToString());
}
} }
else // case 1: Login 테이블에 값이 없다 == 로그인이 처음
// case 2: User 테이블에 값이 없다 == 이건 문제가 있는 상황 -> 해결은 회원가입 재 진행 시도
// Login에는 있는데 User 테이블에 없다? 말이 안되긴 하는데...
return Ok(new APIResponseStatus<dynamic>
{ {
// 계정이 없다는 거 status = new Status
var response = new APIResponseStatus<dynamic>
{ {
status = new Status code = "010",
{ message = "로그인 정보 없음 > 회원 가입 진행"
code = "010", },
message = "정상" data = new
}, {
data = new token = "",
{ refresh = ""
uid = "", // bidList = new string[] { }
bid = new string[] { } }
} }.JsonToString());
};
return Ok(response.JsonToString());
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogInformation($"[로그인] 에러 발생 : {ex}");
return StatusCode(500, DefaultResponse.UnknownError); return StatusCode(500, DefaultResponse.UnknownError);
} }
} }
[HttpPost("academy")] [HttpGet("academy")]
[CustomOperation("학원 리스트 확인", "등록된 학원 리스트 확인", "사용자")] [CustomOperation("학원 리스트 확인", "사용자가 등록된 학원 리스트 확인", "사용자")]
public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) public async Task<IActionResult> ReadAcademyInfo(string token, string refresh)
{ {
if (!request.bids.Any()) _logger.LogInformation($"토큰 : {token}, {refresh}");
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh))
{ {
var error = DefaultResponse.InvalidInputError; var error = DefaultResponse.InvalidInputError;
return Ok(error); return Ok(error);
} }
var academies = _dbContext try
.Academy
.Where(a => request.bids.Contains(a.bid))
.Select(a => new AcademyName
{
bid = a.bid,
name = a.business_name
})
.ToList();
var response = new APIResponseStatus<List<AcademyName>>
{ {
status = new Status var validateToken = await _repositoryService.ValidateToken(token, refresh);
var uid = validateToken.uid;
var userAcademy = await _dbContext.UserAcademy
.Where(ua => ua.uid == uid)
.Select(ua => ua.bid)
.ToListAsync();
var academies = await _dbContext.Academy
.Where(a => userAcademy.Contains(a.bid))
.Select(a => new AcademyName
{
bid = a.bid,
name = a.business_name
})
.ToListAsync();
var response = new APIResponseStatus<List<AcademyName>>
{ {
code = "000", status = new Status
message = "정상" {
}, code = "000",
data = academies message = "정상"
}; },
return Ok(response); data = academies
};
return Ok(response);
}
catch (SecurityTokenException tokenEx)
{
_logger.LogInformation($"[로그인][오류] 토큰 검증 : {tokenEx}");
return StatusCode(500, DefaultResponse.InvalidInputError);
}
catch (Exception ex)
{
_logger.LogInformation($"[로그인][오류] 에러 발생 : {ex}");
return StatusCode(500, DefaultResponse.UnknownError);
}
} }
[HttpPost("register")] [HttpPost("register")]
@ -231,41 +257,18 @@ public class UserController : ControllerBase
}; };
if (await SaveData<User, string>(user, u => u.uid)) if (await _repositoryService.SaveData<User, string>(user, u => u.uid))
{ {
await SaveData<Login, string>(login, l => l.sns_id); await _repositoryService.SaveData<Login, string>(login, l => l.sns_id);
await SaveData<Permission, string>(permission, p => p.uid); await _repositoryService.SaveData<Permission, string>(permission, p => p.uid);
await SaveData<Contact, string>(contact, c => c.uid); await _repositoryService.SaveData<Contact, string>(contact, c => c.uid);
} }
// TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함
var token = _jwtTokenService.GenerateJwtToken(uid, "admin"); var token = _jwtTokenService.GenerateJwtToken(uid);//, "admin");
var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); var refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid); await _repositoryService.SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
/* */
var principalToken = _jwtTokenService.ValidateToken(token);
if (principalToken != null)
{
var jti = principalToken.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
var sub = principalToken.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
var id = principalToken.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var id2 = principalToken.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
// var check = principalToken.FindFirst("sub")?.Value;
_logger.LogInformation($"토큰? - {jti}");
_logger.LogInformation($"토큰? - {sub}");
_logger.LogInformation($"토큰? - {id}");
// TODO: 도대체 토큰 이거 sub확인이 왜 안되는걸까.?
}
else
{
_logger.LogInformation("dd");
}
/* */
var result = new APIResponseStatus<dynamic>() var result = new APIResponseStatus<dynamic>()
{ {
status = new Status() status = new Status()
@ -275,7 +278,6 @@ public class UserController : ControllerBase
}, },
data = new data = new
{ {
uid = uid,
accessToken = token, accessToken = token,
refreshToken = refreshToken.refresh_token refreshToken = refreshToken.refresh_token
} }
@ -283,75 +285,27 @@ public class UserController : ControllerBase
return Ok(result.JsonToString()); return Ok(result.JsonToString());
} }
private async Task<bool> SaveData<T, K> (T entity, Expression<Func<T, K>> key) where T : class
{
try
{
var value = key.Compile()(entity);
// x 라 함은 Expression 으로 생성되는 트리에서 T 타입으의 매개변수를 지칭함
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var dbSet = _dbContext.Set<T>();
var entityData = await dbSet.FirstOrDefaultAsync(predicate);
if (entityData != null)
{
_logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 [{value}]: 계속");
var entry = _dbContext.Entry(entityData);
entry.CurrentValues.SetValues(entity);
if (entry.Properties.Any(p => p.IsModified))
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 존재: 계속");
}
else
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 없음: 종료");
return true;
}
}
else
{
dbSet.Add(entity);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB 저장 완료: 종료");
return true;
}
catch (Exception ex)
{
_logger.LogInformation($"[{typeof(T)}] 알 수 없는 오류: 종료\n{ex}");
return false;
}
}
[HttpGet("logout")] [HttpGet("logout")]
[CustomOperation("로그아웃", "사용자 로그아웃", "사용자")] [CustomOperation("로그아웃", "사용자 로그아웃", "사용자")]
public async Task<IActionResult> LogOut(string token, string refresh)//([FromBody] UserAll request) public async Task<IActionResult> LogOut(string token, string refresh) //([FromBody] UserAll request)
{ {
var principalToken = _jwtTokenService.ValidateToken(token); /* */
if (principalToken != null) // var value = await ValidateToken(token, refresh);
{ // _logger.LogInformation(value.uid);
// var uid = principalToken.FindFirst(JwtRegisteredClaimNames.Jti)?.Value; // _logger.LogInformation(value.refresh);
var uid = principalToken.FindFirst("sub")?.Value; // _logger.LogInformation(value.token);
_logger.LogInformation($"토큰? - {uid}"); /* */
}
else
{
_logger.LogInformation("dd");
}
return Ok("로그아웃"); return Ok("로그아웃");
} }
} }

View File

@ -0,0 +1,136 @@
using AcaMate.Common.Data;
using AcaMate.Common.Token;
using AcaMate.Common.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
namespace AcaMate.V1.Services;
public interface IRepositoryService
{
Task<bool> SaveData<T, K>(T entity, Expression<Func<T, K>> key) where T : class;
Task<ValidateToken> ValidateToken(string token, string refresh);
}
public class RepositoryService: IRepositoryService
{
private readonly AppDbContext _dbContext;
private readonly ILogger<RepositoryService> _logger;
private readonly JwtTokenService _jwtTokenService;
public RepositoryService(AppDbContext dbContext, ILogger<RepositoryService> logger, JwtTokenService jwtTokenService)
{
_dbContext = dbContext;
_logger = logger;
_jwtTokenService = jwtTokenService;
}
public async Task<bool> SaveData<T, K> (T entity, Expression<Func<T, K>> key) where T : class
{
try
{
var value = key.Compile()(entity);
// x 라 함은 Expression 으로 생성되는 트리에서 T 타입으의 매개변수를 지칭함
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var dbSet = _dbContext.Set<T>();
var entityData = await dbSet.FirstOrDefaultAsync(predicate);
if (entityData != null)
{
_logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 [{value}]: 계속");
var entry = _dbContext.Entry(entityData);
entry.CurrentValues.SetValues(entity);
if (entry.Properties.Any(p => p.IsModified))
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 존재: 계속");
}
else
{
_logger.LogInformation($"[{typeof(T)}] 변경사항 없음: 종료");
return true;
}
}
else
{
_logger.LogInformation($"[{typeof(T)}] 처음등록");
dbSet.Add(entity);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB 저장 완료: 종료");
return true;
}
catch (Exception ex)
{
_logger.LogInformation($"[{typeof(T)}] 알 수 없는 오류: 종료 {ex}");
return false;
}
}
//토큰 태울때는 인코딩 된 걸로 태워야지 원본꺼 태우면 데이터에 손상옵니다.
public async Task<ValidateToken> ValidateToken(string token, string refresh)
{
var principalToken = _jwtTokenService.ValidateToken(token);
if (principalToken != null)
{
var uid = principalToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
_logger.LogInformation($"토큰 변환 - {uid}");
return new ValidateToken
{
token = token,
refresh = refresh,
uid = uid
};
}
else
{
_logger.LogInformation("엑세스 토큰 만료");
var refreshToken = await _dbContext.RefreshTokens
.FirstOrDefaultAsync(t => t.refresh_token == refresh);
if (refreshToken == null)
{
throw new SecurityTokenException("리프레시 토큰도 잘못되었음");
}
var uid = refreshToken.uid;
if (refreshToken.expire_date > DateTime.Now)
{
_logger.LogInformation($"리프레시 : {uid}");
var access = _jwtTokenService.GenerateJwtToken(uid);
return new ValidateToken
{
token = access,
refresh = refreshToken.refresh_token,
uid = uid
};
}
else
{
refreshToken = _jwtTokenService.GenerateRefreshToken(uid);
_logger.LogInformation("리프레시 토큰 만료");
await SaveData<RefreshToken, string>(refreshToken, rt => rt.uid);
return new ValidateToken
{
token = token,
refresh = refreshToken.refresh_token,
uid = uid
};
}
}
}
}