JSON Web Token(JWT) 적용

JSON Web Token(JWT) 적용

패키지 추가

plaintext
패키지 추가
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.IdentityModel.Tokens
System.IdentityModel.Tokens.Jwt
Microsoft.AspNetCore.Identity.EntityFrameworkCore

appsettings.json 변경

appsettings.json

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "NZWalksConnectionString": "Server=[서버],[포트];Database=[DB이름];User ID=[DB아이디];Password=[DB비];Trusted_Connection=True;TrustServerCertificate=True"
  },
  "Jwt": {
    "key": "TestCustomKey",
    "Issuer": "https://localhost:7256",
    "Audience": "https://localhost:7256"
  }
}

서비스 등록을 통한 의존성 주입(Dependency Injection, DI)

Program.cs

csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore; // Entity Framework Core 기능을 사용하기 위한 네임스페이스
using Microsoft.IdentityModel.Tokens;
using NZWalks.API.Data;
using NZWalks.API.Mappings;
using NZWalks.API.Models.Domain;
using NZWalks.API.Repositories;
using System.Text; // 데이터 컨텍스트를 사용하기 위한 네임스페이스

var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더 객체 생성

// Add services to the container.
builder.Services.AddControllers(); // 컨트롤러 서비스를 추가하여 MVC 패턴을 지원

// Swagger/OpenAPI 설정에 대한 더 많은 정보를 얻으려면 https://aka.ms/aspnetcore/swashbuckle 참고
builder.Services.AddEndpointsApiExplorer(); // API 탐색기를 추가하여 API 문서를 자동 생성
builder.Services.AddSwaggerGen(); // Swagger 생성을 위한 서비스 추가

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksConnectionString")));

builder.Services.AddScoped<IRegionRepository, SQLRegionRepository>(); // Main DB Region
builder.Services.AddScoped<IWalkRepository, SQLWalkRepository>(); // Main DB Walk
// builder.Services.AddScoped<IRegionRepository, InMemoryRegionRepository>(); // Test DB (In Memory)
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles));

// JWT 인증 설정 추가
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // JWT Bearer 인증 스키마 사용
    .AddJwtBearer(options => // JWT Bearer 인증 설정
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuer = true, // 발행자(issuer) 유효성 검사 활성화
        ValidateAudience = true, // 수신자(audience) 유효성 검사 활성화
        ValidateLifetime = true, // 토큰의 유효 기간 검사 활성화
        ValidateIssuerSigningKey = true, // 서명 키 유효성 검사 활성화
        ValidIssuer = builder.Configuration["Jwt:Issuer"], // 유효한 발행자 설정
        ValidAudience = builder.Configuration["Jwt:Audience"], // 유효한 수신자 설정
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) // 서명 키 설정
    });

var app = builder.Build(); // 애플리케이션 빌드

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    // 개발 환경에서만 Swagger를 사용하도록 설정
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection(); // HTTP 요청을 HTTPS로 리디렉션

app.UseAuthentication(); // JWT 인증 미들웨어 추가
// 이 코드는 JWT 인증을 처리하여 사용자 요청을 검증함

app.UseAuthorization(); // 권한 부여 미들웨어 사용
// 인증된 사용자의 권한을 검증함

app.MapControllers(); // 컨트롤러에 대한 요청을 매핑

app.Run(); // 애플리케이션 실행
JSON Web Token 파트
JSON Web Token 파트
UseAuthentication(); 추가로 사용자 요청을 검증
UseAuthentication(); 추가로 사용자 요청을 검증

Controller에 [Authorize] 적용

[Authorize] 적용
[Authorize] 적용
csharp
using Microsoft.AspNetCore.Mvc;
using NZWalks.API.Data;
using NZWalks.API.Models.Domain;
using NZWalks.API.Models.DTO;
using AutoMapper;
using NZWalks.API.CustomActionFilter;
using Microsoft.AspNetCore.Authorization;

namespace NZWalks.API.Controllers
{

    [Route("api/[controller]")] // 컨트롤러의 기본 Route를 설정. [controller]는 컨트롤러 이름으로 대체됨.
    [ApiController] // API 컨트롤러 특성 적용
    [Authorize] // 인증된 사용자만 접급 가능
    public class RegionsController : ControllerBase
    {

        private readonly NZWalksDbcontext dbcontext;
        private readonly IRegionRepository regionRepository;
        private readonly IMapper mapper;

        public RegionsController(NZWalksDbcontext dbContext, IRegionRepository regionRepository, IMapper mapper)
        {
            this.dbcontext = dbContext;
            this.regionRepository = regionRepository;
            this.mapper = mapper;
        }

        // GET ALL REGIONS
        // GET: https://localhost:1234/api/regions
        [HttpGet]
        public async Task<IActionResult> GetAll()
        {
            // Get Data From Database - Domain models
            // var regionDomain = await dbcontext.Regions.ToListAsync(); // Change Async
            var regionDomain = await regionRepository.GetAllAsync();

            // Map Domain Models to DTOs
            // var regionDto = new List<RegionDto>();
            // foreach (var region in regionDomain) 
            // {
            //     regionDto.Add(new RegionDto() 
            //     {
            //         Id = region.Id,
            //         Code = region.Code,
            //         Name = region.Name,
            //         RegionImageUrl = region.RegionImageUrl
            //     });
            // }

            // Map Domain Models to DTOs
            var regionDto = mapper.Map<List<RegionDto>>(regionDomain);

            return Ok(regionDto);
        }

        // GET SINGLE REGION (Get Region By ID)
        // GET: https://localhost:1234/api/regions/{id}
        [HttpGet]
        [Route("{id:guid}")]
        public async Task<IActionResult> GetById([FromRoute]Guid id) 
        {
            // Get Data From Database - Domain models
            // var region = dbcontext.Regions.Find(id);
            // var regionDomain = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);
            var regionDomain = await regionRepository.GetByIdAsync(id);
            if (regionDomain == null)
            {
                return NotFound();
            }

            // Map Domain Models to DTOs
            // var regionDto = new RegionDto() 
            // {
            //     Id = regionDomain.Id,
            //     Code = regionDomain.Code,
            //     Name = regionDomain.Name,
            //     RegionImageUrl = regionDomain.RegionImageUrl
            // };

            var regionDto = mapper.Map<RegionDto>(regionDomain);

            return Ok(regionDto);

        }

