paint-brush
ASP.NET Core 프로젝트에 JWT가 필요한 이유는 무엇입니까?by@igorlopushko
7,145
7,145

ASP.NET Core 프로젝트에 JWT가 필요한 이유는 무엇입니까?

Igor Lopushko16m2024/02/13
Read on Terminal Reader

이 이야기는 JWT를 생성하기 위해 웹 API를 생성한 다음 이를 CRUD 웹 API에서 인증에 사용하는 방법에 관한 것입니다.
featured image - ASP.NET Core 프로젝트에 JWT가 필요한 이유는 무엇입니까?
Igor Lopushko HackerNoon profile picture
0-item

JWT는 JSON Web Token 의 약자이며 인증이 아닌 인증 메커니즘입니다. 그럼 이 둘의 차이점이 무엇인지 알아보겠습니다.


인증은 사용자가 자신이 주장하는 사람과 정확히 일치하는지 확인할 수 있는 메커니즘입니다. 사용자가 사용자 이름과 비밀번호를 제공하고 시스템이 이를 확인하는 로그인 프로세스입니다. 따라서 인증은 사용자가 누구인가라는 질문에 답합니다.


권한 부여는 사용자가 특정 리소스에 대해 어떤 액세스 권한을 갖고 있는지 확인할 수 있는 메커니즘입니다. 이는 사용자에게 특정 역할이 갖는 일부 역할과 권한 집합을 부여하는 프로세스입니다. 따라서 인증은 다음 질문에 답합니다. 사용자는 시스템에서 어떤 권한을 갖고 있습니까?

인증과 승인


인증이 항상 먼저이고 승인이 두 번째라는 점을 이해하는 것이 중요합니다. 즉, 본인 인증을 하기 전에는 허가를 받을 수 없습니다. 그렇다면 가장 널리 사용되는 인증 방법은 무엇입니까? 웹 애플리케이션에 대한 인증을 처리하는 데는 두 가지 주요 접근 방식이 있습니다.

세션

인증 사용자를 위한 웹에서의 전통적인 접근 방식은 쿠키 기반 서버측 세션입니다. 사용자가 로그인하고 서버가 사용자를 인증하면 프로세스가 시작됩니다. 그 후, 서버는 세션 ID를 사용하여 세션을 생성하고 이를 서버 메모리 어딘가에 저장합니다. 서버는 세션 ID를 클라이언트에 다시 보내고 클라이언트는 세션 ID를 쿠키에 저장합니다. 모든 요청에 대해 클라이언트는 요청의 일부로 세션 ID를 보내고 서버는 메모리의 세션 ID와 이 세션과 관련된 사용자 권한을 확인합니다.

세션 기반 인증

토큰

또 다른 인기 있는 접근 방식은 인증을 위해 토큰을 사용하는 것입니다. 프로세스는 사용자가 로그인과 비밀번호를 입력하고 클라이언트가 서버에 로그인 요청을 보낼 때 유사하게 시작됩니다. 세션을 생성하는 대신 서버는 비밀 토큰으로 서명된 토큰을 생성합니다. 그런 다음 서버는 토큰을 클라이언트에 다시 보내고 클라이언트는 이를 로컬 저장소에 저장해야 합니다. 세션 기반 접근 방식과 유사하게 클라이언트는 모든 요청에 대해 서버에 토큰을 보내야 합니다. 그러나 서버는 사용자 세션에 대한 추가 정보를 저장하지 않습니다. 서버는 토큰이 생성되고 비밀 키로 서명된 이후 토큰이 변경되지 않았는지 확인해야 합니다.

토큰 기반 인증

세션 대 토큰

세션 기반 권한 부여 접근 방식은 CSRF(교차 사이트 요청 위조)라는 공격에 취약할 수 있습니다. 이는 공격자가 결제 제출이나 비밀번호 변경 등 의도하지 않은 작업을 수행하기 위해 로그인한 사이트를 가리키는 일종의 공격입니다.


또 다른 점은 세션 기반 인증 접근 방식을 사용할 때 클라이언트와 서버 간에 상태 저장 세션이 생성된다는 것입니다. 문제는 클라이언트가 동일한 애플리케이션 범위에 있는 다른 서버에 액세스하려는 경우 해당 서버가 세션 상태를 공유해야 한다는 것입니다. 또 다른 경우에는 세션이 다르기 때문에 클라이언트는 각 서버에서 권한을 부여받아야 합니다.

