재우니의 블로그

마이크로 컨텍스트 아키텍처를 개발할 때 고려해야 할 사항 중 하나는 사용자가 작업을 수행하려고 할 때 전체 시스템에서 사용자를 인증하는 것입니다.

 

 

다이어그램에서 마이크로 서비스 아키텍처의 기본 현황을 볼 수 있습니다. 웹 / 모바일 사용자는 API Gateway (개발 된 AWS / Azure 등의 서비스에서 제공 한 API 게이트웨이)를 통해 시스템과 통신하고 request(요청)은 해당 서비스로 전달됩니다. 그 사이에 사용자가 microservice(마이크로 서비스)에서 해당 특정 리소스에 액세스 할 수 있는지 확인해야합니다.

이를 수행하기 위해 사용자 관리 및 인증(authentication)을 담당하는 분리된 각각의 마이크로서비스(microservice)를 소유할 수 있습니다. 각각의 요청에서, 사용자는 JWT 를 제공 할 것이다 ; API Gateway 는 먼저 토큰을 인증 마이크로 서비스(authentication microservice)에 전송하고 결과가 긍정적이면 요청이 필요할 때마다 (예 : 주문 또는 결제 서비스) 전달됩니다.

서비스 구축하기

Visual Studio에서 "ASP.NET Core Web Application" 프로젝트를 만들고 이름과 경로를 선택하십시오.

“API”를 선택하고 나머지 옵션은 기본값으로 두고 이제 생성 create 버튼을 선택합니다.

설치하는 동안에 추가적으로 설치해야 할 4 개의 package 가 있습니다. 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 "NuGet 패키지 관리…"를 클릭하고 다음을 검색하십시오.

  • Microsoft.EntityFrameworkCore — 데이터베이스 통신을 위한 용도임
  • Microsoft.EntityFrameworkCore.Tools — “Package Manager Console” 을 사용하여 EntityFramework 와 상호작용을 위한 툴임
  • Microsoft.EntityFrameworkCore.SqlServer — SQL database 을 EntityFramework 에 설정하기 위한 용도임
  • Microsoft.AspNetCore.Authentication.JwtBearer — JWT 를 생성(generating) 하고 확인(verifying) 하는 용도임

Model 과 database 구축

SQL 서버와 데이터베이스가 없는 경우, 마이크로 소프트 SQL Server Management Studio를 설치하고 로컬 데이터베이스를 만듭니다 . Visual Studio에서 View-> SQL Server Object Explorer를 클릭하면 아래와 같이 볼 수 있습니다.

데이터베이스를 마우스 오른쪽 버튼으로 클릭하고 "속성(Properties)"을 클릭하면 "연결 문자열(Connection string)"이 표시됩니다. appsettings.json 파일을 열고 <your-connection-string> 부분에 복사한 연결 문자열을 추가하십시오.