        // POST To Create New Region
        // POST : https://localhost:1234/api/regions
        [HttpPost]
        [ValidateModel]
        public async Task<IActionResult> Create([FromBody] AddRegionRequestDto addRegionRequestDto)
        {
            /* [ValidateModel] 적용전
                        if (ModelState.IsValid) // [ValidateModel] 
            {
                // [Require]
                // string Code, Name
                // string? RegionImageUrl

                // Map or Convert DTO to Domain Model
                //var regionDomainModel = new Region
                //{
                //    Code = addRegionRequestDto.Code,
                //    Name = addRegionRequestDto.Name,
                //    RegionImageUrl = addRegionRequestDto.RegionImageUrl
                //};

                // Map or Convert DTO to Domain Model
                var regionDomainModel = mapper.Map<Region>(addRegionRequestDto);

                // Use Domain Model to create Region
                // await dbcontext.Regions.AddAsync(regionDomainModel);
                // await dbcontext.SaveChangesAsync();

                await regionRepository.CreateAsync(regionDomainModel);


                // Map Domain model back to DTO
                //var regionDto = new RegionDto 
                //{ 
                //    Id = regionDomainModel.Id,
                //    Code = addRegionRequestDto.Code,
                //    Name = regionDomainModel.Name, 
                //    RegionImageUrl = regionDomainModel.RegionImageUrl

                //};

                var regionDto = mapper.Map<RegionDto>(regionDomainModel);

                // 201 Created 응답으로 생성된 DTO 반환
                return CreatedAtAction(nameof(GetById), new { id = regionDto.Id }, regionDto);
                // CreatedAtAction은 ASP.NET Core에서 HTTP POST 요청 후 201 Created 상태 코드를 반환할 때 사용되는 메서드
                // 새로운 리소스를 생성한 후, 해당 리소스에 접근할 수 있는 URI를 함께 제공하여 클라이언트가 리소스를 쉽게 찾을 수 있음
            }
            else
            {
                return BadRequest(ModelState);
            }
             
            */

            // [Require]
            // string Code, Name
            // string? RegionImageUrl

            // Map or Convert DTO to Domain Model
            //var regionDomainModel = new Region
            //{
            //    Code = addRegionRequestDto.Code,
            //    Name = addRegionRequestDto.Name,
            //    RegionImageUrl = addRegionRequestDto.RegionImageUrl
            //};

            // Map or Convert DTO to Domain Model
            var regionDomainModel = mapper.Map<Region>(addRegionRequestDto);

            // Use Domain Model to create Region
            // await dbcontext.Regions.AddAsync(regionDomainModel);
            // await dbcontext.SaveChangesAsync();

            await regionRepository.CreateAsync(regionDomainModel);


            // Map Domain model back to DTO
            //var regionDto = new RegionDto 
            //{ 
            //    Id = regionDomainModel.Id,
            //    Code = addRegionRequestDto.Code,
            //    Name = regionDomainModel.Name, 
            //    RegionImageUrl = regionDomainModel.RegionImageUrl

            //};

            var regionDto = mapper.Map<RegionDto>(regionDomainModel);

            // 201 Created 응답으로 생성된 DTO 반환
            return CreatedAtAction(nameof(GetById), new { id = regionDto.Id }, regionDto);
            // CreatedAtAction은 ASP.NET Core에서 HTTP POST 요청 후 201 Created 상태 코드를 반환할 때 사용되는 메서드
            // 새로운 리소스를 생성한 후, 해당 리소스에 접근할 수 있는 URI를 함께 제공하여 클라이언트가 리소스를 쉽게 찾을 수 있음
        }



        // Update Region
        // PUT: https://localhost:1234/api/regions/{id}
        [HttpPut]
        [Route("{id:guid}")]
        [ValidateModel]
        public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] UpdateRegionRequestDto updateRegionRequestDto)
        {
            /* [ValidateModel] 적용 전
            if (ModelState.IsValid)
            {
                // Map 
                //var regionDomainModel = new Region
                //{
                //    Code = updateRegionRequestDto.Code,
                //    Name = updateRegionRequestDto.Name,
                //    RegionImageUrl = updateRegionRequestDto.RegionImageUrl

                //};

                var regionDomainModel = mapper.Map<Region>(updateRegionRequestDto);

                // Check if region exists
                // var regionDomainModel = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);
                regionDomainModel = await regionRepository.UpdateAsync(id, regionDomainModel);

                if (regionDomainModel == null)
                {
                    return NotFound();
                }

                //// Map DTO to Domain model
                //regionDomainModel.Code = updateRegionRequestDto.Code;
                //regionDomainModel.Name = updateRegionRequestDto.Name;
                //regionDomainModel.RegionImageUrl = updateRegionRequestDto.RegionImageUrl;

                //await dbcontext.SaveChangesAsync();

                // Convert Domain Model to DTO
                //var regionDto = new RegionDto
                //{
                //    Id = regionDomainModel.Id,
                //    Code = regionDomainModel.Code,
                //    Name = regionDomainModel.Name,
                //    RegionImageUrl = regionDomainModel.RegionImageUrl
                //};

                var regionDto = mapper.Map<RegionDto>(regionDomainModel);

                return Ok(regionDto);

            }
            else 
            { 
                return BadRequest(ModelState);
            }
            */
            // Map 
            //var regionDomainModel = new Region
            //{
            //    Code = updateRegionRequestDto.Code,
            //    Name = updateRegionRequestDto.Name,
            //    RegionImageUrl = updateRegionRequestDto.RegionImageUrl

            //};

            var regionDomainModel = mapper.Map<Region>(updateRegionRequestDto);

            // Check if region exists
            // var regionDomainModel = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);
            regionDomainModel = await regionRepository.UpdateAsync(id, regionDomainModel);

            if (regionDomainModel == null)
            {
                return NotFound();
            }

            //// Map DTO to Domain model
            //regionDomainModel.Code = updateRegionRequestDto.Code;
            //regionDomainModel.Name = updateRegionRequestDto.Name;
            //regionDomainModel.RegionImageUrl = updateRegionRequestDto.RegionImageUrl;

            //await dbcontext.SaveChangesAsync();

            // Convert Domain Model to DTO
            //var regionDto = new RegionDto
            //{
            //    Id = regionDomainModel.Id,
            //    Code = regionDomainModel.Code,
            //    Name = regionDomainModel.Name,
            //    RegionImageUrl = regionDomainModel.RegionImageUrl
            //};

            var regionDto = mapper.Map<RegionDto>(regionDomainModel);

            return Ok(regionDto);
        }


        // Delete Region
        // Delete https://localhost:1234/api/regions/{id}
        [HttpDelete]
        [Route("{id:guid}")]
        public async Task<IActionResult> Update([FromRoute] Guid id)
        {
            //var regionDomainModel = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);

            var regionDomainModel = await regionRepository.DeleteAsync(id);


            if (regionDomainModel == null)
            {
                return NotFound();
            }

            // dbcontext.Regions.Remove(regionDomainModel);
            // await dbcontext.SaveChangesAsync();

            // return deleted Region back
            // Convert Domain Model to DTO
            //var regionDto = new RegionDto
            //{
            //    Id = regionDomainModel.Id,
            //    Code = regionDomainModel.Code,
            //    Name = regionDomainModel.Name,
            //    RegionImageUrl = regionDomainModel.RegionImageUrl
            //};

            var regionDto = mapper.Map<Region>(regionDomainModel);

            return Ok(regionDto);
        }
    }
}
승인되지 않음 401
승인되지 않음 401

