- Модель данных
public class ApiClient
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsActive { get; set; } = true;
}
- Сервис для работы с API ключами
public interface IApiKeyService
{
Task<ApiClient?> GetClientByIdAsync(Guid id);
Task<bool> ValidateTokenAsync(string token, Guid clientId, long timestamp);
string GenerateApiKey();
}
public class ApiKeyService : IApiKeyService
{
private readonly ApplicationDbContext _context;
public ApiKeyService(ApplicationDbContext context)
{
_context = context;
}
public async Task<ApiClient?> GetClientByIdAsync(Guid id)
{
return await _context.ApiClients
.FirstOrDefaultAsync(c => c.Id == id && c.IsActive);
}
public async Task<bool> ValidateTokenAsync(string token, Guid clientId, long timestamp)
{
// Проверяем временную метку (например, ±5 минут)
var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime;
var currentTime = DateTime.UtcNow;
if (Math.Abs((currentTime - requestTime).TotalMinutes) > 5)
return false;
// Получаем клиента
var client = await GetClientByIdAsync(clientId);
if (client == null) return false;
// Формируем ожидаемый токен
var expectedToken = GenerateHash($"{client.ApiKey}:{clientId}:{timestamp}");
return string.Equals(token, expectedToken, StringComparison.OrdinalIgnoreCase);
}
public string GenerateApiKey()
{
var key = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
return GenerateHash(key);
}
private static string GenerateHash(string input)
{
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hash = sha256.ComputeHash(bytes);
return Convert.ToHexString(hash).ToLower();
}
}
- Кастомный AuthenticationHandler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private readonly IApiKeyService _apiKeyService;
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IApiKeyService apiKeyService)
: base(options, logger, encoder)
{
_apiKeyService = apiKeyService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Пытаемся получить токен из заголовка
if (!Request.Headers.TryGetValue("X-API-Key", out var apiKeyHeader) ||
!Request.Headers.TryGetValue("X-Client-Id", out var clientIdHeader) ||
!Request.Headers.TryGetValue("X-Timestamp", out var timestampHeader))
{
return AuthenticateResult.Fail("Missing API authentication headers");
}
var token = apiKeyHeader.ToString();
if (!Guid.TryParse(clientIdHeader, out var clientId))
return AuthenticateResult.Fail("Invalid client ID format");
if (!long.TryParse(timestampHeader, out var timestamp))
return AuthenticateResult.Fail("Invalid Timestamp format");
// Валидируем токен
var isValid = await _apiKeyService.ValidateTokenAsync(token, clientId, timestamp);
if (!isValid)
return AuthenticateResult.Fail("Invalid API token");
// Получаем клиента
var client = await _apiKeyService.GetClientByIdAsync(clientId);
if (client == null)
return AuthenticateResult.Fail("Client not found");
// Создаем claims
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, client.Id.ToString()),
new Claim(ClaimTypes.Name, client.Name),
new Claim("ClientType", "ApiClient")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
}
- Кастомный AuthorizationHandler
public class ClientAuthorizationHandler : AuthorizationHandler<ClientRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ClientRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "ClientType" && c.Value == "ApiClient"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class ClientRequirement : IAuthorizationRequirement
{
}
- Контроллер для управления клиентами
[ApiController]
[Route("api/[controller]")]
public class ClientsController : ControllerBase
{
private readonly IApiKeyService _apiKeyService;
private readonly ApplicationDbContext _context;
public ClientsController(IApiKeyService apiKeyService, ApplicationDbContext context)
{
_apiKeyService = apiKeyService;
_context = context;
}
[HttpPost]
[Authorize] // Стандартная авторизация для админов
public async Task<IActionResult> CreateClient([FromBody] CreateClientRequest request)
{
var client = new ApiClient
{
Id = Guid.NewGuid(),
Name = request.Name,
ApiKey = _apiKeyService.GenerateApiKey(),
CreatedAt = DateTime.UtcNow,
IsActive = true
};
_context.ApiClients.Add(client);
await _context.SaveChangesAsync();
// Передаем клиенту
return Ok(new
{
ClientId = client.Id,
ApiKey = client.ApiKey
});
}
}
public class CreateClientRequest
{
public string Name { get; set; } = string.Empty;
}
- Настройка в Program.cs
var builder = WebApplication.CreateBuilder(args);
// База данных
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Стандартная аутентификация (JWT или другая)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// Настройки JWT
});
// Кастомная аутентификация API ключей
builder.Services.AddAuthentication()
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationOptions.DefaultScheme,
options => { });
// Авторизация
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Client", policy =>
policy.Requirements.Add(new ClientRequirement()));
});
// Сервисы
builder.Services.AddScoped<IApiKeyService, ApiKeyService>();
builder.Services.AddScoped<IAuthorizationHandler, ClientAuthorizationHandler>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
- Использование в контроллерах
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
[HttpGet("client-data")]
[Authorize(Policy = "Client")] // Только для API клиентов
public IActionResult GetClientData()
{
var clientId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var clientName = User.FindFirst(ClaimTypes.Name)?.Value;
return Ok(new { message = $"Hello {clientName} ({clientId})" });
}
[HttpGet("user-data")]
[Authorize] // Стандартная авторизация
public IActionResult GetUserData()
{
return Ok(new { message = "Hello regular user" });
}
[HttpGet("mixed-data")]
[Authorize] // Любой авторизованный пользователь (и стандартный, и API клиент)
public IActionResult GetMixedData()
{
var isApiClient = User.HasClaim("ClientType", "ApiClient");
return Ok(new {
message = "Hello authenticated entity",
isApiClient = isApiClient
});
}
}
- Пример запроса от клиента
GET /api/secure/client-data HTTP/1.1
Host: yourapi.com
X-API-Key: d49deb22c6e43bfdd9e161d5f1d4710e4300570845a4fe16e0263140e2a7c510
X-Server-Id: 12345678-1234-1234-1234-123456789abc
X-Timestamp: 1759091560