ASP.NET Core Web API에서 Basic 인증으로 WeatherForecast API 보호하기

  • 12 minutes to read

이 문서에서는 ASP.NET Core Web API 프로젝트를 생성하고, 고정된 이메일과 암호를 사용하는 Basic 인증을 구현하여 WeatherForecast API를 보호하는 과정을 단계별로 설명합니다. 또한, C# Interactive 및 JavaScript를 사용하여 인증 토큰(Base64 인코딩) 생성 방법도 다룹니다.

1. 프로젝트 생성

1-1 Web API 프로젝트 생성

.NET CLI를 사용해 Web API 프로젝트를 생성합니다.

dotnet new webapi -o VisualAcademy.ApiService
cd VisualAcademy.ApiService

2. Basic 인증 구현

2-1 인증 핸들러 클래스 작성

ASP.NET Core의 인증 미들웨어를 확장하기 위해 AuthenticationHandler를 상속받아 Basic 인증 핸들러를 작성합니다.

BasicAuthenticationHandler.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace VisualAcademy.ApiService.Security
{
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private const string FixedEmail = "admin@visualacademy.com";
        private const string FixedPassword = "securepassword";

        private readonly TimeProvider _timeProvider;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            TimeProvider timeProvider)
            : base(options, logger, encoder, null) // ISystemClock 제거
        {
            _timeProvider = timeProvider;
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
            }

            try
            {
                var authHeader = Request.Headers["Authorization"].ToString();
                if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
                {
                    return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header."));
                }

                var encodedCredentials = authHeader.Substring("Basic ".Length).Trim();
                var decodedBytes = Convert.FromBase64String(encodedCredentials);
                var decodedCredentials = Encoding.UTF8.GetString(decodedBytes);
                var credentials = decodedCredentials.Split(':');

                if (credentials.Length != 2)
                {
                    return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header format."));
                }

                var email = credentials[0];
                var password = credentials[1];

                if (email != FixedEmail || password != FixedPassword)
                {
                    return Task.FromResult(AuthenticateResult.Fail("Invalid email or password."));
                }

                var claims = new[] { new Claim(ClaimTypes.Name, email) };
                var identity = new ClaimsIdentity(claims, Scheme.Name);
                var principal = new ClaimsPrincipal(identity);
                var ticket = new AuthenticationTicket(principal, Scheme.Name);

                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            catch
            {
                return Task.FromResult(AuthenticateResult.Fail("Error occurred during authentication."));
            }
        }
    }
}

TimeProvider 제거한 소스:

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace Kodee.ApiService.Security
{
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private const string FixedEmail = "admin@visualacademy.com";
        private const string FixedPassword = "securepassword";

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder) : base(options, logger, encoder)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
            }

            try
            {
                var authHeader = Request.Headers["Authorization"].ToString();
                if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
                {
                    return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header."));
                }

                var encodedCredentials = authHeader.Substring("Basic ".Length).Trim();
                var decodedBytes = Convert.FromBase64String(encodedCredentials);
                var decodedCredentials = Encoding.UTF8.GetString(decodedBytes);
                var credentials = decodedCredentials.Split(':');

                if (credentials.Length != 2)
                {
                    return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header format."));
                }

                var email = credentials[0];
                var password = credentials[1];

                if (email != FixedEmail || password != FixedPassword)
                {
                    return Task.FromResult(AuthenticateResult.Fail("Invalid email or password."));
                }

                var claims = new[] { new Claim(ClaimTypes.Name, email) };
                var identity = new ClaimsIdentity(claims, Scheme.Name);
                var principal = new ClaimsPrincipal(identity);
                var ticket = new AuthenticationTicket(principal, Scheme.Name);

                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            catch
            {
                return Task.FromResult(AuthenticateResult.Fail("Error occurred during authentication."));
            }
        }
    }
}

TimeProvider 주입

TimeProviderProgram.cs에서 DI(Dependency Injection)로 등록합니다.

Program.cs 변경
var builder = WebApplication.CreateBuilder(args);

// TimeProvider를 서비스로 추가
builder.Services.AddSingleton(TimeProvider.System);

builder.Services.AddAuthentication("BasicAuthentication")
    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

2-2 인증 스키마 등록

Program.cs에 커스텀 Basic 인증 핸들러를 등록합니다.

Program.cs

using VisualAcademy.ApiService.Security;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication("BasicAuthentication")
    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

3. WeatherForecast API 보호

3-1 [Authorize] 특성 추가

WeatherForecastController에서 [Authorize] 속성을 추가해 인증이 필요하도록 설정합니다.