인증을 위한 DB 생성 및 연결

연결문자열 추가

appsettings.json

json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "NZWalksConnectionString": "Server=[서버],[포트];Database=[DB이름];User ID=[DB아이디];Password=[DB비];Trusted_Connection=True;TrustServerCertificate=True",
    "NZWalksAuthConnectionString": "Server=[서버],[포트];Database=[DB이름];User ID=[DB아이디];Password=[DB비];Trusted_Connection=True;TrustServerCertificate=True"
  },
  "Jwt": {
    "key": "TestCustomKey",
    "Issuer": "https://localhost:7256",
    "Audience": "https://localhost:7256"
  }
}

NZWalksDbcontext.cs 생성

csharp
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace NZWalks.API.Data
{
    public class NZWalksAuthDbcontext : IdentityDbContext
    {
        public NZWalksAuthDbcontext(DbContextOptions options) : base(options)
        {
        }
    }
}

의존성 주입(Dependency Injection, DI)

csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore; // Entity Framework Core 기능을 사용하기 위한 네임스페이스
using Microsoft.IdentityModel.Tokens;
using NZWalks.API.Data;
using NZWalks.API.Mappings;
using NZWalks.API.Models.Domain;
using NZWalks.API.Repositories;
using System.Text; // 데이터 컨텍스트를 사용하기 위한 네임스페이스

var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더 객체 생성

// Add services to the container.
builder.Services.AddControllers(); // 컨트롤러 서비스를 추가하여 MVC 패턴을 지원

// Swagger/OpenAPI 설정에 대한 더 많은 정보를 얻으려면 https://aka.ms/aspnetcore/swashbuckle 참고
builder.Services.AddEndpointsApiExplorer(); // API 탐색기를 추가하여 API 문서를 자동 생성
builder.Services.AddSwaggerGen(); // Swagger 생성을 위한 서비스 추가

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksConnectionString")));

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksAuthDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksAuthConnectionString")));


builder.Services.AddScoped<IRegionRepository, SQLRegionRepository>(); // Main DB Region
builder.Services.AddScoped<IWalkRepository, SQLWalkRepository>(); // Main DB Walk
// builder.Services.AddScoped<IRegionRepository, InMemoryRegionRepository>(); // Test DB (In Memory)
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles));

// JWT 인증 설정 추가
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // JWT Bearer 인증 스키마 사용
    .AddJwtBearer(options => // JWT Bearer 인증 설정
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuer = true, // 발행자(issuer) 유효성 검사 활성화
        ValidateAudience = true, // 수신자(audience) 유효성 검사 활성화
        ValidateLifetime = true, // 토큰의 유효 기간 검사 활성화
        ValidateIssuerSigningKey = true, // 서명 키 유효성 검사 활성화
        ValidIssuer = builder.Configuration["Jwt:Issuer"], // 유효한 발행자 설정
        ValidAudience = builder.Configuration["Jwt:Audience"], // 유효한 수신자 설정
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) // 서명 키 설정
    });

var app = builder.Build(); // 애플리케이션 빌드

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    // 개발 환경에서만 Swagger를 사용하도록 설정
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection(); // HTTP 요청을 HTTPS로 리디렉션

app.UseAuthentication(); // JWT 인증 미들웨어 추가
// 이 코드는 JWT 인증을 처리하여 사용자 요청을 검증함

app.UseAuthorization(); // 권한 부여 미들웨어 사용
// 인증된 사용자의 권한을 검증함

app.MapControllers(); // 컨트롤러에 대한 요청을 매핑

app.Run(); // 애플리케이션 실행
[Authorize] 없이 사용 시
[Authorize] 없이 사용 시
NZWalksDbcontext.cs 수정
NZWalksDbcontext.cs 수정
NZWalksAuthDbcontext.cs 수정
NZWalksAuthDbcontext.cs 수정

NZWalksDbcontext.cs

csharp
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NZWalks.API.Models.Domain;

namespace NZWalks.API.Data
{
    public class NZWalksAuthDbcontext : IdentityDbContext
    {
        public NZWalksAuthDbcontext(DbContextOptions<NZWalksAuthDbcontext> options) : base(options)
        {


        }

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

            var readerRoleId = "4597cc15-43f1-481c-904a-27097ce8f294";
            var writerRoleId = "4b9e9601-65e4-4b6d-9a30-bda7550c73e6";

            var roles = new List<IdentityRole>
            {
                new IdentityRole
                {
                    Id = readerRoleId,
                    ConcurrencyStamp = readerRoleId,
                    Name = "Reader",
                    NormalizedName = "Reader".ToUpper()

                },
                new IdentityRole
                {
                    Id = writerRoleId,
                    ConcurrencyStamp = writerRoleId,
                    Name = "writer",
                    NormalizedName = "writer".ToUpper()

                },

            };

            // Seed difficulties to the database
            modelBuilder.Entity<IdentityRole>().HasData(roles);

            // Add-Migration "Creating Auth Database" -Context "NZWalksAuthDbcontext"
            // Update-Database
        }
    }
}

패키지 관리자 콘솔에 명령어 입력

plaintext
Add-Migration "Creating Auth Database" -Context "NZWalksAuthDbcontext"
Update-Database -Context "NZWalksAuthDbcontext"

Identity Core 서비스를 추가 및 옵션

Program.cs

csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; // Entity Framework Core 기능을 사용하기 위한 네임스페이스
using Microsoft.IdentityModel.Tokens;
using NZWalks.API.Data;
using NZWalks.API.Mappings;
using NZWalks.API.Models.Domain;
using NZWalks.API.Repositories;
using System.Text; // 데이터 컨텍스트를 사용하기 위한 네임스페이스

var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더 객체 생성

// Add services to the container.
builder.Services.AddControllers(); // 컨트롤러 서비스를 추가하여 MVC 패턴을 지원

// Swagger/OpenAPI 설정에 대한 더 많은 정보를 얻으려면 https://aka.ms/aspnetcore/swashbuckle 참고
builder.Services.AddEndpointsApiExplorer(); // API 탐색기를 추가하여 API 문서를 자동 생성
builder.Services.AddSwaggerGen(); // Swagger 생성을 위한 서비스 추가

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksConnectionString")));

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksAuthDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksAuthConnectionString")));


