ASP.NET Core Web API에서 Basic 인증으로 WeatherForecast API 보호하기
이 문서에서는 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 주입
TimeProvider
는 Program.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. 주의사항
- 보안: Basic 인증은 보안이 강하지 않으므로 테스트나 간단한 용도로만 사용하세요. 반드시 HTTPS를 활성화해 전송 데이터 암호화를 보장하세요.
- 확장성: 고정된 이메일과 암호 대신 데이터베이스 기반 인증 또는 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 인증을 수행합니다.
Swagger UI 실행:
- 앱을 실행하고 브라우저에서 기본 경로(예:
https://localhost:5001/
)로 접속합니다. - Swagger UI가 기본적으로 표시됩니다.
- 앱을 실행하고 브라우저에서 기본 경로(예:
Authorize
버튼 클릭:- Swagger UI 상단의
Authorize
버튼을 클릭합니다.
- Swagger UI 상단의
인증 정보 입력:
- 사용자 이름:
admin@visualacademy.com
- 비밀번호:
securepassword
- 정보를 입력한 후 Authorize 버튼을 클릭합니다.
- 사용자 이름:
인증 성공 후 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 설명이 각 엔드포인트와 함께 표시됩니다.
Swagger UI 실행:
- 브라우저에서 Swagger UI(예:
https://localhost:5001/
)를 열어 API 설명을 확인합니다.
- 브라우저에서 Swagger UI(예:
결과:
[SwaggerResponse]
를 통해 정의된 상태 코드와 응답 설명이 Swagger UI에 명확히 표시됩니다.- XML 주석은 엔드포인트의 개요 및 반환 데이터를 설명합니다.
9. 요약
완료된 작업
<GenerateDocumentationFile>
속성을 통해 XML 문서를 생성하도록 설정.- 컨트롤러 및 메서드에 XML 주석을 작성하여 API 설명 추가.
[SwaggerResponse]
특성을 사용하여 응답 코드와 설명을 명시적으로 정의.- Swagger와 XML 문서를 통합하여 Swagger UI에서 API 설명과 응답 상태 코드를 명확히 표시.
장점
- XML 주석과
[SwaggerResponse]
특성으로 작성된 API 문서는 Swagger UI와 IntelliSense에서 활용 가능합니다. - 응답 상태 코드와 설명이 명확히 제공되어 API 사용자와 개발자 간 소통이 원활해집니다.