■ JWT(Json Web Token) 인증을 사용하는 방법을 보여준다.
[TestLibrary 프로젝트]
▶ Models/UserModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
namespace TestLibrary.Models { /// <summary> /// 사용자 /// </summary> public class UserModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region ID - ID /// <summary> /// ID /// </summary> public int ID { get; set; } #endregion #region 사용자명 - UserName /// <summary> /// 사용자명 /// </summary> public string UserName { get; set; } #endregion #region 패스워드 - Password /// <summary> /// 패스워드 /// </summary> public string Password { get; set; } #endregion } } |
▶ Models/TestModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace TestLibrary.Models { /// <summary> /// 테스트 모델 /// </summary> public class TestModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 값 - Value /// <summary> /// 값 /// </summary> public string Value { get; set; } #endregion } } |
[TestClient 프로젝트]
▶ Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using TestLibrary.Models; namespace TestClient { /// <summary> /// 프로그램 /// </summary> class Program { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region 프로그램 시작하기 - Main() /// <summary> /// 프로그램 시작하기 /// </summary> static void Main() { UserModel user = new UserModel { UserName = "admin", Password = "1234"}; string token = Login("http://localhost:4035/api/authentication/login", user).GetAwaiter().GetResult(); Console.WriteLine($"JWT 토큰 : {token}"); Console.WriteLine("--------------------------------------------------"); TestModel test = new TestModel { Value = "테스트" }; string result = Test("http://localhost:4035/api/authentication/test", token, test).GetAwaiter().GetResult(); Console.WriteLine($"결과 : {result}"); Console.WriteLine("--------------------------------------------------"); Console.WriteLine("아무 키나 눌러 주시기 바랍니다."); Console.ReadKey(true); } #endregion #region HTTP 클라이언트 구하기 - GetHTTPClient(useCookie, baseAddress, tokenShema, token) /// <summary> /// HTTP 클라이언트 구하기 /// </summary> /// <param name="useCookie">쿠키 사용 여부</param> /// <param name="baseAddress">기본 주소</param> /// <param name="tokenShema">토큰 스키마</param> /// <param name="token">토큰</param> /// <returns>HTTP 클라이언트</returns> private static HttpClient GetHTTPClient(bool useCookie, string baseAddress, string tokenShema, string token) { HttpClient client; if(useCookie) { HttpClientHandler httpClientHandler = new HttpClientHandler(); httpClientHandler.UseCookies = true; httpClientHandler.CookieContainer = new CookieContainer(); client = new HttpClient(httpClientHandler); } else { client = new HttpClient(); } client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if(!string.IsNullOrWhiteSpace(baseAddress)) { client.BaseAddress = new Uri(baseAddress); } if(!string.IsNullOrWhiteSpace(tokenShema)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenShema, token); } return client; } #endregion #region 로그인하기 - Login(url, user) /// <summary> /// 로그인하기 /// </summary> /// <param name="url">URL</param> /// <param name="user">사용자</param> /// <returns>JWT 토큰</returns> private static async Task<string> Login(string url, UserModel user) { using(HttpClient client = GetHTTPClient(false, url, null, null)) { var response = await client.PostAsJsonAsync<UserModel>(url, user); if(response.IsSuccessStatusCode) { string token = await response.Content.ReadAsAsync<string>(); return token; } return null; } } #endregion #region 테스트하기 - Test(url, token, test) /// <summary> /// 테스트하기 /// </summary> /// <param name="url">URL</param> /// <param name="token">JWT 토큰</param> /// <param name="test">테스트</param> /// <returns>테스트 결과</returns> private static async Task<string> Test(string url, string token, TestModel test) { using(HttpClient client = GetHTTPClient(false, url, "Bearer", token)) { var response = await client.PostAsJsonAsync<TestModel>(url, test); if(response.IsSuccessStatusCode) { string result = await response.Content.ReadAsAsync<string>(); return result; } else // http://로 호출한 경우 https://로 다시 호출한다. { string finalRequestURL = response.RequestMessage.RequestUri.AbsoluteUri; if(finalRequestURL != url) { response = await client.PostAsJsonAsync<TestModel>(finalRequestURL, test); if(response.IsSuccessStatusCode) { string result = await response.Content.ReadAsAsync<string>(); return result; } else { return null; } } else { return null; } } } } #endregion } } |
[TestServer 프로젝트]
▶ Properties/launchSettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{ "$schema" : "http://json.schemastore.org/launchsettings.json", "iisSettings" : { "windowsAuthentication" : false, "anonymousAuthentication" : true, "iisExpress" : { "applicationUrl" : "http://localhost:4035", "sslPort" : 44364 } }, "profiles" : { "IIS Express" : { "commandName" : "IISExpress", "launchBrowser" : false, "environmentVariables" : { "ASPNETCORE_ENVIRONMENT" : "Development" } }, "TestProject" : { "commandName" : "Project", "launchBrowser" : false, "applicationUrl" : "https://localhost:5001;http://localhost:5000", "environmentVariables" : { "ASPNETCORE_ENVIRONMENT" : "Development" } } } } |
▶ appsettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{ "ConnectionStrings" : { "DefaultConnection" : "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TestDB;Integrated Security=True;" }, "Logging" : { "LogLevel" : { "Default" : "Information", "Microsoft" : "Warning", "Microsoft.Hosting.Lifetime" : "Information" } }, "AllowedHosts": "*" } |
▶ Database/DatabaseContext.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
using Microsoft.EntityFrameworkCore; using TestLibrary.Models; namespace TestServer.Database { /// <summary> /// 데이터베이스 컨텍스트 /// </summary> public class DatabaseContext : DbContext { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 사용자 테이블 - User /// <summary> /// 사용자 테이블 /// </summary> public DbSet<UserModel> User { get; set; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - DatabaseContext(options) /// <summary> /// 생성자 /// </summary> /// <param name="options">DB 컨텍스트 옵션</param> public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Protected #region 모델 생성시 처리하기 - OnModelCreating(builder) /// <summary> /// 모델 생성시 처리하기 /// </summary> /// <param name="builder">모델 빌더</param> protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<UserModel>().HasData ( new UserModel { ID = 1, UserName = "admin" , Password = "1234" }, new UserModel { ID = 2, UserName = "tester", Password = "1234" } ); } #endregion } } |
※ Enitity Framework Core를 사용해 데이터베이스를 액세스하므로 appsettings.json 파일의 DefaultConnection 항목에 설정한 DB 연결 문자열에 맞추어 데이터베이스를 생성하고 [패키지 관리자 콘솔]에서 아래 명령을 실행한다.
▶ 실행 명령
1 2 3 |
Update-Database |
▶ Services/ITokenBuilder.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace TestServer.Services { /// <summary> /// 토큰 빌더 인터페이스 /// </summary> public interface ITokenBuilder { //////////////////////////////////////////////////////////////////////////////////////////////////// Method #region 토큰 만들기 - BuildToken(userName) /// <summary> /// 토큰 만들기 /// </summary> /// <param name="userName">사용자명</param> /// <returns>JWT 토큰</returns> string BuildToken(string userName); #endregion } } |
▶ Services/TokenBuilder.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace TestServer.Services { /// <summary> /// 토큰 빌더 /// </summary> public class TokenBuilder : ITokenBuilder { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 토큰 만들기 - BuildToken(userName) /// <summary> /// 토큰 만들기 /// </summary> /// <param name="userName">사용자명</param> /// <returns>토큰</returns> public string BuildToken(string userName) { SymmetricSecurityKey securityKey = new SymmetricSecurityKey ( Encoding.UTF8.GetBytes("placeholder-key-that-is-long-enough-for-sha256") ); SigningCredentials signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); Claim[] claimArray = new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, userName) }; JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(claims : claimArray, signingCredentials : signingCredentials); string token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); return token; } #endregion } } |
▶ Controllers/AuthenticationController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using TestLibrary.Models; using TestServer.Database; using TestServer.Services; namespace TestServer.Controllers { /// <summary> /// 인증 컨트롤러 /// </summary> [ApiController] [Route("api/[controller]")] public class AuthenticationController : ControllerBase { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 데이터베이스 컨텍스트 /// </summary> private readonly DatabaseContext databaseContext; /// <summary> /// 토큰 빌더 /// </summary> private readonly ITokenBuilder tokenBuilder; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - AuthenticationController(databaseContext, tokenBuilder) /// <summary> /// 생성자 /// </summary> /// <param name="databaseContext">데이터베이스 컨텍스트</param> /// <param name="tokenBuilder">토큰 빌더</param> public AuthenticationController(DatabaseContext databaseContext, ITokenBuilder tokenBuilder) { this.databaseContext = databaseContext; this.tokenBuilder = tokenBuilder; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 로그인하기 - Login(user) /// <summary> /// 로그인하기 /// </summary> /// <param name="user">사용자</param> /// <returns>액션 결과 태스크</returns> [HttpPost("login")] public async Task<IActionResult> Login([FromBody]UserModel user) { UserModel existingUser = await this.databaseContext.User.SingleOrDefaultAsync(u => u.UserName == user.UserName); if(existingUser == null) { return NotFound("USER NOT FOUND"); } bool isValid = existingUser.Password == user.Password; if(!isValid) { return BadRequest("COULD NOT AUTHENTICATE USER"); } string token = this.tokenBuilder.BuildToken(user.UserName); return Ok(token); } #endregion #region 테스트하기 - Test(test) /// <summary> /// 테스트하기 /// </summary> /// <param name="test">테스트</param> /// <returns>액션 결과</returns> [HttpPost("test")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public async Task<IActionResult> Test([FromBody]TestModel test) { Claim userNameClaim = User.Claims.SingleOrDefault(); if(userNameClaim == null) { return Unauthorized(); } bool exist = await this.databaseContext.User.AnyAsync(user => user.UserName == userNameClaim.Value); if(!exist) { return Unauthorized(); } return Ok($"SUCCESS : {DateTime.Now:yyyy-MM-dd HH:mm:ss} {test.Value}"); } #endregion } } |
▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using System; using System.Text; using TestServer.Database; using TestServer.Services; namespace TestServer { /// <summary> /// 시작 /// </summary> public class Startup { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구성 - Configuration /// <summary> /// 구성 /// </summary> public IConfiguration Configuration { get; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - Startup(configuration) /// <summary> /// 생성자 /// </summary> /// <param name="configuration">구성</param> public Startup(IConfiguration configuration) { Configuration = configuration; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 서비스 컬렉션 구성하기 - ConfigureServices(services) /// <summary> /// 서비스 컬렉션 구성하기 /// </summary> /// <param name="services">서비스 컬렉션</param> public void ConfigureServices(IServiceCollection services) { services.AddDbContext<DatabaseContext> ( options => { options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); } ); services.AddAuthentication ( options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; } ) .AddJwtBearer ( options => { options.RequireHttpsMetadata = true; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters() { IssuerSigningKey = new SymmetricSecurityKey ( Encoding.UTF8.GetBytes("placeholder-key-that-is-long-enough-for-sha256") ), ValidateAudience = false, ValidateIssuer = false, ValidateLifetime = false, RequireExpirationTime = false, ClockSkew = TimeSpan.Zero, ValidateIssuerSigningKey = true }; } ); services.AddScoped<ITokenBuilder, TokenBuilder>(); services.AddControllers(); } #endregion #region 구성하기 - Configure(app, environment) /// <summary> /// 구성하기 /// </summary> /// <param name="app">애플리케이션 빌더</param> /// <param name="environment">웹 로스트 환경</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { if(environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints ( endpoints => { endpoints.MapControllers(); } ); } #endregion } } |