From 9cdb4867854fedd9fb87e6be2b089f3aa2b90d89 Mon Sep 17 00:00:00 2001 From: "seonkyu.kim" Date: Tue, 18 Feb 2025 01:16:17 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[=F0=9F=91=B7=F0=9F=8F=BB]=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85(Base)=20=EC=88=98=EC=A0=95=206=EC=B0=A8=20-=20?= =?UTF-8?q?=EB=A1=9C=EC=BB=AC=EC=97=90=EC=84=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: seonkyu.kim --- Program.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Program.cs b/Program.cs index 3637a7d..d44c67c 100644 --- a/Program.cs +++ b/Program.cs @@ -88,7 +88,8 @@ builder.Services.AddCors(option => }); }); - +// 로컬 테스트 위한 부분 +builder.WebHost.UseUrls("http://0.0.0.0:5144"); ///// ===== builder 설정 부 ===== ///// var app = builder.Build(); @@ -106,8 +107,9 @@ else app.UseHsts(); } +// 로컬 테스트 위한 부분 -app.UseHttpsRedirection(); +// app.UseHttpsRedirection(); app.UseRouting(); // app.MapControllers(); From 41c8d940016b2366e793f00cc729193cbe274912 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Tue, 18 Feb 2025 17:30:20 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[=E2=9C=A8]=20=EC=95=84=EC=B9=B4=EB=8D=B0?= =?UTF-8?q?=EB=AF=B8=20=EC=A0=95=EB=B3=B4=20API=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 2 +- Program/V1/Controllers/MemberController.cs | 39 ++++++++++++++++++++-- Program/V1/Models/Academy.cs | 13 +++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Program.cs b/Program.cs index d44c67c..9bc14f9 100644 --- a/Program.cs +++ b/Program.cs @@ -108,8 +108,8 @@ else } // 로컬 테스트 위한 부분 - // app.UseHttpsRedirection(); + app.UseRouting(); // app.MapControllers(); diff --git a/Program/V1/Controllers/MemberController.cs b/Program/V1/Controllers/MemberController.cs index 1bbfcd2..a45dacc 100644 --- a/Program/V1/Controllers/MemberController.cs +++ b/Program/V1/Controllers/MemberController.cs @@ -2,7 +2,7 @@ using System.Text.Json; using AcaMate.Common.Data; using AcaMate.Common.Models; - +using AcaMate.V1.Models; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -13,6 +13,13 @@ namespace AcaMate.V1.Controllers; [ApiExplorerSettings(GroupName = "사업자 정보")] public class MemberController: ControllerBase { + private readonly AppDbContext _dbContext; + + public MemberController(AppDbContext dbContext) + { + _dbContext = dbContext; + } + [HttpGet("business")] public IActionResult GetBusinessData() { @@ -20,10 +27,38 @@ public class MemberController: ControllerBase return Ok("DB 참조"); } + [HttpPost("academy")] + [CustomOperation("앱 버전 확인","앱 버전을 확인해서 업데이트 여부 판단", "시스템")] + public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) + { + if (request.bids == null || !request.bids.Any()) + { + var response = DefaultResponse.InvalidInputError; + return Ok(response); + } - [HttpGet("/v1/out/member/business")] + var academies = _dbContext + .Academy + .Where(a => request.bids.Contains(a.bid)) + .Select(a => new AcademyName + { + bid = a.bid, + name = a.business_name + }) + .ToList(); + return Ok(academies); + } + + + + + + // -- -- -- -- -- -- -- -- -- -- -- -- // + + [HttpGet("/api/v1/out/member/business")] public IActionResult SearchBusinessNo() { return Ok("외부 참조"); } + } \ No newline at end of file diff --git a/Program/V1/Models/Academy.cs b/Program/V1/Models/Academy.cs index 947c2c7..e097311 100644 --- a/Program/V1/Models/Academy.cs +++ b/Program/V1/Models/Academy.cs @@ -9,10 +9,21 @@ public class Academy [Key] public string bid { get; set; } public string business_name { get; set; } - public string business_ownder { get; set; } + public string business_owner { get; set; } public string business_number { get; set; } public DateTime business_date { get; set; } public string business_address { get; set; } public string business_contact { get; set; } public string uid { get; set; } +} + +public class AcademyName +{ + public string bid { get; set; } + public string name { get; set; } +} + +public class RequestAcademy +{ + public List bids { get; set; } } \ No newline at end of file From b2e72a2f9c08031795f33c8569dde704edd004ef Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Wed, 19 Feb 2025 17:22:50 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[=E2=9C=A8]=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20API=20=EC=B6=94=EA=B0=80=20+=20=EC=95=84=EC=B9=B4?= =?UTF-8?q?=EB=8D=B0=EB=AF=B8=20=EC=9D=B4=EB=A6=84=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program/Common/Data/AppDbContext.cs | 1 + Program/Common/Model/Status.cs | 2 +- Program/V1/Controllers/MemberController.cs | 21 ------ Program/V1/Controllers/UserController.cs | 77 +++++++++++++++++++++- Program/V1/Models/{Login.cs => User.cs} | 18 ++++- 5 files changed, 94 insertions(+), 25 deletions(-) rename Program/V1/Models/{Login.cs => User.cs} (62%) diff --git a/Program/Common/Data/AppDbContext.cs b/Program/Common/Data/AppDbContext.cs index f3832c9..f03f11c 100644 --- a/Program/Common/Data/AppDbContext.cs +++ b/Program/Common/Data/AppDbContext.cs @@ -17,6 +17,7 @@ public class AppDbContext: DbContext //MARK: USER public DbSet Login { get; set; } public DbSet UserAcademy { get; set; } + public DbSet User { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Program/Common/Model/Status.cs b/Program/Common/Model/Status.cs index 18752e1..593d759 100644 --- a/Program/Common/Model/Status.cs +++ b/Program/Common/Model/Status.cs @@ -2,7 +2,7 @@ using System.Text.Json; namespace AcaMate.Common.Models; -public class APIResponseStatus +public class APIResponseStatus { public Status status { get; set; } public T? data { get; set; } diff --git a/Program/V1/Controllers/MemberController.cs b/Program/V1/Controllers/MemberController.cs index a45dacc..bf8d442 100644 --- a/Program/V1/Controllers/MemberController.cs +++ b/Program/V1/Controllers/MemberController.cs @@ -27,27 +27,6 @@ public class MemberController: ControllerBase return Ok("DB 참조"); } - [HttpPost("academy")] - [CustomOperation("앱 버전 확인","앱 버전을 확인해서 업데이트 여부 판단", "시스템")] - public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) - { - if (request.bids == null || !request.bids.Any()) - { - var response = DefaultResponse.InvalidInputError; - return Ok(response); - } - - var academies = _dbContext - .Academy - .Where(a => request.bids.Contains(a.bid)) - .Select(a => new AcademyName - { - bid = a.bid, - name = a.business_name - }) - .ToList(); - return Ok(academies); - } diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index a0af8ad..1a83408 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -19,9 +19,51 @@ public class UserController: ControllerBase _dbContext = dbContext; } + [HttpGet()] + [CustomOperation("회원 정보 조회", "회원 정보 조회", "사용자")] + public IActionResult GetUserData(string uid) + { + if (string.IsNullOrEmpty(uid)) + { + return BadRequest(DefaultResponse.InvalidInputError); + } + + try + { + var user = _dbContext.User + .Where(u => u.uid == uid) + .Select(u => new User + { + uid = u.uid, + name = u.name, + auto_login_yn = u.auto_login_yn, + birth = u.birth, + device_id = u.device_id, + login_date = u.login_date, + type = u.type, + }) + .FirstOrDefault(); + + var response = new APIResponseStatus + { + status = new Status() + { + code = "000", + message = "정상" + }, + data = user + }; + return Ok(response.JsonToString()); + } + catch(Exception ex) + { + return StatusCode(500, DefaultResponse.UnknownError); + } + } + [HttpGet("login")] [CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")] - public IActionResult SNSLogin(string acctype,string sns_id) + public IActionResult SNSLogin(string acctype, string sns_id) { if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id)) @@ -74,4 +116,37 @@ public class UserController: ControllerBase } } + [HttpPost("academy")] + [CustomOperation("학원 리스트 확인","등록된 학원 리스트 확인", "사용자")] + public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) + { + if (!request.bids.Any()) + { + var error = DefaultResponse.InvalidInputError; + return Ok(error); + } + + var academies = _dbContext + .Academy + .Where(a => request.bids.Contains(a.bid)) + .Select(a => new AcademyName + { + bid = a.bid, + name = a.business_name + }) + .ToList(); + + var response = new APIResponseStatus> + { + status = new Status() + { + code = "000", + message = "정상" + }, + data = academies + }; + return Ok(response); + } + + } \ No newline at end of file diff --git a/Program/V1/Models/Login.cs b/Program/V1/Models/User.cs similarity index 62% rename from Program/V1/Models/Login.cs rename to Program/V1/Models/User.cs index eed116d..576dc8f 100644 --- a/Program/V1/Models/Login.cs +++ b/Program/V1/Models/User.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Runtime.InteropServices.JavaScript; namespace AcaMate.V1.Models; @@ -13,7 +14,7 @@ public class Login public string uid {get; set;} [MaxLength(4)] public string sns_type {get; set;} - [MaxLength(255)] + [MaxLength(255)] public string sns_token {get; set;} [MaxLength(100)] public string sns_email {get; set;} @@ -27,4 +28,17 @@ public class User_Academy public string bid { get; set; } public DateTime register_date { get; set; } public bool status { get; set; } -} \ No newline at end of file +} + +[Table("user")] +public class User +{ + [Key] + public string uid { get; set; } + public string name { get; set; } + public DateTime birth { get; set; } + public string type { get; set; } + public string device_id { get; set; } + public int auto_login_yn { get; set; } + public DateTime login_date { get; set; } +} From cebf5a42cb09fdde3977482cbd1cc74b6c09d10a Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Thu, 20 Feb 2025 17:47:51 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[[=E2=9C=A8]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8(=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=A1=B0=ED=9A=8C)=20=EB=B6=80=EB=B6=84=20AP?= =?UTF-8?q?I=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 10 ++- Program/V1/Controllers/UserController.cs | 79 +++++++++++++++++------- Program/V1/Models/Academy.cs | 2 + Program/V1/Models/User.cs | 77 +++++++++++++++++++++-- 4 files changed, 141 insertions(+), 27 deletions(-) diff --git a/Program.cs b/Program.cs index 9bc14f9..a48805d 100644 --- a/Program.cs +++ b/Program.cs @@ -38,7 +38,15 @@ var dbString = builder.Configuration.GetConnectionString("MariaDbConnection"); var userString = builder.Configuration.GetConnectionString("DBAccount"); // JWT 설정부 시작 -builder.Configuration.AddJsonFile("private/jwtSetting.json", optional: true, reloadOnChange: true); +if (builder.Environment.IsDevelopment()) +{ + builder.Configuration.AddJsonFile("private/jwtSetting.Development.json", optional: true, reloadOnChange: true); +} +else +{ + builder.Configuration.AddJsonFile("private/jwtSetting.json", optional: true, reloadOnChange: true); +} + builder.Services.Configure(builder.Configuration.GetSection("JwtSettings")); builder.Services.AddAuthentication(options => diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index 1a83408..d2c8c99 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -3,7 +3,8 @@ using System.Text.Json; using AcaMate.Common.Data; using AcaMate.Common.Models; using AcaMate.V1.Models; - +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.IdentityModel.Tokens; namespace AcaMate.V1.Controllers; @@ -63,7 +64,7 @@ public class UserController: ControllerBase [HttpGet("login")] [CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")] - public IActionResult SNSLogin(string acctype, string sns_id) + public IActionResult Login(string acctype, string sns_id) { if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id)) @@ -74,41 +75,61 @@ public class UserController: ControllerBase try { var login = _dbContext.Login.FirstOrDefault(l => l.sns_type == acctype && l.sns_id == sns_id); - string uid = ""; List bids = new List(); if (login != null) { uid = login.uid; - var userAcademy = _dbContext.UserAcademy.Where(u => u.uid == uid).ToList(); + var user = _dbContext.User.FirstOrDefault(user => user.uid == uid); + if (user != null) + { + user.login_date = DateTime.Now; + _dbContext.SaveChanges(); + } + + var userAcademy = _dbContext.UserAcademy.Where(u => u.uid == uid).ToList(); foreach(User_Academy userData in userAcademy) { Console.WriteLine($"uid: {userData.uid} || bid: {userData.bid}"); bids.Add(userData.bid); } + + var response = new APIResponseStatus + { + status = new Status() + { + code = "000", + message = "정상" + }, + data = new + { + uid = $"{uid}", + bid = bids + } + }; + + return Ok(response.JsonToString()); } else { - return StatusCode(002, DefaultResponse.NotFoundError); + // 계정이 없다는 거 + var response = new APIResponseStatus + { + status = new Status() + { + code = "010", + message = "정상" + }, + data = new + { + uid = "", + bid = new string[]{} + } + }; + return Ok(response.JsonToString()); } - - var response = new APIResponseStatus - { - status = new Status() - { - code = "000", - message = "정상" - }, - data = new - { - uid = $"{uid}", - bid = bids - } - }; - - return Ok(response.JsonToString()); } catch (Exception ex) { @@ -148,5 +169,19 @@ public class UserController: ControllerBase return Ok(response); } - + [HttpPost("register")] + [CustomOperation("회원 가입", "사용자 회원 가입", "사용자")] + public IActionResult UserRegister([FromBody] User request) + { + if (request.uid.IsNullOrEmpty()) + { + var error = DefaultResponse.InvalidInputError; + return Ok(error); + } + else + { + return Ok("회원가입"); + } + } + } \ No newline at end of file diff --git a/Program/V1/Models/Academy.cs b/Program/V1/Models/Academy.cs index e097311..8f8cc02 100644 --- a/Program/V1/Models/Academy.cs +++ b/Program/V1/Models/Academy.cs @@ -17,6 +17,8 @@ public class Academy public string uid { get; set; } } +// -- -- -- -- -- DB 테이블 -- -- -- -- -- // + public class AcademyName { public string bid { get; set; } diff --git a/Program/V1/Models/User.cs b/Program/V1/Models/User.cs index 576dc8f..c1c0871 100644 --- a/Program/V1/Models/User.cs +++ b/Program/V1/Models/User.cs @@ -14,10 +14,6 @@ public class Login public string uid {get; set;} [MaxLength(4)] public string sns_type {get; set;} - [MaxLength(255)] - public string sns_token {get; set;} - [MaxLength(100)] - public string sns_email {get; set;} } [Table("user_academy")] @@ -42,3 +38,76 @@ public class User public int auto_login_yn { get; set; } public DateTime login_date { get; set; } } + + +[Table("permission")] +public class Permission +{ + [Key] + public string uid { get; set; } + + public bool location_yn {get; set;} + public bool camera_yn {get; set;} + public bool photo_yn {get; set;} + public bool push_yn {get; set;} + public bool market_app_yn {get; set;} + public bool market_sms_yn {get; set;} + public bool market_email_yn {get; set;} +} + +[Table("token")] +public class Token +{ + [Key] + public string uid { get; set; } + public string refresh_token { get; set; } + public DateTime create_date { get; set; } + public DateTime expires_date { get; set; } + public DateTime revoke_date { get; set; } +} + +[Table("location")] +public class Location +{ + [Key] + public string uid { get; set; } + public string lat { get; set; } + public string lng { get; set; } +} + +[Table("contact")] +public class Contact +{ + [Key] + public string uid { get; set; } + public string email { get; set; } + public string phone { get; set; } +} + +// -- -- -- -- -- DB 테이블 -- -- -- -- -- // + +public class UserAll +{ + public string uid { get; set; } + public string name { get; set; } + public DateTime birth { get; set; } + public string type { get; set; } + public string device_id { get; set; } + public int auto_login_yn { get; set; } + public DateTime login_date { get; set; } + + public string email { get; set; } + public string phone { get; set; } + + public bool location_yn {get; set;} + public bool camera_yn {get; set;} + public bool photo_yn {get; set;} + public bool push_yn {get; set;} + public bool market_app_yn {get; set;} + public bool market_sms_yn {get; set;} + public bool market_email_yn {get; set;} + + public string sns_id {get; set;} + public string sns_type {get; set;} + public string sns_email {get; set;} +} \ No newline at end of file From 55f40e56cfd2bbc8eee9c56cfbbc3429b16ebb7e Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Fri, 21 Feb 2025 17:53:32 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[=E2=9C=A8]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=ED=9A=8C=EC=9B=90=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20API=20&=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20API=20=EB=A7=8C=EB=93=9C=EB=8A=94=20?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 16 +- Program/Common/Data/AppDbContext.cs | 8 + Program/Common/JWTToken/JwtTokenService.cs | 72 +++++++++ Program/Common/Model/JwtSettings.cs | 18 +++ Program/V1/Controllers/UserController.cs | 180 ++++++++++++++++----- Program/V1/Models/User.cs | 83 ++++++++-- appsettings.json | 2 +- 7 files changed, 326 insertions(+), 53 deletions(-) create mode 100644 Program/Common/JWTToken/JwtTokenService.cs diff --git a/Program.cs b/Program.cs index a48805d..ddddd9f 100644 --- a/Program.cs +++ b/Program.cs @@ -65,7 +65,8 @@ builder.Services.AddAuthentication(options => ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings.Issuer, ValidAudience = jwtSettings.Audience, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)) + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)), + ClockSkew = TimeSpan.FromMinutes(jwtSettings.ClockSkewMinutes) }; }); // JWT 설정부 끝 @@ -96,6 +97,19 @@ builder.Services.AddCors(option => }); }); +// 로그 설정 부분 +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +if (builder.Environment.IsDevelopment()) { + builder.Logging.SetMinimumLevel(LogLevel.Trace); +} +else +{ + builder.Logging.SetMinimumLevel(LogLevel.Warning); +} + + + // 로컬 테스트 위한 부분 builder.WebHost.UseUrls("http://0.0.0.0:5144"); ///// ===== builder 설정 부 ===== ///// diff --git a/Program/Common/Data/AppDbContext.cs b/Program/Common/Data/AppDbContext.cs index f03f11c..3fddb4f 100644 --- a/Program/Common/Data/AppDbContext.cs +++ b/Program/Common/Data/AppDbContext.cs @@ -1,3 +1,4 @@ +using AcaMate.Common.Models; using Microsoft.EntityFrameworkCore; using AcaMate.V1.Models; using Version = AcaMate.V1.Models.Version; @@ -13,11 +14,18 @@ public class AppDbContext: DbContext //MARK: Program public DbSet Version { get; set; } public DbSet Academy { get; set; } + public DbSet RefreshTokens { get; set; } //MARK: USER public DbSet Login { get; set; } public DbSet UserAcademy { get; set; } public DbSet User { get; set; } + public DbSet Permission { get; set; } + // public DbSet Token { get; set; } + public DbSet Location { get; set; } + public DbSet Contact { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Program/Common/JWTToken/JwtTokenService.cs b/Program/Common/JWTToken/JwtTokenService.cs new file mode 100644 index 0000000..0995a5d --- /dev/null +++ b/Program/Common/JWTToken/JwtTokenService.cs @@ -0,0 +1,72 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Collections.Generic; +using AcaMate.Common.Models; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Options; + +using System.Security.Cryptography; + +namespace AcaMate.Common.Token; + +public class JwtTokenService +{ + private readonly JwtSettings _jwtSettings; + + public JwtTokenService(IOptions jwtSettings) + { + _jwtSettings = jwtSettings.Value; + } + + public string GenerateJwtToken(string uid, string role) + { + // 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함 + var claims = new List + { + // 토큰 주체(sub) 생성을 위해 값으로 uid를 사용함 : 토큰이 대표하는 고유 식별자 + new Claim(JwtRegisteredClaimNames.Sub, uid), + // Jti 는 토큰 식별자로 토큰의 고유 ID 이다. + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + // jwt 토큰이 가지는 권한 + new Claim(ClaimTypes.Role, role) + // 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin") + }; + + // 2. 비밀 키와 SigningCredentials 생성 + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + // 3. 토큰 생성 (Issuer, Audience, 만료 시간, 클레임, 서명 정보 포함) + var token = new JwtSecurityToken( + issuer: _jwtSettings.Issuer, + audience: _jwtSettings.Audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryMinutes), + signingCredentials: credentials + ); + + // 4. 토큰 객체를 문자열로 변환하여 반환 + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public RefreshToken GenerateRefreshToken(string uid) + { + var randomNumber = new byte[32]; // 256비트 + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomNumber); + } + + // return Convert.ToBase64String(randomNumber); + return new RefreshToken() + { + uid = uid, + token = Convert.ToBase64String(randomNumber), + create_Date = DateTime.UtcNow, + expire_date = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays) + + }; + } +} \ No newline at end of file diff --git a/Program/Common/Model/JwtSettings.cs b/Program/Common/Model/JwtSettings.cs index 7f8d826..8f949fe 100644 --- a/Program/Common/Model/JwtSettings.cs +++ b/Program/Common/Model/JwtSettings.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + namespace AcaMate.Common.Models; public class JwtSettings @@ -6,4 +9,19 @@ public class JwtSettings public string Issuer { get; set; } public string Audience { get; set; } public int ExpiryMinutes { get; set; } + public int ClockSkewMinutes { get; set; } + public int RefreshTokenExpiryDays { get; set; } +} +[Table(("refresh_token"))] +public class RefreshToken +{ + [Key] + [Required(ErrorMessage = "필수 항목 누락")] + public string uid { get; set; } + public string token { get; set; } + public DateTime create_Date { get; set; } + public DateTime expire_date { get; set; } + + // 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다. + public DateTime? revoke_Date { get; set; } } \ No newline at end of file diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index d2c8c99..df702c7 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -1,33 +1,38 @@ -using Microsoft.AspNetCore.Mvc; -using System.Text.Json; using AcaMate.Common.Data; using AcaMate.Common.Models; using AcaMate.V1.Models; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using AcaMate.V1.Services; +using AcaMate.Common.Token; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore.Query; +using MySqlConnector; namespace AcaMate.V1.Controllers; - [ApiController] [Route("/api/v1/in/user")] [ApiExplorerSettings(GroupName = "사용자")] -public class UserController: ControllerBase +public class UserController : ControllerBase { private readonly AppDbContext _dbContext; - public UserController(AppDbContext dbContext) + private readonly ILogger _logger; + private readonly JwtTokenService _jwtTokenService; + + public UserController(AppDbContext dbContext, ILogger logger, JwtTokenService jwtTokenService) { _dbContext = dbContext; + _logger = logger; + _jwtTokenService = jwtTokenService; } - [HttpGet()] + [HttpGet] [CustomOperation("회원 정보 조회", "회원 정보 조회", "사용자")] public IActionResult GetUserData(string uid) { - if (string.IsNullOrEmpty(uid)) - { - return BadRequest(DefaultResponse.InvalidInputError); - } + if (string.IsNullOrEmpty(uid)) return BadRequest(DefaultResponse.InvalidInputError); try { @@ -41,13 +46,13 @@ public class UserController: ControllerBase birth = u.birth, device_id = u.device_id, login_date = u.login_date, - type = u.type, + type = u.type }) .FirstOrDefault(); - + var response = new APIResponseStatus { - status = new Status() + status = new Status { code = "000", message = "정상" @@ -56,7 +61,7 @@ public class UserController: ControllerBase }; return Ok(response.JsonToString()); } - catch(Exception ex) + catch (Exception ex) { return StatusCode(500, DefaultResponse.UnknownError); } @@ -66,22 +71,21 @@ public class UserController: ControllerBase [CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")] public IActionResult Login(string acctype, string sns_id) { - if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id)) - { return BadRequest(DefaultResponse.InvalidInputError); - } try { var login = _dbContext.Login.FirstOrDefault(l => l.sns_type == acctype && l.sns_id == sns_id); - string uid = ""; + var uid = ""; + List bids = new List(); if (login != null) { uid = login.uid; + var user = _dbContext.User.FirstOrDefault(user => user.uid == uid); if (user != null) { @@ -89,16 +93,22 @@ public class UserController: ControllerBase _dbContext.SaveChanges(); } + // 토큰 생성은 로그인 부분에서 하는거지 + var accessToken = _jwtTokenService.GenerateJwtToken(uid, "Normal"); + var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); + _dbContext.RefreshTokens.Add(refreshToken); + _dbContext.SaveChanges(); + var userAcademy = _dbContext.UserAcademy.Where(u => u.uid == uid).ToList(); - foreach(User_Academy userData in userAcademy) + foreach (var userData in userAcademy) { - Console.WriteLine($"uid: {userData.uid} || bid: {userData.bid}"); + _logger.LogInformation($"uid: {userData.uid} || bid: {userData.bid}"); bids.Add(userData.bid); } - + var response = new APIResponseStatus { - status = new Status() + status = new Status { code = "000", message = "정상" @@ -109,7 +119,7 @@ public class UserController: ControllerBase bid = bids } }; - + return Ok(response.JsonToString()); } else @@ -117,7 +127,7 @@ public class UserController: ControllerBase // 계정이 없다는 거 var response = new APIResponseStatus { - status = new Status() + status = new Status { code = "010", message = "정상" @@ -125,7 +135,7 @@ public class UserController: ControllerBase data = new { uid = "", - bid = new string[]{} + bid = new string[] { } } }; return Ok(response.JsonToString()); @@ -136,9 +146,9 @@ public class UserController: ControllerBase return StatusCode(500, DefaultResponse.UnknownError); } } - + [HttpPost("academy")] - [CustomOperation("학원 리스트 확인","등록된 학원 리스트 확인", "사용자")] + [CustomOperation("학원 리스트 확인", "등록된 학원 리스트 확인", "사용자")] public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) { if (!request.bids.Any()) @@ -156,10 +166,10 @@ public class UserController: ControllerBase name = a.business_name }) .ToList(); - + var response = new APIResponseStatus> { - status = new Status() + status = new Status { code = "000", message = "정상" @@ -171,17 +181,111 @@ public class UserController: ControllerBase [HttpPost("register")] [CustomOperation("회원 가입", "사용자 회원 가입", "사용자")] - public IActionResult UserRegister([FromBody] User request) + public async Task UserRegister([FromBody] UserAll request) { - if (request.uid.IsNullOrEmpty()) + if (!ModelState.IsValid) return BadRequest(DefaultResponse.InvalidInputError); + + var atIndext = request.email.IndexOf('@'); + var localPart_email = request.email.Substring(0, atIndext); + var uid = $"AM{localPart_email}{DateTime.Now:yyyyMMdd}"; + + var user = new User { - var error = DefaultResponse.InvalidInputError; - return Ok(error); + uid = uid, + name = request.name, + birth = request.birth, + type = request.type, + device_id = request.device_id, + auto_login_yn = request.auto_login_yn, + login_date = request.login_date + }; + var login = new Login + { + uid = uid, + sns_id = request.sns_id, + sns_type = request.sns_type + }; + + var permission = new Permission + { + uid = uid, + location_yn = request.location_yn, + camera_yn = request.camera_yn, + photo_yn = request.photo_yn, + push_yn = request.push_yn, + market_app_yn = request.market_app_yn, + market_sms_yn = request.market_sms_yn, + market_email_yn = request.market_email_yn + }; + + var contact = new Contact + { + uid = uid, + email = request.email, + phone = request.phone, + address = request.address + }; + + + if (await SaveData(user, u => u.uid)) + { + await SaveData(login, l => l.sns_id); + await SaveData(permission, p => p.uid); + await SaveData(contact, c => c.uid); } - else + + // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 + + return Ok($"회원가입 : {uid}"); + } + private async Task SaveData (T entity, Expression> key) where T : class + { + try { - return Ok("회원가입"); + 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>(equalsExpr, parameter); + + var dbSet = _dbContext.Set(); + 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; } } - + + } \ No newline at end of file diff --git a/Program/V1/Models/User.cs b/Program/V1/Models/User.cs index c1c0871..4aecbef 100644 --- a/Program/V1/Models/User.cs +++ b/Program/V1/Models/User.cs @@ -8,10 +8,15 @@ namespace AcaMate.V1.Models; public class Login { [Key] + [Required(ErrorMessage = "필수 항목 누락")] [MaxLength(100)] public string sns_id {get; set;} + + [Required(ErrorMessage = "필수 항목 누락")] [MaxLength(70)] public string uid {get; set;} + + [Required(ErrorMessage = "필수 항목 누락")] [MaxLength(4)] public string sns_type {get; set;} } @@ -20,9 +25,16 @@ public class Login public class User_Academy { [Key] + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string bid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime register_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public bool status { get; set; } } @@ -30,12 +42,22 @@ public class User_Academy public class User { [Key] + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string name { get; set; } - public DateTime birth { get; set; } + public DateTime? birth { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string type { get; set; } - public string device_id { get; set; } - public int auto_login_yn { get; set; } + + public string? device_id { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] + public bool auto_login_yn { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime login_date { get; set; } } @@ -44,6 +66,8 @@ public class User public class Permission { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } public bool location_yn {get; set;} @@ -59,10 +83,20 @@ public class Permission public class Token { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string refresh_token { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime create_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime expires_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime revoke_date { get; set; } } @@ -70,8 +104,14 @@ public class Token public class Location { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string lat { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string lng { get; set; } } @@ -79,25 +119,40 @@ public class Location public class Contact { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string email { get; set; } - public string phone { get; set; } + public string? phone { get; set; } + public string? address { get; set; } } // -- -- -- -- -- DB 테이블 -- -- -- -- -- // public class UserAll { - public string uid { get; set; } - public string name { get; set; } - public DateTime birth { get; set; } - public string type { get; set; } - public string device_id { get; set; } - public int auto_login_yn { get; set; } - public DateTime login_date { get; set; } + // [Required(ErrorMessage = "필수 항목 누락")] + // public string uid { get; set; } + [Required(ErrorMessage = "필수 항목 누락")] + public string name { get; set; } + + public DateTime? birth { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] + public string type { get; set; } + + public string? device_id { get; set; } + + public bool auto_login_yn { get; set; } + public DateTime login_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string email { get; set; } - public string phone { get; set; } + public string? phone { get; set; } + public string? address { get; set; } public bool location_yn {get; set;} public bool camera_yn {get; set;} @@ -107,7 +162,9 @@ public class UserAll public bool market_sms_yn {get; set;} public bool market_email_yn {get; set;} + [Required(ErrorMessage = "필수 항목 누락")] public string sns_id {get; set;} + + [Required(ErrorMessage = "필수 항목 누락")] public string sns_type {get; set;} - public string sns_email {get; set;} } \ No newline at end of file diff --git a/appsettings.json b/appsettings.json index ec04bc1..d66442c 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Warning", "Microsoft.AspNetCore": "Warning" } }, From f1a901820f2caf354774a02446c10c13f9a6be94 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Mon, 24 Feb 2025 17:54:38 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[=E2=9C=A8]=20JWT=20=EC=83=9D=EC=84=B1=20+?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EC=A4=91=20&=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20API=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 2 + Program/Common/JWTToken/JwtTokenService.cs | 46 ++++++++++++++- Program/Common/Model/JwtSettings.cs | 24 +++++++- Program/V1/Controllers/UserController.cs | 68 +++++++++++++++++++++- Program/V1/Models/User.cs | 20 ------- 5 files changed, 133 insertions(+), 27 deletions(-) diff --git a/Program.cs b/Program.cs index ddddd9f..bc65410 100644 --- a/Program.cs +++ b/Program.cs @@ -72,7 +72,9 @@ builder.Services.AddAuthentication(options => // JWT 설정부 끝 builder.Services.AddControllers(); + // 여기다가 API 있는 컨트롤러들 AddScoped 하면 되는건가? +builder.Services.AddScoped(); // builder.Services.AddScoped(); // // builder.Services.AddScoped(); diff --git a/Program/Common/JWTToken/JwtTokenService.cs b/Program/Common/JWTToken/JwtTokenService.cs index 0995a5d..a99a2ad 100644 --- a/Program/Common/JWTToken/JwtTokenService.cs +++ b/Program/Common/JWTToken/JwtTokenService.cs @@ -6,6 +6,9 @@ using System.Collections.Generic; using AcaMate.Common.Models; using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; + using System.Security.Cryptography; @@ -30,8 +33,9 @@ public class JwtTokenService // Jti 는 토큰 식별자로 토큰의 고유 ID 이다. new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // jwt 토큰이 가지는 권한 - new Claim(ClaimTypes.Role, role) + new Claim(ClaimTypes.Role, role), // 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin") + new Claim(ClaimTypes.NameIdentifier, uid) }; // 2. 비밀 키와 SigningCredentials 생성 @@ -43,7 +47,7 @@ public class JwtTokenService issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, - expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryMinutes), + expires: DateTime.Now.AddMinutes(_jwtSettings.ExpiryMinutes), signingCredentials: credentials ); @@ -63,10 +67,46 @@ public class JwtTokenService return new RefreshToken() { uid = uid, - token = Convert.ToBase64String(randomNumber), + refresh_token = Convert.ToBase64String(randomNumber), create_Date = DateTime.UtcNow, expire_date = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays) }; } + + + public ClaimsPrincipal ValidateToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) return null; + var tokenHandler = new JwtSecurityTokenHandler(); + + try + { + var key = Encoding.UTF8.GetBytes(_jwtSettings.SecretKey); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidIssuer = _jwtSettings.Issuer, + ValidateAudience = true, + ValidAudience = _jwtSettings.Audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(_jwtSettings.ClockSkewMinutes) + }; + var principal = tokenHandler.ValidateToken(token, validationParameters, out var securityToken); + return principal; + } + catch (Exception e) + { + Console.WriteLine($"검증 실패 {e}"); + return null; + } + } + + + + + + } \ No newline at end of file diff --git a/Program/Common/Model/JwtSettings.cs b/Program/Common/Model/JwtSettings.cs index 8f949fe..a7b57cb 100644 --- a/Program/Common/Model/JwtSettings.cs +++ b/Program/Common/Model/JwtSettings.cs @@ -12,16 +12,34 @@ public class JwtSettings public int ClockSkewMinutes { get; set; } public int RefreshTokenExpiryDays { get; set; } } -[Table(("refresh_token"))] + + +[Table("refresh_token")] public class RefreshToken { [Key] [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } - public string token { get; set; } + public string refresh_token { get; set; } public DateTime create_Date { get; set; } public DateTime expire_date { get; set; } // 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다. public DateTime? revoke_Date { get; set; } -} \ No newline at end of file +} +/* +""" +토큰 동작 관련 +다시 물어보자 토큰 로직 관련해서 일단은 로그인을 예로 들면 +1. 로그인을 진행한다. +2. 회원이 DB에 정상적으로 존재한다. +3. 엑세스 토큰과 리프레시 토큰을 생성한다. +4. 엑세스 토큰은 클라이언트로 리프래시 토큰은 서버에 저장하고 클라이언트로도 보낸다. +5. (상황1) 시간이 경과해 엑세스 토큰의 시간이 경과했다. +6. (상황2) 회원 정보에 대한 접근이 필요한 동작을 수행한다. +7. 엑세스 토큰과 리프레시 토큰을 서버로 전송한다. +8. 엑세스 토큰이 만료가 되었음이 확인이 되면 리프레시 토큰으로 새 엑세스를 만들기 위해 리프레시 토큰을 확인한다. +9. 리프레시 토큰이 만료가 되지 않았다면 리프레시 토큰을 토대로 엑세스 토큰을 생성한다. +10. 생성된 엑세스 토큰을 가지고 상황2의 동작을 수행하고 엑세스 토큰을 반환한다. +""" +*/ \ No newline at end of file diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index df702c7..e40ae7c 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -1,3 +1,6 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; using AcaMate.Common.Data; using AcaMate.Common.Models; using AcaMate.V1.Models; @@ -9,6 +12,7 @@ using AcaMate.Common.Token; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore.Query; using MySqlConnector; +using System.Linq; namespace AcaMate.V1.Controllers; @@ -235,8 +239,49 @@ public class UserController : ControllerBase } // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 + var token = _jwtTokenService.GenerateJwtToken(uid, "admin"); + var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); + await SaveData(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"); + } + /* */ - return Ok($"회원가입 : {uid}"); + var result = new APIResponseStatus() + { + status = new Status() + { + code = "000", + message = "정상" + }, + data = new + { + uid = uid, + accessToken = token, + refreshToken = refreshToken.refresh_token + } + }; + + return Ok(result.JsonToString()); } private async Task SaveData (T entity, Expression> key) where T : class { @@ -287,5 +332,26 @@ public class UserController : ControllerBase } } + [HttpGet("logout")] + [CustomOperation("로그아웃", "사용자 로그아웃", "사용자")] + public async Task LogOut(string token, string refresh)//([FromBody] UserAll request) + { + var principalToken = _jwtTokenService.ValidateToken(token); + if (principalToken != null) + { + // var uid = principalToken.FindFirst(JwtRegisteredClaimNames.Jti)?.Value; + var uid = principalToken.FindFirst("sub")?.Value; + _logger.LogInformation($"토큰? - {uid}"); + + } + else + { + + _logger.LogInformation("dd"); + } + + return Ok("로그아웃"); + } + } \ No newline at end of file diff --git a/Program/V1/Models/User.cs b/Program/V1/Models/User.cs index 4aecbef..2eab154 100644 --- a/Program/V1/Models/User.cs +++ b/Program/V1/Models/User.cs @@ -79,26 +79,6 @@ public class Permission public bool market_email_yn {get; set;} } -[Table("token")] -public class Token -{ - [Key] - - [Required(ErrorMessage = "필수 항목 누락")] - public string uid { get; set; } - - [Required(ErrorMessage = "필수 항목 누락")] - public string refresh_token { get; set; } - - [Required(ErrorMessage = "필수 항목 누락")] - public DateTime create_date { get; set; } - - [Required(ErrorMessage = "필수 항목 누락")] - public DateTime expires_date { get; set; } - - [Required(ErrorMessage = "필수 항목 누락")] - public DateTime revoke_date { get; set; } -} [Table("location")] public class Location From 0e2452207c8a5370a4e3664bd3e8724e085848dc Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Tue, 25 Feb 2025 17:27:23 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[=E2=9C=A8]=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=8F=84=EC=9E=85,=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8,=20=EC=95=84=EC=B9=B4=EB=8D=B0=EB=AF=B8=20API=20?= =?UTF-8?q?=EC=97=90=20JWT=20=EC=9D=B8=EC=A6=9D=20=EB=8F=84=EC=9E=85?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 1 + Program/Common/JWTToken/JwtTokenService.cs | 14 +- Program/Common/Model/JwtSettings.cs | 7 + Program/V1/Controllers/UserController.cs | 300 +++++++++------------ Program/V1/Services/RepositoryService.cs | 136 ++++++++++ 5 files changed, 280 insertions(+), 178 deletions(-) create mode 100644 Program/V1/Services/RepositoryService.cs diff --git a/Program.cs b/Program.cs index bc65410..2f460d4 100644 --- a/Program.cs +++ b/Program.cs @@ -75,6 +75,7 @@ builder.Services.AddControllers(); // 여기다가 API 있는 컨트롤러들 AddScoped 하면 되는건가? builder.Services.AddScoped(); +builder.Services.AddScoped(); // builder.Services.AddScoped(); // // builder.Services.AddScoped(); diff --git a/Program/Common/JWTToken/JwtTokenService.cs b/Program/Common/JWTToken/JwtTokenService.cs index a99a2ad..250fc0e 100644 --- a/Program/Common/JWTToken/JwtTokenService.cs +++ b/Program/Common/JWTToken/JwtTokenService.cs @@ -17,13 +17,16 @@ namespace AcaMate.Common.Token; public class JwtTokenService { private readonly JwtSettings _jwtSettings; + + private readonly ILogger _logger; - public JwtTokenService(IOptions jwtSettings) + public JwtTokenService(IOptions jwtSettings, ILogger logger) { _jwtSettings = jwtSettings.Value; + _logger = logger; } - public string GenerateJwtToken(string uid, string role) + public string GenerateJwtToken(string uid)//, string role) { // 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함 var claims = new List @@ -33,9 +36,8 @@ public class JwtTokenService // Jti 는 토큰 식별자로 토큰의 고유 ID 이다. new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // jwt 토큰이 가지는 권한 - new Claim(ClaimTypes.Role, role), + // new Claim(ClaimTypes.Role, role), // 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin") - new Claim(ClaimTypes.NameIdentifier, uid) }; // 2. 비밀 키와 SigningCredentials 생성 @@ -50,7 +52,7 @@ public class JwtTokenService expires: DateTime.Now.AddMinutes(_jwtSettings.ExpiryMinutes), signingCredentials: credentials ); - + // 4. 토큰 객체를 문자열로 변환하여 반환 return new JwtSecurityTokenHandler().WriteToken(token); } @@ -102,6 +104,8 @@ public class JwtTokenService Console.WriteLine($"검증 실패 {e}"); return null; } + + } diff --git a/Program/Common/Model/JwtSettings.cs b/Program/Common/Model/JwtSettings.cs index a7b57cb..5836c3e 100644 --- a/Program/Common/Model/JwtSettings.cs +++ b/Program/Common/Model/JwtSettings.cs @@ -27,6 +27,13 @@ public class RefreshToken // 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다. public DateTime? revoke_Date { get; set; } } + +public class ValidateToken +{ + public string token { get; set; } + public string refresh { get; set; } + public string uid { get; set; } +} /* """ 토큰 동작 관련 diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index e40ae7c..00b165d 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -1,19 +1,21 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; -using AcaMate.Common.Data; -using AcaMate.Common.Models; -using AcaMate.V1.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; -using AcaMate.V1.Services; -using AcaMate.Common.Token; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore.Query; using MySqlConnector; 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; [ApiController] @@ -24,12 +26,14 @@ public class UserController : ControllerBase private readonly AppDbContext _dbContext; private readonly ILogger _logger; private readonly JwtTokenService _jwtTokenService; + private readonly IRepositoryService _repositoryService; - public UserController(AppDbContext dbContext, ILogger logger, JwtTokenService jwtTokenService) + public UserController(AppDbContext dbContext, ILogger logger, JwtTokenService jwtTokenService, IRepositoryService repositoryService) { _dbContext = dbContext; _logger = logger; _jwtTokenService = jwtTokenService; + _repositoryService = repositoryService; } [HttpGet] @@ -73,114 +77,136 @@ public class UserController : ControllerBase [HttpGet("login")] [CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")] - public IActionResult Login(string acctype, string sns_id) + public async Task Login(string acctype, string sns_id) { + // API 동작 파라미터 입력 값 확인 if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id)) return BadRequest(DefaultResponse.InvalidInputError); try { - var login = _dbContext.Login.FirstOrDefault(l => l.sns_type == acctype && l.sns_id == sns_id); - var uid = ""; - - List bids = new List(); + var login = await _dbContext.Login + .FirstOrDefaultAsync(l => l.sns_type == acctype && l.sns_id == sns_id); 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) { + // 정상적으로 User 테이블에도 있는것이 확인 됨 user.login_date = DateTime.Now; - _dbContext.SaveChanges(); - } - - // 토큰 생성은 로그인 부분에서 하는거지 - var accessToken = _jwtTokenService.GenerateJwtToken(uid, "Normal"); - var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); - _dbContext.RefreshTokens.Add(refreshToken); - _dbContext.SaveChanges(); + await _dbContext.SaveChangesAsync(); + + // 토큰 생성은 로그인이 이제 되고 나서 한다. + var accessToken = _jwtTokenService.GenerateJwtToken(uid);//, "Normal"); + var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); + _logger.LogInformation($"{uid}: {accessToken}, {refreshToken}"); + + await _repositoryService.SaveData(refreshToken, rt => rt.uid); + - var userAcademy = _dbContext.UserAcademy.Where(u => u.uid == uid).ToList(); - foreach (var userData in userAcademy) - { - _logger.LogInformation($"uid: {userData.uid} || bid: {userData.bid}"); - bids.Add(userData.bid); - } - - var response = new APIResponseStatus - { - status = new Status + var response = new APIResponseStatus { - code = "000", - message = "정상" - }, - data = new - { - uid = $"{uid}", - bid = bids - } - }; - - return Ok(response.JsonToString()); + status = new Status + { + code = "000", + message = "정상" + }, + data = new + { + token = accessToken, + refresh = refreshToken.refresh_token + } + }; + return Ok(response.JsonToString()); + } } - else + // case 1: Login 테이블에 값이 없다 == 로그인이 처음 + // case 2: User 테이블에 값이 없다 == 이건 문제가 있는 상황 -> 해결은 회원가입 재 진행 시도 + // Login에는 있는데 User 테이블에 없다? 말이 안되긴 하는데... + return Ok(new APIResponseStatus { - // 계정이 없다는 거 - var response = new APIResponseStatus + status = new Status { - status = new Status - { - code = "010", - message = "정상" - }, - data = new - { - uid = "", - bid = new string[] { } - } - }; - return Ok(response.JsonToString()); - } + code = "010", + message = "로그인 정보 없음 > 회원 가입 진행" + }, + data = new + { + token = "", + refresh = "" + // bidList = new string[] { } + } + }.JsonToString()); } catch (Exception ex) { + _logger.LogInformation($"[로그인] 에러 발생 : {ex}"); return StatusCode(500, DefaultResponse.UnknownError); } } - [HttpPost("academy")] - [CustomOperation("학원 리스트 확인", "등록된 학원 리스트 확인", "사용자")] - public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) + [HttpGet("academy")] + [CustomOperation("학원 리스트 확인", "사용자가 등록된 학원 리스트 확인", "사용자")] + public async Task ReadAcademyInfo(string token, string refresh) { - if (!request.bids.Any()) + _logger.LogInformation($"토큰 : {token}, {refresh}"); + + if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh)) { var error = DefaultResponse.InvalidInputError; return Ok(error); } - var academies = _dbContext - .Academy - .Where(a => request.bids.Contains(a.bid)) - .Select(a => new AcademyName - { - bid = a.bid, - name = a.business_name - }) - .ToList(); - - var response = new APIResponseStatus> + try { - 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> { - code = "000", - message = "정상" - }, - data = academies - }; - return Ok(response); + status = new Status + { + code = "000", + message = "정상" + }, + 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")] @@ -231,41 +257,18 @@ public class UserController : ControllerBase }; - if (await SaveData(user, u => u.uid)) + if (await _repositoryService.SaveData(user, u => u.uid)) { - await SaveData(login, l => l.sns_id); - await SaveData(permission, p => p.uid); - await SaveData(contact, c => c.uid); + await _repositoryService.SaveData(login, l => l.sns_id); + await _repositoryService.SaveData(permission, p => p.uid); + await _repositoryService.SaveData(contact, c => c.uid); } // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 - var token = _jwtTokenService.GenerateJwtToken(uid, "admin"); + var token = _jwtTokenService.GenerateJwtToken(uid);//, "admin"); var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); - await SaveData(refreshToken, rt => rt.uid); + await _repositoryService.SaveData(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() { status = new Status() @@ -275,7 +278,6 @@ public class UserController : ControllerBase }, data = new { - uid = uid, accessToken = token, refreshToken = refreshToken.refresh_token } @@ -283,75 +285,27 @@ public class UserController : ControllerBase return Ok(result.JsonToString()); } - private async Task SaveData (T entity, Expression> 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>(equalsExpr, parameter); - - var dbSet = _dbContext.Set(); - 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")] [CustomOperation("로그아웃", "사용자 로그아웃", "사용자")] - public async Task LogOut(string token, string refresh)//([FromBody] UserAll request) + public async Task LogOut(string token, string refresh) //([FromBody] UserAll request) { - var principalToken = _jwtTokenService.ValidateToken(token); - if (principalToken != null) - { - // var uid = principalToken.FindFirst(JwtRegisteredClaimNames.Jti)?.Value; - var uid = principalToken.FindFirst("sub")?.Value; - _logger.LogInformation($"토큰? - {uid}"); - - } - else - { - - _logger.LogInformation("dd"); - } + /* */ + // var value = await ValidateToken(token, refresh); + // _logger.LogInformation(value.uid); + // _logger.LogInformation(value.refresh); + // _logger.LogInformation(value.token); + /* */ + return Ok("로그아웃"); } + + + + + + } \ No newline at end of file diff --git a/Program/V1/Services/RepositoryService.cs b/Program/V1/Services/RepositoryService.cs new file mode 100644 index 0000000..f53fb60 --- /dev/null +++ b/Program/V1/Services/RepositoryService.cs @@ -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 SaveData(T entity, Expression> key) where T : class; + Task ValidateToken(string token, string refresh); +} + +public class RepositoryService: IRepositoryService +{ + private readonly AppDbContext _dbContext; + private readonly ILogger _logger; + private readonly JwtTokenService _jwtTokenService; + + public RepositoryService(AppDbContext dbContext, ILogger logger, JwtTokenService jwtTokenService) + { + _dbContext = dbContext; + _logger = logger; + _jwtTokenService = jwtTokenService; + } + + public async Task SaveData (T entity, Expression> 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>(equalsExpr, parameter); + + var dbSet = _dbContext.Set(); + + 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(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, rt => rt.uid); + return new ValidateToken + { + token = token, + refresh = refreshToken.refresh_token, + uid = uid + }; + } + } + } + +} \ No newline at end of file From b5021eaaa428ddc714cfd6d5eb30f2e7dc7ef59b Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Tue, 25 Feb 2025 17:38:30 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[=F0=9F=91=B7]=20DEV=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=EC=98=AC=EB=A6=AC=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Program.cs b/Program.cs index 2f460d4..3b7eed2 100644 --- a/Program.cs +++ b/Program.cs @@ -113,8 +113,9 @@ else -// 로컬 테스트 위한 부분 -builder.WebHost.UseUrls("http://0.0.0.0:5144"); +// 로컬 테스트 위한 부분 (올릴때는 꺼두기) +// builder.WebHost.UseUrls("http://0.0.0.0:5144"); + ///// ===== builder 설정 부 ===== ///// var app = builder.Build(); @@ -132,8 +133,8 @@ else app.UseHsts(); } -// 로컬 테스트 위한 부분 -// app.UseHttpsRedirection(); +// 로컬 테스트 위한 부분 (올릴떄는 켜두기) +app.UseHttpsRedirection(); app.UseRouting(); // app.MapControllers();