JWT의 기본 개념
JSON Web Token은 당사자 간 정보를 JSON 객체로 안전하게 전달하기 위한 개방형 규격(RFC 7519)이다. 디지털 서명을 통해 무결성과 발신자 신원을 보장하며, 사용자 인증·권한 부여·정보 교환에 널리 인다.
토큰 구성 요소
JWT는 점(.)으로 구분된 세 부분으로 이루어진다.
| 구성 | 설명 |
| Header | 알고리즘과 토큰 유형 정보 |
| Payload | 사용자 식별자, 권한, 만료 시각 등 클레임 |
| Signature | Header와 Payload를 비밀키로 서명한 값 |
최종 형태: Base64Url(Header).Base64Url(Payload).Base64Url(Signature)
.NET Core 환경 설정
필요 패키지 설치
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
애플리케이션 설정
appsettings.json에 인증 파라미터를 정의한다.
{
"AuthSettings": {
"Key": "MinimumSixteenCharacters!",
"Issuer": "MyApplication",
"Recipient": "WebClients",
"DurationMinutes": 60
}
}
서비스 등록 및 파이프라인 구성
// Program.cs (.NET 6+)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
var authConfig = builder.Configuration.GetSection("AuthSettings");
var signingKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(authConfig["Key"]!));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg => {
cfg.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = authConfig["Issuer"],
ValidateAudience = true,
ValidAudience = authConfig["Recipient"],
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
토큰 발급 서비스 구현
public interface IAccessTokenGenerator {
string CreateToken(UserIdentity identity);
}
public class AccessTokenGenerator : IAccessTokenGenerator {
private readonly IConfiguration _config;
public AccessTokenGenerator(IConfiguration config) {
_config = config;
}
public string CreateToken(UserIdentity identity) {
var key = Encoding.UTF8.GetBytes(_config["AuthSettings:Key"]!);
var securityKey = new SymmetricSecurityKey(key);
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim> {
new(JwtRegisteredClaimNames.Sub, identity.UserId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, identity.Username),
new(JwtRegisteredClaimNames.Email, identity.Email),
new("role", identity.Role),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var tokenDescriptor = new JwtSecurityToken(
issuer: _config["AuthSettings:Issuer"],
audience: _config["AuthSettings:Recipient"],
claims: claims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(
double.Parse(_config["AuthSettings:DurationMinutes"]!)),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
}
}
보호된 엔드포인트 구성
[ApiController]
[Route("[controller]")]
public class SessionController : ControllerBase {
private readonly IAccessTokenGenerator _tokenGenerator;
public SessionController(IAccessTokenGenerator generator) {
_tokenGenerator = generator;
}
[HttpPost("authenticate")]
public ActionResult<AuthResponse> Authenticate(LoginModel model) {
// 실제로는 데이터베이스 검증 수행
if (!ValidateCredentials(model)) {
return Unauthorized();
}
var identity = new UserIdentity {
UserId = 1001,
Username = model.Username,
Email = "user@example.com",
Role = "StandardUser"
};
var token = _tokenGenerator.CreateToken(identity);
return Ok(new AuthResponse { AccessToken = token });
}
[HttpGet("admin-only")]
[Authorize(Roles = "SystemAdmin")]
public IActionResult AdminResource() {
return Ok(new { Data = "관리자 전용 정보" });
}
}
JwtBearerOptions 상세 설정
증 파라미터
| 속성 | 기능 | 권장 설정 |
| TokenValidationParameters | 발행자, 수신자, 서명키, 유효기간 등 검증 규칙 정의 | 모든 검증 항목 명시적 활성화 |
| Authority | OIDC 공개 메타데이터를 자동 수집할 인증 서버 주소 | 신뢰할 수 있는 ID 공급자 URL |
| Audience | 토큰의 의도된 수신자 식별자 | API 식별자와 일치 |
메타데이터 관리
| 속성 | 기능 | 권장 설정 |
| MetadataAddress | 공개 메타데이터 문서(.well-known) 직접 지정 | Authority로 자동 생성 시 불필요 |
| RequireHttpsMetadata | 메타덷 주소의 HTTPS 강제 여부 | 운영 환경 필수 true |
| AutomaticRefreshInterval | 메타데이터 자동 갱신 주기 | 기본 24시간, 키 교체 주고 고려 |
| RefreshOnIssuerKeyNotFound | 서명키 미발견 시 자동 재조회 | true (키 롤오버 대응) |
통신 및 디버깅
| 속성 | 기능 | 권장 설정 |
| BackchannelTimeout | 메타데이터 서버 통신 타임아웃 | 네트워크 환경에 따라 30~60초 |
| Backchannel | 커스텀 HttpClient 인스턴스 주입 | 프록시/인증서 필요 시 사용 |
| IncludeErrorDetails | 인증 실패 시 상세 오류 응답 포함 | 개발 true, 운영 false |
| SaveToken | 검증된 토큰을 HttpContext에 보존 | 다운스트림 API 호출 시 true |
클레임 및 호환성
| 속성 | 기능 | 권장 설정 |
| MapInboundClaims | JWT 표준 클레임을 .NET ClaimTypes으로 매핑 | 표준 라이브러리 연동 시 true |
| TokenHandlers | 비동기 토큰 처리기 목록 | JsonWebTokenHandler 우선 사용 |
| UseSecurityTokenValidators | 레거시 검증기 강제 사용 | 새 프로젝트는 false 유지 |
실용적 구성 예시
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts => {
// 검증 규칙
opts.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["AuthSettings:Key"]!)),
ValidateLifetime = true,
ValidateIssuer = true,
ValidIssuer = builder.Configuration["AuthSettings:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["AuthSettings:Recipient"],
ClockSkew = TimeSpan.Zero
};
// 메타데이터 보안
opts.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
// 이벤트 처리
opts.Events = new JwtBearerEvents {
OnAuthenticationFailed = ctx => {
// 로깅 또는 모니터링 연동
return Task.CompletedTask;
},
OnTokenValidated = ctx => {
// 추가 권한 검증 또는 감사 로깅
return Task.CompletedTask;
}
};
});
클라이언트 요청 예시
// HTTP 헤더에 토큰 포함
GET /api/protected-resource HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
주의사항
- Payload는 Base64Url 인코딩될 뿐 암호화되지 않으므로 민감 정보 저장 금지
- 대칭키(HMAC)는 서버 간 공유, 비대칭키(RSA/ECDSA)는 공개키로 검증
- 토큰 폐기(로그아웃)는 블랙리스트 또는 짧은 만료 시간 + 재발급 전략으로 구현
- HTTPS 미사용 시 토큰 탈취 위험, 반드시 TLS 적용