builder.Services.AddScoped<IRegionRepository, SQLRegionRepository>(); // Main DB Region
builder.Services.AddScoped<IWalkRepository, SQLWalkRepository>(); // Main DB Walk
// builder.Services.AddScoped<IRegionRepository, InMemoryRegionRepository>(); // Test DB (In Memory)
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles));


// Identity Core 서비스를 추가하여 IdentityUser를 사용하도록 설정
builder.Services.AddIdentityCore<IdentityUser>()
    // IdentityRole을 추가하여 역할 관리를 지원
    .AddRoles<IdentityRole>()
    // 데이터 보호 토큰 제공자를 추가하여 "NZWalks"라는 이름의 토큰 제공자 설정
    .AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>("NZWalks")
    // Entity Framework 스토어를 사용하여 NZWalksAuthDbcontext를 통한 저장소 관리
    .AddEntityFrameworkStores<NZWalksAuthDbcontext>()
    // 기본 토큰 제공자 추가
    .AddDefaultTokenProviders();

// Identity 옵션을 구성하여 비밀번호 정책 설정
builder.Services.Configure<IdentityOptions>(options =>
{
    options.Password.RequireDigit = false; // 비밀번호에 숫자 필요 없음
    options.Password.RequireLowercase = false; // 비밀번호에 소문자 필요 없음
    options.Password.RequireNonAlphanumeric = false; // 비밀번호에 알파벳 이외의 문자 필요 없음
    options.Password.RequireUppercase = false; // 비밀번호에 대문자 필요 없음
    options.Password.RequiredLength = 6; // 비밀번호 최소 길이 6자
    options.Password.RequiredUniqueChars = 1; // 비밀번호에 최소 1개의 고유 문자 필요
});

// JWT 인증 설정 추가
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // JWT Bearer 인증 스키마 사용
    .AddJwtBearer(options => // JWT Bearer 인증 설정
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuer = true, // 발행자(issuer) 유효성 검사 활성화
        ValidateAudience = true, // 수신자(audience) 유효성 검사 활성화
        ValidateLifetime = true, // 토큰의 유효 기간 검사 활성화
        ValidateIssuerSigningKey = true, // 서명 키 유효성 검사 활성화
        ValidIssuer = builder.Configuration["Jwt:Issuer"], // 유효한 발행자 설정
        ValidAudience = builder.Configuration["Jwt:Audience"], // 유효한 수신자 설정
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) // 서명 키 설정
    });

var app = builder.Build(); // 애플리케이션 빌드

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    // 개발 환경에서만 Swagger를 사용하도록 설정
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection(); // HTTP 요청을 HTTPS로 리디렉션

app.UseAuthentication(); // JWT 인증 미들웨어 추가
// 이 코드는 JWT 인증을 처리하여 사용자 요청을 검증함

app.UseAuthorization(); // 권한 부여 미들웨어 사용
// 인증된 사용자의 권한을 검증함

app.MapControllers(); // 컨트롤러에 대한 요청을 매핑

app.Run(); // 애플리케이션 실행

인증 컨트롤러 만들기

AuthController.cs

csharp
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NZWalks.API.Models.DTO;

namespace NZWalks.API.Controllers
{
    [Route("api/[controller]")] // 컨트롤러의 기본 경로를 설정. [controller]는 컨트롤러 이름으로 대체됨.
    [ApiController] // API 컨트롤러 특성 적용
    public class AuthController : ControllerBase
    {
        private readonly UserManager<IdentityUser> userManager;

        // UserManager를 주입받아 초기화
        public AuthController(UserManager<IdentityUser> userManager)
        {
            this.userManager = userManager;
        }

        // Post: /api/Auth/Register
        [HttpPost]
        [Route("Register")]
        public async Task<IActionResult> Register([FromBody] RegisterRequestDto registerRequestDto)
        {
            // 새로운 IdentityUser 생성
            var identityUser = new IdentityUser
            {
                UserName = registerRequestDto.Username,
                Email = registerRequestDto.Username
            };

            // UserManager를 사용하여 사용자 생성
            var identityResult = await userManager.CreateAsync(identityUser, registerRequestDto.Password);

            if (identityResult.Succeeded)
            {
                // 사용자에게 역할(Role) 추가
                if (registerRequestDto.Roles != null && registerRequestDto.Roles.Any())
                {
                    identityResult = await userManager.AddToRolesAsync(identityUser, registerRequestDto.Roles);
                    if (identityResult.Succeeded)
                    {
                        return Ok("User was registered! Please login."); // 사용자 등록 성공 메시지 반환
                    }
                }
            }
            return BadRequest("Something went wrong!"); // 오류 발생 시 반환 메시지
        }
    }
}

RegisterRequestDto.cs

csharp
using System.ComponentModel.DataAnnotations;

namespace NZWalks.API.Models.DTO
{
    public class RegisterRequestDto
    {
        [Required]
        [DataType(DataType.EmailAddress)]
        public string Username { get; set; }
        [Required]
        [DataType(DataType.EmailAddress)]
        public string Password { get; set; }

        public string[] Roles { get; set; }

    }
}

Login 기능 만들기

AuthController.cs

csharp
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NZWalks.API.Models.DTO;

namespace NZWalks.API.Controllers
{
    [Route("api/[controller]")] // 컨트롤러의 기본 경로를 설정. [controller]는 컨트롤러 이름으로 대체됨.
    [ApiController] // API 컨트롤러 특성 적용
    public class AuthController : ControllerBase
    {
        private readonly UserManager<IdentityUser> userManager;

        // UserManager를 주입받아 초기화
        public AuthController(UserManager<IdentityUser> userManager)
        {
            this.userManager = userManager;
        }

        // Post: /api/Auth/Register
        [HttpPost]
        [Route("Register")]
        public async Task<IActionResult> Register([FromBody] RegisterRequestDto registerRequestDto)
        {
            // 새로운 IdentityUser 생성
            var identityUser = new IdentityUser
            {
                UserName = registerRequestDto.Username,
                Email = registerRequestDto.Username
            };

            // UserManager를 사용하여 사용자 생성
            var identityResult = await userManager.CreateAsync(identityUser, registerRequestDto.Password);

            if (identityResult.Succeeded)
            {
                // 사용자에게 역할(Role) 추가
                if (registerRequestDto.Roles != null && registerRequestDto.Roles.Any())
                {
                    identityResult = await userManager.AddToRolesAsync(identityUser, registerRequestDto.Roles);
                    if (identityResult.Succeeded)
                    {
                        return Ok("User was registered! Please login."); // 사용자 등록 성공 메시지 반환
                    }
                }
            }
            return BadRequest("Something went wrong!"); // 오류 발생 시 반환 메시지
        }