{
  "ConnectionStrings": {
    "DefaultConnection": "<your-connection-string>"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

 

설정이 완료되면 프로젝트에 "Models"폴더를 만들 수 있습니다. 폴더 내부에 "User"라는 클래스를 만듭니다.

namespace JWTMicroNetCore.Models
{
    public class User
    {
        public int Id { get; set; } 
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

이 샘플 예제 프로젝트는 인증(authentication)을 설정하는 유일한 방법이 아니라, 서비스를 구성하는 방법의 하나의 샘플 예제입니다. microsoft 가 제공하는 기본 제공 시스템을 원한다면 ASP.NET Core Identity를 사용할 수 있습니다 .

그런 다음 “Data”라는 새로운 폴더를 생성하고 “ApplicationDbContext”라는 클래스를 만듭니다. 이는 모든 데이터베이스 통신을 수행하게 될 저장소 역할을 하게 됩니다.

using JWTMicroNetCore.Models;
using Microsoft.EntityFrameworkCore;

namespace JWTMicroNetCore.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<User> Users { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder
                .Entity<User>()
                .HasData(
                new User { Id = 1, Username = "User1", Password = "Password1" },
                new User { Id = 2, Username = "User2", Password = "Password2" }
                );
        }
    }
}

 

이 클래스는 Entity Framework에서 제공되는 "DbContext"에서 상속하게 됩니다. 생성자는 "DbContextOptions"매개 변수를 사용하며 "Startup"클래스에서 주입(inject) 됩니다.

또한 데이터베이스의 "Users"테이블을 나타내는 "User"개체의 "DbSet" 도 존재합니다.

또한 “OnModelCreating” 메소드도 존재하는데요. 이는 나중에 테스트하기 쉽도록 두 개의 사용자 Entity 으로 데이터베이스를 seed 하는 데 사용됩니다.

마지막 단계는 컨텍스트(context) 를 주입(inject ) 하는 것입니다. "Startup.cs" 파일을 열고 "ConfigureServices"메소드에  4개 행의 코드를 추가하십시오.

 // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });
            
            services.AddControllers();
        }

마이그래이션 하기

먼저 마이그래이션을 하기 위해서는 패키지 관리자 콘솔을 실행해서, 아래의 구분을 복사해서 실행합니다.

Add-Migration Users

 

실해 완료 후, Users 라는 새로운 클래스 파일이 생성되며, 파일을 열어보면 내부에 아래의 코드가 기재되어 있는것을 보실 수 있습니다.

using Microsoft.EntityFrameworkCore.Migrations;

namespace JWTMicroNetCore.Migrations
{
    public partial class Users : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Users",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Username = table.Column<string>(nullable: true),
                    Password = table.Column<string>(nullable: true),
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Users", x => x.Id);
                });

            migrationBuilder.InsertData(
                table: "Users",
                columns: new[] { "Id", "Password", "Username" },
                values: new object[] { 1, "Password1", "User1" });

            migrationBuilder.InsertData(
                table: "Users",
                columns: new[] { "Id", "Password", "Username" },
                values: new object[] { 2, "Password2", "User2" });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Users");
        }
    }
}

마이그레이션할 Users 클래스 파일이 생성되었으니 해당 클래스를 토대로 데이터베이스에 테이블 생성 및 데이터 insert 가 적용해야합니다. 이는 코드를 토대로 SQL로 변환하여 연결된 데이터베이스에서 실행합니다. 이제 이와 같은 시나리오로 수행할려면 아래 구문을 패키지 관리자 콘솔에 입력하여 실행합니다.

Update-Database

 

테이블이 올바르게 생성 되었는지 확인하려면 보기 메뉴 에서 SQL Server 오브젝트 탐색기  열고, 로컬 데이터베이스를 열고 테이블을 확인해 보세요. 클래스명과 동일하게 Users 라는 테이블과 id 가 1 과 2 라는 2개의 row 가 추가되어 있는것을 확인 해 보세요. 

JWTs  작업하기

There are 2 parts when it comes to working with JWTs in our service: creating the tokens and sending them to the client when a user logs in, and verifying whether a token is valid or not.

For the first part, create a new folder called “Services”. Inside, create an interface called “ITokenBuilder” and a class “TokenBuilder” that implements it. This will contain the logic for generating tokens based on usernames:

서비스에서 JWT와 관련하여 작업 할 때 토큰을 생성하고 사용자가 로그인 할 때 클라이언트로 토큰을 전송하고 토큰이 유효한지 여부를 확인하는 두개의 부분이 있습니다.

우선 첫번째 부분을 구성하기 위해 “Services”라는 새 폴더를 만듭니다. 내부에 "ITokenBuilder"라는 인터페이스와 이를 구현할 "TokenBuilder"클래스를 만듭니다. 여기에는 사용자 이름을 기반으로 토큰을 생성하는 로직이 포함됩니다.