WeatherForecastController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace VisualAcademy.ApiService.Controllers
{
    [Authorize] // Basic 인증 요구
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

4. HTTP 요청 테스트

4-1 인증 정보 생성

Base64 인코딩: C# Interactive

C# Interactive 환경에서 Base64 인코딩을 사용해 인증 정보를 생성합니다.

string email = "admin@visualacademy.com";
string password = "securepassword";
string credentials = $"{email}:{password}";
string base64Credentials = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(credentials));
Console.WriteLine($"Authorization: Basic {base64Credentials}");

출력:

Authorization: Basic YWRtaW5AdmlzdWFsYWNhZGVteS5jb206c2VjdXJlcGFzc3dvcmQ=

Base64 인코딩: JavaScript

브라우저 콘솔이나 Node.js 환경에서도 동일한 결과를 생성할 수 있습니다.

const email = "admin@visualacademy.com";
const password = "securepassword";
const credentials = `${email}:${password}`;
const base64Credentials = btoa(credentials);
console.log(`Authorization: Basic ${base64Credentials}`);

출력:

Authorization: Basic YWRtaW5AdmlzdWFsYWNhZGVteS5jb206c2VjdXJlcGFzc3dvcmQ=

인증 요청 HTTP 파일

WeatherForecastTest.http

@HostAddress = https://localhost:5001

### GET 요청 (성공)
GET {{HostAddress}}/weatherforecast
Authorization: Basic YWRtaW5AdmlzdWFsYWNhZGVteS5jb206c2VjdXJlcGFzc3dvcmQ=

### GET 요청 (실패)
GET {{HostAddress}}/weatherforecast
Authorization: Basic YWRtaW5AdmlzdWFsYWNhZGVteS5jb206YmFkcGFzc3dvcmQ=

5. 결과

5-1 성공 응답

[
  {
    "date": "2024-11-30T00:00:00",
    "temperatureC": 20,
    "summary": "Warm"
  },
  {
    "date": "2024-12-01T00:00:00",
    "temperatureC": 25,
    "summary": "Hot"
  }
]

5-2 실패 응답

{
    "title": "Unauthorized",
    "status": 401,
    "detail": "Invalid email or password."
}

6. 주의사항

  1. 보안: Basic 인증은 보안이 강하지 않으므로 테스트나 간단한 용도로만 사용하세요. 반드시 HTTPS를 활성화해 전송 데이터 암호화를 보장하세요.
  2. 확장성: 고정된 이메일과 암호 대신 데이터베이스 기반 인증 또는 OAuth2 같은 더 안전한 방식을 고려하세요.

7. Swagger UI에서 Basic 인증 활성화

7-1 Swagger 문서 생성기 설정

Swagger에서 Basic Authentication을 설정하려면, AddSwaggerGen 메서드에서 보안 정의와 보안 요구사항을 추가합니다.

Program.cs - Swagger 설정

builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "VisualAcademy API", Version = "v1" });

    // Basic Authentication을 위한 보안 정의 추가
    c.AddSecurityDefinition("basic", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "basic",
        In = ParameterLocation.Header,
        Description = "Basic Authentication을 사용하여 인증합니다. 'Username:Password'를 Base64로 인코딩하여 전송하세요."
    });

    // Basic Authentication 보안 요구사항 추가
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "basic"
                }
            },
            new string[] {}
        }
    });
});

7-2 HTTP 요청 파이프라인 구성

Swagger UI를 활성화하고, 기본 경로로 설정하여 앱 실행 시 Swagger UI가 기본적으로 표시되도록 설정합니다.

Program.cs - Swagger UI 활성화

// Configure the HTTP request pipeline
app.MapOpenApi(); // OpenAPI 매핑
app.UseSwagger();
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/swagger/v1/swagger.json", "VisualAcademy API V1");
    options.DefaultModelExpandDepth(-1); // 모델 목록 접힘
    options.RoutePrefix = string.Empty;  // 기본 경로를 Swagger UI로 설정
});

7-3 인증된 API 호출 테스트