        // Post: /api/Auth/Login
        [HttpPost]
        [Route("Login")]
        public async Task<IActionResult> Login([FromBody] LoginRequestDto loginRequestDto)
        {
            // 사용자 이메일로 사용자 찾기
            var user = await userManager.FindByEmailAsync(loginRequestDto.Username);

            if (user != null)
            {
                // 사용자의 비밀번호 확인
                var checkPasswordResult = await userManager.CheckPasswordAsync(user, loginRequestDto.Password);

                if (checkPasswordResult)
                {
                    // 토큰 생성 로직
                    return Ok(); // 성공 응답
                }
            }

            return BadRequest("Username or Password incorrect"); // 사용자 이름 또는 비밀번호가 잘못된 경우 오류 응답
        }


    }
}

LoginRequestDto.cs

csharp
using System.ComponentModel.DataAnnotations;

namespace NZWalks.API.Models.DTO
{
    public class LoginRequestDto
    {
        [Required]
        [DataType(DataType.EmailAddress)]
        public string Username { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
로그인 실패 (400)
로그인 실패 (400)
로그인 성공 (200)
로그인 성공 (200)

JWT 토큰 생성 및 적용

JWT 토큰 생성

TokenRepository.cs

csharp
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace NZWalks.API.Repositories
{
    public class TokenRepository : ITokenRepository
    {
        private readonly IConfiguration configuration;

        // IConfiguration 주입받아 초기화
        public TokenRepository(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        // JWT 토큰 생성 메서드
        public string CreateJWTToken(IdentityUser user, List<string> roles)
        {
            // 클레임 생성
            // Claim은 사용자의 특정 속성이나 권한을 나타내는 정보 조각
            // JWT 토큰에서는 이러한 클레임을 사용하여 사용자의 신원과 권한을 나타냄
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim(ClaimTypes.Name, user.UserName)
            };

            // 역할(Role)을 클레임에 추가
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }

            // 대칭 보안 키 생성 (Jwt:Key 사용)
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]));

            // 서명 자격 증명 생성 (HmacSha256 알고리즘 사용)
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            // JWT 토큰 생성
            var token = new JwtSecurityToken(
                configuration["Jwt:Issuer"], // 유효한 발행자
                configuration["Jwt:Audience"], // 유효한 수신자
                claims, // 클레임
                expires: DateTime.Now.AddMinutes(15), // 토큰 만료 시간 (현재 시간부터 15분 후)
                signingCredentials: credentials // 서명 자격 증명
            );

            // 토큰 문자열로 반환
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}

ITokenRepository.cs

csharp
using Microsoft.AspNetCore.Identity;

namespace NZWalks.API.Repositories
{
    public interface ITokenRepository
    {
        string CreateJWTToken(IdentityUser user, List<string> roles)
    }
}

JWT 토큰 적용

program.cs 수정

csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; // Entity Framework Core 기능을 사용하기 위한 네임스페이스
using Microsoft.IdentityModel.Tokens;
using NZWalks.API.Data;
using NZWalks.API.Mappings;
using NZWalks.API.Models.Domain;
using NZWalks.API.Repositories;
using System.Text; // 데이터 컨텍스트를 사용하기 위한 네임스페이스

var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더 객체 생성

// Add services to the container.
builder.Services.AddControllers(); // 컨트롤러 서비스를 추가하여 MVC 패턴을 지원

// Swagger/OpenAPI 설정에 대한 더 많은 정보를 얻으려면 https://aka.ms/aspnetcore/swashbuckle 참고
builder.Services.AddEndpointsApiExplorer(); // API 탐색기를 추가하여 API 문서를 자동 생성
builder.Services.AddSwaggerGen(); // Swagger 생성을 위한 서비스 추가

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksConnectionString")));

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksAuthDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksAuthConnectionString")));


builder.Services.AddScoped<IRegionRepository, SQLRegionRepository>(); // Main DB Region
builder.Services.AddScoped<IWalkRepository, SQLWalkRepository>(); // Main DB Walk
builder.Services.AddScoped<ITokenRepository, TokenRepository>(); // Token - Controller에서 사용가능
// builder.Services.AddScoped<IRegionRepository, InMemoryRegionRepository>(); // Test DB (In Memory)
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles));


// Identity Core 서비스를 추가하여 IdentityUser를 사용하도록 설정
builder.Services.AddIdentityCore<IdentityUser>()
    // IdentityRole을 추가하여 역할 관리를 지원
    .AddRoles<IdentityRole>()
    // 데이터 보호 토큰 제공자를 추가하여 "NZWalks"라는 이름의 토큰 제공자 설정
    .AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>("NZWalks")
    // Entity Framework 스토어를 사용하여 NZWalksAuthDbcontext를 통한 저장소 관리
    .AddEntityFrameworkStores<NZWalksAuthDbcontext>()
    // 기본 토큰 제공자 추가
    .AddDefaultTokenProviders();

// Identity 옵션을 구성하여 비밀번호 정책 설정
builder.Services.Configure<IdentityOptions>(options =>
{
    options.Password.RequireDigit = false; // 비밀번호에 숫자 필요 없음
    options.Password.RequireLowercase = false; // 비밀번호에 소문자 필요 없음
    options.Password.RequireNonAlphanumeric = false; // 비밀번호에 알파벳 이외의 문자 필요 없음
    options.Password.RequireUppercase = false; // 비밀번호에 대문자 필요 없음
    options.Password.RequiredLength = 6; // 비밀번호 최소 길이 6자
    options.Password.RequiredUniqueChars = 1; // 비밀번호에 최소 1개의 고유 문자 필요
});

// JWT 인증 설정 추가
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // JWT Bearer 인증 스키마 사용
    .AddJwtBearer(options => // JWT Bearer 인증 설정
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuer = true, // 발행자(issuer) 유효성 검사 활성화
        ValidateAudience = true, // 수신자(audience) 유효성 검사 활성화
        ValidateLifetime = true, // 토큰의 유효 기간 검사 활성화
        ValidateIssuerSigningKey = true, // 서명 키 유효성 검사 활성화
        ValidIssuer = builder.Configuration["Jwt:Issuer"], // 유효한 발행자 설정
        ValidAudience = builder.Configuration["Jwt:Audience"], // 유효한 수신자 설정
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) // 서명 키 설정
    });

var app = builder.Build(); // 애플리케이션 빌드

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    // 개발 환경에서만 Swagger를 사용하도록 설정
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection(); // HTTP 요청을 HTTPS로 리디렉션

app.UseAuthentication(); // JWT 인증 미들웨어 추가
// 이 코드는 JWT 인증을 처리하여 사용자 요청을 검증함

app.UseAuthorization(); // 권한 부여 미들웨어 사용
// 인증된 사용자의 권한을 검증함