namespace JWTMicroNetCore.Services
{
    public interface ITokenBuilder
    {
        string BuildToken(string username);
    }
}
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JWTMicroNetCore.Services
{
    public class TokenBuilder : ITokenBuilder
    {
        public string BuildToken(string username)
        {
            var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("placeholder-key-that-is-long-enough-for-sha256"));
            var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
            var claims = new Claim[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, username),
            };
            var jwt = new JwtSecurityToken(claims: claims, signingCredentials: signingCredentials);
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            return encodedJwt;
        }
    }
}

"BuildToken"메소드는 HmacSha256 알고리즘과 대칭 키를 사용하여 JWT를 생성하는 표준 방법을 구현합니다. 키 문자열이 충분히 길어야 합니다. 그렇지 않으면 예외가 발생합니다.

이 경우 JWT에 사용 된 유일한 클레임은“sub”(Subject)이며, 이는 사용자의 사용자 이름으로 설정됩니다.

두 번째 부분에서는 다시 “ConfigureServices” 메소드를 사용하여 JWT를 확인합니다. 동일한 대칭 키를 사용하고 토큰을 확인하도록 인증을 구성하고, 메소드의 로직에 들어가기 전에 특정 endpoint(엔드 포인트)가 점검을 수행하도록 지시합니다. "ConfigureServices"메소드에 다음과 같이 코드를 입력해 주세요. ( "TokenBuilder"클래스도 주입(inject) 됨).

         // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });

            services
               .AddAuthentication(options =>
               {
                   options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                   options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
               })
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = true;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.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();
        }

 

endpoints 생성하기

With the JWT creation/verification set up, the final step is to create the endpoints that will be called by the API Gateway. In the “Controllers” folder, create a new controller called “AuthenticationController” — choose the “API Controller — Empty” option:

JWT 생성 / 검증이 설정되면 마지막 단계는 API 게이트웨이에서 호출 할 endpoints 를 작성하는 것입니다. "Controllers"폴더에서 "AuthenticationController"라는 신규 컨트롤러를 생성합니다. "API Controller — Empty"옵션을 선택하십시오.

먼저 “ApplicationDbContext” 와 “ITokenBuilder” 를 주입하면, 데이터베이스에 접근이 가능하고 controller 내부에서 토큰을 생성 할 수 있습니다.

using JWTMicroNetCore.Models;
using JWTMicroNetCore.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace JWTMicroNetCore.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticationController : ControllerBase
    {
        private readonly ApplicationDbContext _context;
        private readonly ITokenBuilder _tokenBuilder;

        public AuthenticationController(
            ApplicationDbContext context,
            ITokenBuilder tokenBuilder)
        {
            _context = context;
            _tokenBuilder = tokenBuilder;
        }
    }
}

Login

먼저 endpoints 는 로그인에 사용됩니다. 사용자는 API 게이트웨이를 통해 credentials 정보를 제공하고, 인증정보가 정확하다면 토큰이 생성됩니다.

 [HttpPost("login")]
        public async Task<IActionResult> Login([FromBody]User user)
        {
            var dbUser = await _context
                .Users
                .SingleOrDefaultAsync(u => u.Username == user.Username);

            if (dbUser == null)
            {
                return NotFound("User not found.");
            }

            // This is just an example, made for simplicity; do not store plain passwords in the database
            // Always hash and salt your passwords
            var isValid = dbUser.Password == user.Password;

            if (!isValid)
            {
                return BadRequest("Could not authenticate user.");
            }

            var token = _tokenBuilder.BuildToken(user.Username);

            return Ok(token);
        }