세션 기반 인증 상태 공유


반면, 토큰 기반 권한 부여 접근 방식은 서버 측에 세션 데이터를 저장할 필요가 없으며 여러 서버 간의 권한 부여를 단순화할 수 있습니다.


그러나 공격자가 토큰을 훔칠 수 있으며 토큰을 무효화하는 것도 어려울 수 있습니다. 이 문서에서 자세한 내용과 무효화 처리 방법을 살펴보겠습니다.

JWT

JWT(JSON 웹 토큰)는 당사자 간에 정보를 JSON 개체로 안전하게 전송하기 위한 간결하고 독립적인 방법을 정의하는 개방형 표준입니다. 이 정보는 디지털 서명이 되어 있으므로 확인하고 신뢰할 수 있습니다. JWT는 비밀( HMAC 알고리즘 사용) 또는 RSA 또는 ECDSA를 사용하는 공개/개인 키 쌍을 사용하여 서명할 수 있습니다.

JWT 구조

JSON 웹 토큰은 점으로 구분된 세 부분으로 구성됩니다 .


  • 머리글
 { "alg": "HS256", "typ": "JWT" }

헤더는 일반적으로 토큰 유형과 사용되는 서명 알고리즘의 두 부분으로 구성됩니다.


  • 유효 탑재량
 { "sub": "1234567890", "name": "John Doe", "admin": true }

페이로드에는 사용자에 대한 설명인 클레임이 포함되어 있습니다. 그런 다음 페이로드는 Base64Url로 인코딩되어 JSON 웹 토큰의 두 번째 부분을 구성합니다. 여기에서 클레임으로 사용되는 표준 필드에 대한 설명을 찾을 수 있습니다.


  • 서명
 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

서명 부분을 생성하려면 인코딩된 헤더, 인코딩된 페이로드, 비밀, 헤더에 지정된 알고리즘을 가져와서 서명해야 합니다.


토큰은 일반적으로 다음과 같습니다.

xxxxx.yyyyy.zzzzz


jwt.io 로 이동하여 샘플 토큰이나 자체 토큰을 디버깅할 수 있습니다. 토큰을 인코딩된 필드에 붙여넣고 토큰 서명 알고리즘을 선택하면 됩니다.

jwt.io 디버거

.NET 프로젝트

이제 JWT의 작동 방식에 대한 이론적 지식을 얻었으므로 이를 실제 프로젝트에 적용할 수 있습니다. 커피 엔터티에 대한 CRUD 작업을 나타내는 간단한 API가 있다고 가정해 보겠습니다. Coffee API를 나타내는 ASP.NET Core API 프로젝트를 만들겠습니다. 그런 다음 JWT를 생성할 수 있는 ID API를 나타내는 또 다른 ASP.NET Core API 프로젝트를 만듭니다. 실제 생활에서는 인증/권한 부여 목적으로 Identity Server , Okta 또는 Auth0을 사용할 것입니다. 그러나 JWT를 생성하는 방법을 보여주기 위해 자체 Identity API를 만들겠습니다. Identity API가 완료되면 해당 컨트롤러를 호출하고 사용자 데이터를 기반으로 JWT를 생성할 수 있습니다. 또한 각 요청마다 JWT를 전달해야 하는 인증 구성을 통해 Coffee API를 보호할 수 있습니다.

.NET 프로젝트 환경

커피 API

먼저 Coffee API를 나타내는 간단한 ASP.NET Core API 프로젝트를 만들어 보겠습니다. 이 프로젝트의 구조는 다음과 같습니다.

커피 API - 프로젝트 구조


Model 폴더의 Coffee.cs 부터 시작해 보겠습니다. IdName 속성이 있는 간단한 엔터티입니다.

 namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }


API로 작업하는 동안 엔터티를 저장해야 합니다. 그럼 간단한 인메모리 저장소를 소개하겠습니다. Data 폴더의 Storage.cs 파일에 있습니다.

 namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }


Coffee API에 대한 요청을 나타내는 클래스가 필요합니다. 그럼 Contracts 폴더에 CoffeeRequest.cs 생성해 보겠습니다.

 namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }


완료되면 커피 엔터티에 대한 CRUD 작업을 나타내는 Controller 폴더에 CoffeeController.cs 구현할 수 있습니다.

 using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }


Coffee API가 완료되었으며, 프로젝트를 실행하면 다음과 같이 Swagger UI를 볼 수 있습니다.

커피 API - 스웨거 UI

ID API

Identity API를 나타내는 또 다른 ASP.NET Core API 프로젝트를 만들어 보겠습니다. 이 프로젝트의 구조는 다음과 같습니다.

ID API - 프로젝트 구조

EmailPassword 속성을 사용하여 새 JWT 생성 요청을 나타내는 Contracts 폴더의 TokenGenerationRequest.cs 부터 시작해 보겠습니다.

 namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }


JWT 생성 논리를 나타내는 TokenController.cs 만 구현하면 됩니다. 하지만 그러기 전에 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 패키지를 설치해야 합니다.

 using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }


SecretKey , IssuerAudience 와 같은 민감한 const는 구성 어딘가에 배치되어야 합니다. 이 테스트 프로젝트를 단순화하기 위해 하드코딩되었습니다. Lifetime 필드는 20분으로 설정되어 있으며 이는 토큰이 해당 시간 동안 유효함을 의미합니다. 이 매개변수를 구성할 수도 있습니다.


이제 프로젝트를 실행하고 다음과 같이 Swagger UI를 볼 수 있습니다.

ID API - Swagger UI


/token 엔드포인트를 호출하고 새 JWT를 생성해 보겠습니다. 다음 페이로드를 사용해 보세요.

 { "email": "[email protected]", "password": "password" }


Identity API는 해당 JWT를 생성합니다.

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM

Coffee API에서 인증 활성화

이제 Identity API가 준비되어 토큰을 제공하면 Coffee API를 인증으로 보호할 수 있습니다. 다시 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 패키지를 설치해야 합니다.


인증 서비스별로 필요한 서비스를 등록해야 합니다. 빌더를 생성한 직후 Program.cs 파일에 다음 코드를 추가합니다.

 var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();


미들웨어에서는 순서가 중요하다는 점을 기억하는 것이 중요합니다. AddAuthentication() 메서드를 호출하고 JwtBearerDefaults.AuthenticationScheme 인증 스키마로 지정하여 인증을 활성화합니다. Bearer 값을 포함하는 상수입니다.

 namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }


승인 중에 유효성을 검사할 JWT 매개변수를 설명하는 TokenValidationParameters 지정해야 합니다. 또한 JWT 서명을 확인하기 위해 Identity API의 signingCredentials 와 유사한 IssuerSigningKey 지정합니다. TokenValidationParameters 에 대한 자세한 내용은 여기에서 확인하세요.


다음 코드 부분은 인증 및 권한 부여 기능을 활성화하는 미들웨어를 빌더에 추가합니다. UseHttpsRedirection()MapControllers() 메서드 사이에 추가해야 합니다.

 app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();


이제 컨트롤러나 해당 작업에 Authorize 속성을 사용할 수 있습니다. 이 코드를 적용하면 이제 CoffeeController 의 모든 작업이 인증 메커니즘으로 보호되며 JWT가 요청의 일부로 전송되어야 합니다.

 [Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..


Coffee API의 엔드포인트를 호출하면 HttpContext.User 디버깅하고 JWT에서 지정한 클레임이 포함된 Identity 채워져 있는지 확인할 수 있습니다. ASP.NET Core가 내부적으로 권한 부여를 처리하는 방법을 이해하는 것이 중요합니다.

Coffee API - 클레임은 JWT에서 채워집니다.

Swagger UI에 승인 추가

우리는 인증을 통해 Coffee API를 보호하기 위해 많은 노력을 기울였습니다. 그러나 Coffee API 프로젝트를 실행하고 Swagger UI를 열면 요청의 일부로 JWT를 보낼 수 없습니다. 이 문제를 해결하려면 다음 코드를 사용하여 Program.cs 파일을 업데이트해야 합니다.

 builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });


그 후 오른쪽 상단에 승인 버튼이 표시됩니다.