app.MapControllers(); // 컨트롤러에 대한 요청을 매핑

app.Run(); // 애플리케이션 실행
program.cs 수정
program.cs 수정
AuthController.cs 생성자 수정
AuthController.cs 생성자 수정

AuthController.cs

csharp
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NZWalks.API.Models.DTO;
using NZWalks.API.Repositories;

namespace NZWalks.API.Controllers
{
    [Route("api/[controller]")] // 컨트롤러의 기본 경로를 설정. [controller]는 컨트롤러 이름으로 대체됨.
    [ApiController] // API 컨트롤러 특성 적용
    public class AuthController : ControllerBase
    {
        private readonly UserManager<IdentityUser> userManager;
        private readonly ITokenRepository tokenRepository;

        // UserManager와 ITokenRepository를 주입받아 초기화
        public AuthController(UserManager<IdentityUser> userManager, ITokenRepository tokenRepository)
        {
            this.userManager = userManager;
            this.tokenRepository = tokenRepository;
        }

        // Post: /api/Auth/Register
        [HttpPost]
        [Route("Register")]
        public async Task<IActionResult> Register([FromBody] RegisterRequestDto registerRequestDto)
        {
            // 새로운 IdentityUser 생성
            var identityUser = new IdentityUser
            {
                UserName = registerRequestDto.Username,
                Email = registerRequestDto.Username
            };

            // UserManager를 사용하여 사용자 생성
            var identityResult = await userManager.CreateAsync(identityUser, registerRequestDto.Password);

            if (identityResult.Succeeded)
            {
                // 사용자에게 역할(Role) 추가
                if (registerRequestDto.Roles != null && registerRequestDto.Roles.Any())
                {
                    identityResult = await userManager.AddToRolesAsync(identityUser, registerRequestDto.Roles);
                    if (identityResult.Succeeded)
                    {
                        return Ok("User was registered! Please login."); // 사용자 등록 성공 메시지 반환
                    }
                }
            }
            return BadRequest("Something went wrong!"); // 오류 발생 시 반환 메시지
        }

        // Post: /api/Auth/Login
        [HttpPost]
        [Route("Login")]
        public async Task<IActionResult> Login([FromBody] LoginRequestDto loginRequestDto)
        {
            // 사용자 이메일로 사용자 찾기
            var user = await userManager.FindByEmailAsync(loginRequestDto.Username);

            if (user != null)
            {
                // 사용자의 비밀번호 확인
                var checkPasswordResult = await userManager.CheckPasswordAsync(user, loginRequestDto.Password);

                if (checkPasswordResult)
                {
                    // 사용자 역할(Role) 가져오기
                    var roles = await userManager.GetRolesAsync(user);

                    if (roles != null)
                    {
                        // 토큰 생성 로직
                        var jwtToken = tokenRepository.CreateJWTToken(user, roles.ToList());
                        var response = new LoginResposeDto
                        {
                            JwtToken = jwtToken
                        };
                        return Ok(response); // 생성된 토큰을 포함한 응답 반환
                    }
                }
            }
            return BadRequest("Username or Password incorrect"); // 사용자 이름 또는 비밀번호가 잘못된 경우 오류 응답
        }
    }
}

LoginResposeDto.cs

csharp
namespace NZWalks.API.Models.DTO
{
    public class LoginResposeDto
    {
        public string JwtToken { get; set; }
    }
}
현재 사용 중인 대칭 키가 HS256 암호화 알고리즘에 필요한 최소 128비트 키보다 작기 때문에 발생
현재 사용 중인 대칭 키가 HS256 암호화 알고리즘에 필요한 최소 128비트 키보다 작기 때문에 발생
아직 사용하지 못하는 API
아직 사용하지 못하는 API

RegionsController.cs - Role에 따른 접근으로 변경

csharp
using Microsoft.AspNetCore.Mvc;
using NZWalks.API.Data;
using NZWalks.API.Models.Domain;
using NZWalks.API.Models.DTO;
using AutoMapper;
using NZWalks.API.CustomActionFilter;
using Microsoft.AspNetCore.Authorization;

namespace NZWalks.API.Controllers
{

    [Route("api/[controller]")] // 컨트롤러의 기본 Route를 설정. [controller]는 컨트롤러 이름으로 대체됨.
    [ApiController] // API 컨트롤러 특성 적용
    public class RegionsController : ControllerBase
    {

        private readonly NZWalksDbcontext dbcontext;
        private readonly IRegionRepository regionRepository;
        private readonly IMapper mapper;

        public RegionsController(NZWalksDbcontext dbContext, IRegionRepository regionRepository, IMapper mapper)
        {
            this.dbcontext = dbContext;
            this.regionRepository = regionRepository;
            this.mapper = mapper;
        }

        // GET ALL REGIONS
        // GET: https://localhost:1234/api/regions
        [HttpGet]
        [Authorize(Roles = "Reader, writer")] // Reader, writer에서 인증된 사용자만 접근 가능
        public async Task<IActionResult> GetAll()
        {
            // Get Data From Database - Domain models
            // var regionDomain = await dbcontext.Regions.ToListAsync(); // Change Async
            var regionDomain = await regionRepository.GetAllAsync();

            // Map Domain Models to DTOs
            // var regionDto = new List<RegionDto>();
            // foreach (var region in regionDomain) 
            // {
            //     regionDto.Add(new RegionDto() 
            //     {
            //         Id = region.Id,
            //         Code = region.Code,
            //         Name = region.Name,
            //         RegionImageUrl = region.RegionImageUrl
            //     });
            // }

            // Map Domain Models to DTOs
            var regionDto = mapper.Map<List<RegionDto>>(regionDomain);

            return Ok(regionDto);
        }

        // GET SINGLE REGION (Get Region By ID)
        // GET: https://localhost:1234/api/regions/{id}
        [HttpGet]
        [Route("{id:guid}")]
        [Authorize(Roles = "Reader")] // Reader에서 인증된 사용자만 접근 가능
        public async Task<IActionResult> GetById([FromRoute]Guid id) 
        {
            // Get Data From Database - Domain models
            // var region = dbcontext.Regions.Find(id);
            // var regionDomain = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);
            var regionDomain = await regionRepository.GetByIdAsync(id);
            if (regionDomain == null)
            {
                return NotFound();
            }

            // Map Domain Models to DTOs
            // var regionDto = new RegionDto() 
            // {
            //     Id = regionDomain.Id,
            //     Code = regionDomain.Code,
            //     Name = regionDomain.Name,
            //     RegionImageUrl = regionDomain.RegionImageUrl
            // };

            var regionDto = mapper.Map<RegionDto>(regionDomain);

            return Ok(regionDto);

        }

