Как была реализована аутентификация в API на вашем проекте?

Ответ

В проекте использовалась аутентификация на основе JWT (JSON Web Tokens) по схеме Bearer Token в связке с Refresh Tokens для баланса безопасности и удобства пользователя.

Архитектура потока

  1. Логин: Клиент отправляет POST /api/auth/login с учетными данными.
  2. Верификация: Сервер проверяет логин/пароль (обычно против хэша в БД).
  3. Генерация токенов:
    • Access Token (JWT): Короткоживущий (15-30 мин), содержит claims (userId, roles, permissions). Подписывается секретным ключом.
    • Refresh Token: Долгоживущий (7-30 дней), хранится в БД в связке с userId, IP-адресом (опционально) и может быть отозван.
  4. Ответ: Сервер возвращает оба токена клиенту.
  5. Доступ к API: Клиент прикладывает Access Token в заголовке Authorization: Bearer <token> к каждому запросу.
  6. Обновление: По истечении срока Access Token, клиент использует Refresh Token для вызова POST /api/auth/refresh и получения новой пары токенов.

Реализация в ASP.NET Core

Конфигурация сервисов (Program.cs / Startup.cs):

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = Configuration["Jwt:Audience"],
            ValidateLifetime = true, // Критически важно!
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])
            ),
            ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.Zero // Убираем запас времени для точной проверки срока.
        };
        // Для аутентификации из WebSocket или SignalR соединений
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                // Извлекаем токен из query string для WebSockets
                if (!string.IsNullOrEmpty(accessToken))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy =>
        policy.RequireClaim("role", "Admin"));
});

Генерация токена (пример):

public string GenerateAccessToken(User user)
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim("role", user.Role),
        new Claim("custom_claim", "some_value")
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"],
        audience: _config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(30), // Короткая жизнь
        signingCredentials: creds);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

Защита endpoint'ов:

[ApiController]
[Route("api/[controller]")]
[Authorize] // Все методы контроллера требуют аутентификации
public class SecureController : ControllerBase
{
    [HttpGet("admin-data")]
    [Authorize(Policy = "RequireAdminRole")] // Дополнительная проверка роли
    public IActionResult GetAdminData() { ... }

    [HttpGet("profile")]
    public IActionResult GetProfile()
    {
        // Получение ID текущего пользователя из токена
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        ...
    }
}

Дополнительные меры безопасности

  • HTTPS: Обязательное использование для всего трафика.
  • Хранение на клиенте: Access Token хранится в памяти (переменная JS), а не в localStorage, чтобы избежать XSS-атак. Refresh Token хранится в HttpOnly cookie для защиты от XSS.
  • Отзыв токенов: При смене пароля или выходе (logout) Refresh Token удаляется/инвалидируется на сервере.
  • Валидация: Проверка срока действия, подписи, издателя и аудитории.

Ответ 18+ 🔞

Смотри, вот тебе как это всё работает, если на пальцах объяснять. Представь, что у тебя есть клуб, а я — вышибала.

JWT (JSON Web Tokens) — это, по сути, твой пропуск в этот клуб. Не билетик бумажный, который можно подделать в подворотне, а типа электронный браслет. Схема Bearer Token — это как если бы ты просто показывал этот браслет и тебя пускали. Кто браслет принёс, тот и молодец, поэтому его как семечки воровать нельзя.

А Refresh Tokens — это такая волшебная фигня, чтобы тебе, ленивой жопе, не приходилось каждый раз заново паспорт показывать на входе, если браслет сдох.