endpoint 는 POST 메소드를 통해 액세스되며 request 의 body 을 통해 "User" 오브젝트를 받게 됩니다. 먼저, 사용자 이름이 데이터베이스에 존재하는지 여부를 확인하고 그렇지 않은 경우 "Not Found"응답을 반환합니다. 그런 다음 데이터베이스와 비밀번호를 확인합니다.
중요 : 이것은 단순한 예제 일 뿐이며 실제 개발에서 작동하는 방법에 맞지 않습니다. 데이터베이스에 비밀번호를 삽입하기 전에 항상 비밀번호를 Hash 하고 Salt 하세요. 암호가 유효하지 않으면 "Bad Request"응답이 반환됩니다. 마지막으로 모든 것이 정상이면 "ITokenBuilder"인스턴스를 사용하여 토큰이 생성되고 "OK" 응답과 함께 토큰값을 반환됩니다.

Verifying

두 번째 endpoint 제공된 토큰이 유효한지 (올바르게 서명되었으며 기존 사용자에 해당) 여부를 확인하는 데 사용됩니다. 메소드 이름 위에 [Authorize] 속성에 주목하십시오 . 이렇게 하면 ASP.NET이 앞서 Startup”에서 추가 한 권한 부여 검사를 수행합니다. 그러면 올바른 비밀 및 알고리즘을 사용하여 JWT에 서명하게 됩니다.

 [HttpGet("verify")]
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        public async Task<IActionResult> VerifyToken()
        {
            var username = User
                .Claims
                .SingleOrDefault();

            if (username == null)
            {
                return Unauthorized();
            }

            var userExists = await _context
                .Users
                .AnyAsync(u => u.Username == username.Value);

            if (!userExists)
            {
                return Unauthorized();
            }

            return NoContent();
        }

 

실제 method 내부코드를 보면 "ControllerBase"의 일부이고, JWT를 기반으로 현재 사용자에 대한 정보를 포함하는 "User" 객체로 부터 "Claims"를 가져옵니다. 클레임(Claims)은 하나뿐이므로 또 다른 where 조건을 따질 필요가 없습니다. 사용자 이름 값 기반으로써 데이터베이스에 해당 사용자 이름을 가진 사용자가 있는지 확인합니다. 그렇지 않은 경우 "Unauthorized" 응답을 반환합니다. 해당 사용자 이름이 존재하면  “No Content”를 반환함과 동시에 성공했음을 의미합니다.

Testing

Postman 또는 HTTP 요청을 보내는 다른 애플리케이션을 사용하여  서비스를 테스트 할 수 있습니다. F5를 눌러 Visual Studio에서 응용 프로그램을 실행하고 브라우저 창에서 URI를 복사 한 다음 먼저 "login" endpoints 을 호출하겠습니다.

 

올바른 사용자 이름-암호을 보내면 토큰이 반환되며, 상태값은 200 이며 ok 로 전달됩니다.

이제 "verify" endpoints를 사용하여 토큰을 테스트 해 보겠습니다. "Headers"탭에서 "Bearer <your-token>"을 "Authorization"으로 추가하십시오.

"No Content"응답이 수신되며, 상태값은 204 입니다. 토큰값이 잘못된 경우는 상태값이 401 이며 unauthorized 로 표기됩니다.

다시 login 으로 가서 패스워드를 틀리게 수정하여 전송해 보겠습니다.

존재하지 않은 사용자 이름으로 판명되어 Could not authenicatite user. 라는 메시지를 제공하며, 상태값이 400 코드 즉, Bad Request 라고 전달합니다.

번역이 깔끔하지 않을 수 있습니다. 원본 블로그는 아래 남겨 두겠습니다. 

https://medium.com/streamwriter/jwt-authentication-for-microservices-in-asp-net-core-b350c1a3e9fa

 

JWT Authentication for Microservices in ASP.NET Core

Two years ago, I published the JWT Authentication for Microservices in .NET article, and it got some pretty good traction. In the meantime…

medium.com

https://github.com/Mirch/JwtAuthenticationService

 

Mirch/JwtAuthenticationService

Basic example for creating an authentication service based on JWT in ASP.NET Core. - Mirch/JwtAuthenticationService

github.com