        // POST To Create New Region
        // POST : https://localhost:1234/api/regions
        [HttpPost]
        [ValidateModel]
        [Authorize(Roles = "writer")] // writer에서 인증된 사용자만 접근 가능
        public async Task<IActionResult> Create([FromBody] AddRegionRequestDto addRegionRequestDto)
        {
            /* [ValidateModel] 적용전
                        if (ModelState.IsValid) // [ValidateModel] 
            {
                // [Require]
                // string Code, Name
                // string? RegionImageUrl

                // Map or Convert DTO to Domain Model
                //var regionDomainModel = new Region
                //{
                //    Code = addRegionRequestDto.Code,
                //    Name = addRegionRequestDto.Name,
                //    RegionImageUrl = addRegionRequestDto.RegionImageUrl
                //};

                // Map or Convert DTO to Domain Model
                var regionDomainModel = mapper.Map<Region>(addRegionRequestDto);

                // Use Domain Model to create Region
                // await dbcontext.Regions.AddAsync(regionDomainModel);
                // await dbcontext.SaveChangesAsync();

                await regionRepository.CreateAsync(regionDomainModel);


                // Map Domain model back to DTO
                //var regionDto = new RegionDto 
                //{ 
                //    Id = regionDomainModel.Id,
                //    Code = addRegionRequestDto.Code,
                //    Name = regionDomainModel.Name, 
                //    RegionImageUrl = regionDomainModel.RegionImageUrl

                //};

                var regionDto = mapper.Map<RegionDto>(regionDomainModel);

                // 201 Created 응답으로 생성된 DTO 반환
                return CreatedAtAction(nameof(GetById), new { id = regionDto.Id }, regionDto);
                // CreatedAtAction은 ASP.NET Core에서 HTTP POST 요청 후 201 Created 상태 코드를 반환할 때 사용되는 메서드
                // 새로운 리소스를 생성한 후, 해당 리소스에 접근할 수 있는 URI를 함께 제공하여 클라이언트가 리소스를 쉽게 찾을 수 있음
            }
            else
            {
                return BadRequest(ModelState);
            }
             
            */

            // [Require]
            // string Code, Name
            // string? RegionImageUrl

            // Map or Convert DTO to Domain Model
            //var regionDomainModel = new Region
            //{
            //    Code = addRegionRequestDto.Code,
            //    Name = addRegionRequestDto.Name,
            //    RegionImageUrl = addRegionRequestDto.RegionImageUrl
            //};

            // Map or Convert DTO to Domain Model
            var regionDomainModel = mapper.Map<Region>(addRegionRequestDto);

            // Use Domain Model to create Region
            // await dbcontext.Regions.AddAsync(regionDomainModel);
            // await dbcontext.SaveChangesAsync();

            await regionRepository.CreateAsync(regionDomainModel);


            // Map Domain model back to DTO
            //var regionDto = new RegionDto 
            //{ 
            //    Id = regionDomainModel.Id,
            //    Code = addRegionRequestDto.Code,
            //    Name = regionDomainModel.Name, 
            //    RegionImageUrl = regionDomainModel.RegionImageUrl

            //};

            var regionDto = mapper.Map<RegionDto>(regionDomainModel);

            // 201 Created 응답으로 생성된 DTO 반환
            return CreatedAtAction(nameof(GetById), new { id = regionDto.Id }, regionDto);
            // CreatedAtAction은 ASP.NET Core에서 HTTP POST 요청 후 201 Created 상태 코드를 반환할 때 사용되는 메서드
            // 새로운 리소스를 생성한 후, 해당 리소스에 접근할 수 있는 URI를 함께 제공하여 클라이언트가 리소스를 쉽게 찾을 수 있음
        }



        // Update Region
        // PUT: https://localhost:1234/api/regions/{id}
        [HttpPut]
        [Route("{id:guid}")]
        [ValidateModel]
        [Authorize(Roles = "writer")] // writer에서 인증된 사용자만 접근 가능
        public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] UpdateRegionRequestDto updateRegionRequestDto)
        {
            /* [ValidateModel] 적용 전
            if (ModelState.IsValid)
            {
                // Map 
                //var regionDomainModel = new Region
                //{
                //    Code = updateRegionRequestDto.Code,
                //    Name = updateRegionRequestDto.Name,
                //    RegionImageUrl = updateRegionRequestDto.RegionImageUrl

                //};

                var regionDomainModel = mapper.Map<Region>(updateRegionRequestDto);

                // Check if region exists
                // var regionDomainModel = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);
                regionDomainModel = await regionRepository.UpdateAsync(id, regionDomainModel);

                if (regionDomainModel == null)
                {
                    return NotFound();
                }

                //// Map DTO to Domain model
                //regionDomainModel.Code = updateRegionRequestDto.Code;
                //regionDomainModel.Name = updateRegionRequestDto.Name;
                //regionDomainModel.RegionImageUrl = updateRegionRequestDto.RegionImageUrl;

                //await dbcontext.SaveChangesAsync();

                // Convert Domain Model to DTO
                //var regionDto = new RegionDto
                //{
                //    Id = regionDomainModel.Id,
                //    Code = regionDomainModel.Code,
                //    Name = regionDomainModel.Name,
                //    RegionImageUrl = regionDomainModel.RegionImageUrl
                //};

                var regionDto = mapper.Map<RegionDto>(regionDomainModel);

                return Ok(regionDto);

            }
            else 
            { 
                return BadRequest(ModelState);
            }
            */
            // Map 
            //var regionDomainModel = new Region
            //{
            //    Code = updateRegionRequestDto.Code,
            //    Name = updateRegionRequestDto.Name,
            //    RegionImageUrl = updateRegionRequestDto.RegionImageUrl

            //};

            var regionDomainModel = mapper.Map<Region>(updateRegionRequestDto);

            // Check if region exists
            // var regionDomainModel = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);
            regionDomainModel = await regionRepository.UpdateAsync(id, regionDomainModel);

            if (regionDomainModel == null)
            {
                return NotFound();
            }

            //// Map DTO to Domain model
            //regionDomainModel.Code = updateRegionRequestDto.Code;
            //regionDomainModel.Name = updateRegionRequestDto.Name;
            //regionDomainModel.RegionImageUrl = updateRegionRequestDto.RegionImageUrl;

            //await dbcontext.SaveChangesAsync();

            // Convert Domain Model to DTO
            //var regionDto = new RegionDto
            //{
            //    Id = regionDomainModel.Id,
            //    Code = regionDomainModel.Code,
            //    Name = regionDomainModel.Name,
            //    RegionImageUrl = regionDomainModel.RegionImageUrl
            //};

            var regionDto = mapper.Map<RegionDto>(regionDomainModel);

            return Ok(regionDto);
        }


        // Delete Region
        // Delete https://localhost:1234/api/regions/{id}
        [HttpDelete]
        [Route("{id:guid}")]
        [Authorize(Roles = "writer")] // writer에서 인증된 사용자만 접근 가능
        public async Task<IActionResult> Update([FromRoute] Guid id)
        {
            //var regionDomainModel = await dbcontext.Regions.FirstOrDefaultAsync(x => x.Id == id);

            var regionDomainModel = await regionRepository.DeleteAsync(id);


            if (regionDomainModel == null)
            {
                return NotFound();
            }

            // dbcontext.Regions.Remove(regionDomainModel);
            // await dbcontext.SaveChangesAsync();

            // return deleted Region back
            // Convert Domain Model to DTO
            //var regionDto = new RegionDto
            //{
            //    Id = regionDomainModel.Id,
            //    Code = regionDomainModel.Code,
            //    Name = regionDomainModel.Name,
            //    RegionImageUrl = regionDomainModel.RegionImageUrl
            //};

            var regionDto = mapper.Map<Region>(regionDomainModel);

            return Ok(regionDto);
        }
    }
}
writer 등록
writer 등록
Reader가 Write 역할의 Api 사용 불가 (403)
Reader가 Write 역할의 Api 사용 불가 (403)
Write 역할을 이용하여 제거
Write 역할을 이용하여 제거