Coffee API - 승인 버튼이 나타납니다.


Authorize 버튼을 클릭하면 다음과 같이 JWT를 입력할 수 있습니다.

커피 API - JWT 값 입력

테스트에 Postman 사용

Swagger UI만 사용할 수는 없으며 Postman 도구를 통해 API 테스트를 수행할 수 있습니다. 먼저 Identity API의 /token 엔드포인트를 호출해 보겠습니다. JSON을 페이로드로 사용할 것이므로 Headers 섹션에 application/json 값으로 Content-Type 헤더를 지정해야 합니다.

ID API - 헤더 지정


그런 다음 /token 엔드포인트를 호출하고 새 JWT를 얻을 수 있습니다.

ID API - JWT 생성


이제 JWT를 복사하여 Coffee API를 호출하는 데 사용할 수 있습니다. 엔드포인트를 테스트, 생성 및 업데이트하려면 Identity API와 유사한 Content-Type 헤더를 지정해야 합니다. Authorization 헤더도 Bearer [your JWT value] 값으로 설정되어야 합니다. 그런 다음 보내기 버튼을 누르고 결과를 확인하세요.

Coffee API - 모든 엔터티 가져오기

역할 기반 인증

기억하시겠지만, JWT의 페이로드 부분은 정확히 키-값 쌍인 값을 가진 클레임 집합입니다. 역할 기반 인증을 사용하면 사용자가 속한 역할에 따라 애플리케이션 리소스에 대한 액세스를 차별화할 수 있습니다.


역할에 대한 새 클레임을 추가하는 코드로 Identity API의 TokenController.cs 파일에 있는 Create() 메서드를 업데이트하면 다음과 같습니다. Coffee API에서 역할 기반 인증을 처리할 수 있습니다. ClaimTypes.Role 은 역할 청구의 사전 정의된 이름입니다.

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };


역할 이름을 지정하는 CoffeeController.cs 파일에서 Authorize 속성을 업데이트합니다.

 [Authorize(Roles = "Barista")]


이제 Coffee API를 호출하는 모든 사용자는 Barista 값이 포함된 역할 소유권을 가져야 합니다. 그렇지 않으면 403 Forbidden 상태 코드가 표시됩니다.

청구 기반 승인

Authorize 속성은 역할 기반 인증을 쉽게 처리할 수 있습니다. 하지만 그것만으로는 충분하지 않고 나이 등 일부 사용자 속성을 기반으로 액세스를 차별화하고 싶다면 어떻게 해야 할까요? JWT에 클레임을 추가하고 이를 사용하여 인증 논리를 구축할 수 있다는 것을 이미 짐작하셨을 것입니다. 역할 기반 권한 부여 자체는 역할이 미리 정의된 유형의 동일한 클레임 개체인 것처럼 클레임 기반 권한 부여의 특별한 경우입니다.


새로운 클레임 IsGourmet 추가하는 코드를 사용하여 Identity API의 TokenController.cs 파일에 있는 Create() 메서드를 업데이트해 보겠습니다.

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };


Coffee API의 Program.cs 파일에서 청구를 확인하고 Authorize 속성에서 사용할 수 있는 정책을 생성해야 합니다. AddAuthentication() 메서드 호출 직후에 다음 코드를 추가해야 합니다.

 builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });


정책 이름을 지정하여 CoffeeController.cs 파일에서 Authorize 속성을 업데이트합니다.

 [Authorize(Policy = "OnlyForGourmet")]

요약

축하해요! .NET에서 JWT를 배우느라 많은 노력을 기울였습니다. 이제 JWT 원칙과 이를 사용하여 .NET 애플리케이션에서 인증을 수행하는 것이 중요한 이유를 확실히 이해해야 합니다. 그러나 우리는 ASP.NET Core 애플리케이션의 인증 및 권한 부여 영역에 대해서만 살펴보았습니다.


이 문서에서 논의한 주제와 관련하여 Microsoft 설명서를 살펴보는 것이 좋습니다. .NET 플랫폼에는 권한 부여 및 역할 관리를 위한 기본 제공 기능도 많이 있습니다. 이 문서에 추가로 인증에 대한 Microsoft 설명서를 추가할 수 있습니다.