Как вся эта карусель крутится

  1. Ты пришёл в клуб (Логин). Суёшь мне на стойке свою банковскую карту и паспорт (POST /api/auth/login).
  2. Я проверяю, не мудак ли ты (Верификация). Смотрю в список, сверяю фото — в общем, стандартная процедура.
  3. Я тебе выписываю пропуска (Генерация токенов).
    • Access Token (JWT): Это твой основной браслет. Действует недолго, скажем, полчаса. Внутри него записано, кто ты (ID), на какие танцполы тебе можно (роли) и можно ли тебе заказывать шампанское в долг (права). Подписан он секретным ключом, который знаю только я и бармен. Подделать его — это надо быть семи пядей во лбу.
    • Refresh Token: Это типа твоя фотка в нашей базе. Живёт долго, неделю или месяц. Лежит у нас в сейфе (в БД), привязана к твоей роже, и мы ещё можем записать, с какого ты IP-адреса пришёл, на всякий случай.
  4. Я тебе их вручаю (Ответ). Держи, мужик, браслет и расписка, что ты свой.
  5. Ты гуляешь по клубу (Доступ к API). Подходишь к любому бару или танцполу, показываешь браслет (Authorization: Bearer <token>), и если всё ок — тебе наливают.
  6. Браслет сел (Обновление). Через полчаса браслет потух. Ты не бежишь снова паспорт показывать, а идёшь ко мне и говоришь: «Слушай, а вот у меня ещё фотка в твоей базе есть, выдай новый браслет» (POST /api/auth/refresh). Я смотрю — да, фотка твоя, не отозвана — и выписываю тебе новую парочку (и браслет, и даже фотку-то новую могу выдать).

Как это впихнули в ASP.NET Core

Настраиваем вышибал (Program.cs):

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true, // Проверяем, я ли этот пропуск выдал
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidateAudience = true, // Проверяем, для моего ли клуба он
            ValidAudience = Configuration["Jwt:Audience"],
            ValidateLifetime = true, // А НЕ ПРОСРОЧЕН ЛИ ОН, БЛЯДЬ? Самое важное!
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]) // Секретный ключ
            ),
            ValidateIssuerSigningKey = true, // Проверяем подпись
            ClockSkew = TimeSpan.Zero // Убираем все «пять минут на дорогу», срок есть срок.
        };
        // Это если у нас ещё и комнаты с веб-сокетами (типа приватные кабинки)
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                // Если токен припёрли в строке запроса (как в кабинку ключ передают)
                if (!string.IsNullOrEmpty(accessToken))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

// Настраиваем правила: кому куда можно
services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy =>
        policy.RequireClaim("role", "Admin")); // Только тем, у кого в браслете написано "Admin"
});

Штампуем новые браслеты (генерация токена):

public string GenerateAccessToken(User user)
{
    // Вшиваем в браслет данные о пользователе
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim("role", user.Role),
        new Claim("custom_claim", "some_value") // Можно и свою хуйню записать
    };

    // Берём наш секретный ключ и решаем, как будем подписывать
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    // Собираем сам токен
    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"], // Кто выдал (наш клуб)
        audience: _config["Jwt:Audience"], // Для кого (для нашего API)
        claims: claims, // Данные внутри
        expires: DateTime.UtcNow.AddMinutes(30), // Живёт 30 минут, не больше
        signingCredentials: creds); // И подпись

    // Превращаем в строку, которую можно в заголовок сунуть
    return new JwtSecurityTokenHandler().WriteToken(token);
}

Расставляем вышибал у нужных дверей (контроллеры):

[ApiController]
[Route("api/[controller]")]
[Authorize] // Сюда вообще без браслета не суйся, хоть умри
public class SecureController : ControllerBase
{
    [HttpGet("admin-data")]
    [Authorize(Policy = "RequireAdminRole")] // А сюда только с браслетом, где роль "Admin"
    public IActionResult GetAdminData() { ... }

    [HttpGet("profile")]
    public IActionResult GetProfile()
    {
        // А тут внутри можно посмотреть, а кто, собственно, пришёл?
        // Вытаскиваем ID из того, что вшито в токен.
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        ...
    }
}

Чтобы совсем не обосраться (доп. безопасность)

  • HTTPS — обязательно. Всё это дело крутится только по защищённому каналу. Иначе любой сосед по сети твой браслет перехватит.
  • Где хранить на фронте. Access Token — только в памяти приложения (переменная JS). Никакого localStorage, иначе какой-нибудь скриптик его украдёт. Refresh Token — только в HttpOnly куках, чтобы даже JS до него не дотянулся.
  • Умей отзывать. Пользователь вышел (logout) или пароль сменил — тут же гаси его Refresh Token в базе. Старая фотка в сейфе больше недействительна.
  • Валидируй всё подряд. Срок, подпись, кто выдал, кому предназначен — всё проверяй, не доверяй никому. В мире полно мудаков, которые попробуют подсунуть тебе фальшивку.