Swagger UI에 추가된 Authorize 버튼을 사용하여 Basic Authentication 인증을 수행합니다.

  1. Swagger UI 실행:

    • 앱을 실행하고 브라우저에서 기본 경로(예: https://localhost:5001/)로 접속합니다.
    • Swagger UI가 기본적으로 표시됩니다.
  2. Authorize 버튼 클릭:

    • Swagger UI 상단의 Authorize 버튼을 클릭합니다.
  3. 인증 정보 입력:

    • 사용자 이름: admin@visualacademy.com
    • 비밀번호: securepassword
    • 정보를 입력한 후 Authorize 버튼을 클릭합니다.
  4. 인증 성공 후 API 호출:

    • 인증된 상태에서 API 요청을 수행합니다.
    • Authorization 헤더에 Base64 인코딩된 Basic Authentication 정보가 자동으로 포함됩니다.

8. XML 기반 API 문서화와 [SwaggerResponse] 특성 활용

Basic 인증과 Swagger UI 설정이 완료된 후, XML 기반 문서화와 [SwaggerResponse] 특성을 사용하여 API 설명을 더욱 명확히 하고, 엔드포인트별 응답 정보를 강화할 수 있습니다.

8-1 XML 문서 생성 설정

.csproj 파일에 <GenerateDocumentationFile> 속성을 추가하여 XML 문서 파일을 생성하도록 설정합니다.

VisualAcademy.ApiService.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <GenerateDocumentationFile>True</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.1.0" />
  </ItemGroup>

</Project>
  • <GenerateDocumentationFile>: True로 설정하면, 빌드 시 XML 문서 파일이 생성됩니다.

8-2 코드 주석 작성

XML 주석을 사용하여 API 설명을 추가합니다. Swagger UI와 IntelliSense에서 이 정보를 사용할 수 있습니다.

WeatherForecastController.cs

/// <summary>
/// WeatherForecastController는 날씨 데이터를 제공하는 API입니다.
/// </summary>
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    /// <summary>
    /// 날씨 데이터를 가져옵니다.
    /// </summary>
    /// <returns>날씨 데이터 목록</returns>
    [HttpGet]
    [SwaggerResponse(StatusCodes.Status200OK, "성공적으로 날씨 데이터를 반환합니다.", typeof(IEnumerable<WeatherForecast>))]
    [SwaggerResponse(StatusCodes.Status401Unauthorized, "인증에 실패한 경우")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

8-3 [SwaggerResponse] 특성

[SwaggerResponse]는 Swagger 문서에서 특정 엔드포인트의 응답 상태 코드와 설명을 명시적으로 표시하는 데 사용됩니다.

예제 설명

  • StatusCodes.Status200OK: 요청이 성공했을 때 반환되는 데이터와 설명을 정의.
  • StatusCodes.Status401Unauthorized: 인증이 실패했을 경우 응답 상태 코드를 설명.

Swagger UI에서 각 응답 코드에 대한 설명이 표시되며, API의 의도를 명확히 전달할 수 있습니다.

8-4 Swagger와 XML 문서 통합

Swagger UI에서 XML 주석을 사용하려면 XML 문서 파일 경로를 Swagger 설정에 추가해야 합니다.

Program.cs

builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "VisualAcademy API", Version = "v1" });

    c.AddSecurityDefinition("basic", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "basic",
        In = ParameterLocation.Header,
        Description = "Basic Authentication을 사용하여 인증합니다. 'Username:Password'를 Base64로 인코딩하여 전송하세요."
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "basic"
                }
            },
            new string[] {}
        }
    });

    // XML 문서 파일 경로 추가
    var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

8-5 빌드 후 XML 문서 확인

빌드가 완료되면 프로젝트의 출력 디렉터리(bin/Debug/net9.0/)에 XML 문서 파일이 생성됩니다. 파일 이름은 프로젝트 이름과 동일합니다(예: VisualAcademy.ApiService.xml).

생성된 XML 예제

<?xml version="1.0"?>
<doc>
  <assembly>
    <name>VisualAcademy.ApiService</name>
  </assembly>
  <members>
    <member name="T:VisualAcademy.ApiService.Controllers.WeatherForecastController">
      <summary>
      WeatherForecastController는 날씨 데이터를 제공하는 API입니다.
      </summary>
    </member>
    <member name="M:VisualAcademy.ApiService.Controllers.WeatherForecastController.Get">
      <summary>
      날씨 데이터를 가져옵니다.
      </summary>
      <returns>날씨 데이터 목록</returns>
    </member>
  </members>
</doc>

8-6 Swagger UI에서 문서 확인

Swagger UI에서 API 설명이 각 엔드포인트와 함께 표시됩니다.

  1. Swagger UI 실행:

    • 브라우저에서 Swagger UI(예: https://localhost:5001/)를 열어 API 설명을 확인합니다.
  2. 결과:

    • [SwaggerResponse]를 통해 정의된 상태 코드와 응답 설명이 Swagger UI에 명확히 표시됩니다.
    • XML 주석은 엔드포인트의 개요 및 반환 데이터를 설명합니다.

9. 요약

완료된 작업

  1. <GenerateDocumentationFile> 속성을 통해 XML 문서를 생성하도록 설정.
  2. 컨트롤러 및 메서드에 XML 주석을 작성하여 API 설명 추가.
  3. [SwaggerResponse] 특성을 사용하여 응답 코드와 설명을 명시적으로 정의.
  4. Swagger와 XML 문서를 통합하여 Swagger UI에서 API 설명과 응답 상태 코드를 명확히 표시.

장점

  • XML 주석과 [SwaggerResponse] 특성으로 작성된 API 문서는 Swagger UI와 IntelliSense에서 활용 가능합니다.
  • 응답 상태 코드와 설명이 명확히 제공되어 API 사용자와 개발자 간 소통이 원활해집니다.
VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com