Swagger 적용

Program.cs

csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; // Entity Framework Core 기능을 사용하기 위한 네임스페이스
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using NZWalks.API.Data;
using NZWalks.API.Mappings;
using NZWalks.API.Models.Domain;
using NZWalks.API.Repositories;
using System.Text; // 데이터 컨텍스트를 사용하기 위한 네임스페이스

var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더 객체 생성

// Add services to the container.
builder.Services.AddControllers(); // 컨트롤러 서비스를 추가하여 MVC 패턴을 지원

// Swagger/OpenAPI 설정에 대한 더 많은 정보를 얻으려면 https://aka.ms/aspnetcore/swashbuckle 참고
builder.Services.AddEndpointsApiExplorer(); // API 탐색기를 추가하여 API 문서를 자동 생성

// Swagger 생성을 위한 서비스 추가
builder.Services.AddSwaggerGen(options => {
    // 제목과 버전을 정의하여 Swagger 문서 설정
    options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
    {
        Title = "NZ Walks API",
        Version = "v1"
    });

    // JWT Bearer 보안 정의 추가
    options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme,
        new Microsoft.OpenApi.Models.OpenApiSecurityScheme()
        {
            Name = "Authorization",  // 인증 헤더 이름        
            In = ParameterLocation.Header,  // 인증 정보의 위치 (헤더)        
            Type = SecuritySchemeType.ApiKey,  // 인증 유형 (API 키)        
            Scheme = JwtBearerDefaults.AuthenticationScheme  // 스키마 설정
        }
    );

    // 정의된 보안 스키마를 사용한 보안 요구 사항 추가
    options.AddSecurityRequirement(new OpenApiSecurityRequirement {
        {
            new OpenApiSecurityScheme {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,  // 참조 유형 설정
                    Id = JwtBearerDefaults.AuthenticationScheme  // 참조 ID 설정
                },
                Scheme = "Oauth2",  // 스키마 설정
                Name = JwtBearerDefaults.AuthenticationScheme,  // 스키마 이름
                In = ParameterLocation.Header  // 위치 설정
            },
            new List<string>()
        }
    });
});


// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksConnectionString")));

// DbContext 서비스를 추가하여 SQL Server를 사용하도록 설정
builder.Services.AddDbContext<NZWalksAuthDbcontext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("NZWalksAuthConnectionString")));


builder.Services.AddScoped<IRegionRepository, SQLRegionRepository>(); // Main DB Region
builder.Services.AddScoped<IWalkRepository, SQLWalkRepository>(); // Main DB Walk
builder.Services.AddScoped<ITokenRepository, TokenRepository>(); // Token - Controller에서 사용가능
// builder.Services.AddScoped<IRegionRepository, InMemoryRegionRepository>(); // Test DB (In Memory)
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles));


// Identity Core 서비스를 추가하여 IdentityUser를 사용하도록 설정
builder.Services.AddIdentityCore<IdentityUser>()
    // IdentityRole을 추가하여 역할 관리를 지원
    .AddRoles<IdentityRole>()
    // 데이터 보호 토큰 제공자를 추가하여 "NZWalks"라는 이름의 토큰 제공자 설정
    .AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>("NZWalks")
    // Entity Framework 스토어를 사용하여 NZWalksAuthDbcontext를 통한 저장소 관리
    .AddEntityFrameworkStores<NZWalksAuthDbcontext>()
    // 기본 토큰 제공자 추가
    .AddDefaultTokenProviders();

// Identity 옵션을 구성하여 비밀번호 정책 설정
builder.Services.Configure<IdentityOptions>(options =>
{
    options.Password.RequireDigit = false; // 비밀번호에 숫자 필요 없음
    options.Password.RequireLowercase = false; // 비밀번호에 소문자 필요 없음
    options.Password.RequireNonAlphanumeric = false; // 비밀번호에 알파벳 이외의 문자 필요 없음
    options.Password.RequireUppercase = false; // 비밀번호에 대문자 필요 없음
    options.Password.RequiredLength = 6; // 비밀번호 최소 길이 6자
    options.Password.RequiredUniqueChars = 1; // 비밀번호에 최소 1개의 고유 문자 필요
});

// JWT 인증 설정 추가
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // JWT Bearer 인증 스키마 사용
    .AddJwtBearer(options => // JWT Bearer 인증 설정
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuer = true, // 발행자(issuer) 유효성 검사 활성화
        ValidateAudience = true, // 수신자(audience) 유효성 검사 활성화
        ValidateLifetime = true, // 토큰의 유효 기간 검사 활성화
        ValidateIssuerSigningKey = true, // 서명 키 유효성 검사 활성화
        ValidIssuer = builder.Configuration["Jwt:Issuer"], // 유효한 발행자 설정
        ValidAudience = builder.Configuration["Jwt:Audience"], // 유효한 수신자 설정
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) // 서명 키 설정
    });

var app = builder.Build(); // 애플리케이션 빌드

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    // 개발 환경에서만 Swagger를 사용하도록 설정
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection(); // HTTP 요청을 HTTPS로 리디렉션

app.UseAuthentication(); // JWT 인증 미들웨어 추가
// 이 코드는 JWT 인증을 처리하여 사용자 요청을 검증함

app.UseAuthorization(); // 권한 부여 미들웨어 사용
// 인증된 사용자의 권한을 검증함

app.MapControllers(); // 컨트롤러에 대한 요청을 매핑

app.Run(); // 애플리케이션 실행