Ответ
В проекте использовалась аутентификация на основе JWT (JSON Web Tokens) по схеме Bearer Token в связке с Refresh Tokens для баланса безопасности и удобства пользователя.
Архитектура потока
- Логин: Клиент отправляет
POST /api/auth/loginс учетными данными. - Верификация: Сервер проверяет логин/пароль (обычно против хэша в БД).
- Генерация токенов:
- Access Token (JWT): Короткоживущий (15-30 мин), содержит claims (userId, roles, permissions). Подписывается секретным ключом.
- Refresh Token: Долгоживущий (7-30 дней), хранится в БД в связке с userId, IP-адресом (опционально) и может быть отозван.
- Ответ: Сервер возвращает оба токена клиенту.
- Доступ к API: Клиент прикладывает Access Token в заголовке
Authorization: Bearer <token>к каждому запросу. - Обновление: По истечении срока 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 хранится вHttpOnlycookie для защиты от XSS. - Отзыв токенов: При смене пароля или выходе (logout) Refresh Token удаляется/инвалидируется на сервере.
- Валидация: Проверка срока действия, подписи, издателя и аудитории.
Ответ 18+ 🔞
Смотри, вот тебе как это всё работает, если на пальцах объяснять. Представь, что у тебя есть клуб, а я — вышибала.
JWT (JSON Web Tokens) — это, по сути, твой пропуск в этот клуб. Не билетик бумажный, который можно подделать в подворотне, а типа электронный браслет. Схема Bearer Token — это как если бы ты просто показывал этот браслет и тебя пускали. Кто браслет принёс, тот и молодец, поэтому его как семечки воровать нельзя.
А Refresh Tokens — это такая волшебная фигня, чтобы тебе, ленивой жопе, не приходилось каждый раз заново паспорт показывать на входе, если браслет сдох.
Как вся эта карусель крутится
- Ты пришёл в клуб (Логин). Суёшь мне на стойке свою банковскую карту и паспорт (
POST /api/auth/login). - Я проверяю, не мудак ли ты (Верификация). Смотрю в список, сверяю фото — в общем, стандартная процедура.
- Я тебе выписываю пропуска (Генерация токенов).
- Access Token (JWT): Это твой основной браслет. Действует недолго, скажем, полчаса. Внутри него записано, кто ты (ID), на какие танцполы тебе можно (роли) и можно ли тебе заказывать шампанское в долг (права). Подписан он секретным ключом, который знаю только я и бармен. Подделать его — это надо быть семи пядей во лбу.
- Refresh Token: Это типа твоя фотка в нашей базе. Живёт долго, неделю или месяц. Лежит у нас в сейфе (в БД), привязана к твоей роже, и мы ещё можем записать, с какого ты IP-адреса пришёл, на всякий случай.
- Я тебе их вручаю (Ответ). Держи, мужик, браслет и расписка, что ты свой.
- Ты гуляешь по клубу (Доступ к API). Подходишь к любому бару или танцполу, показываешь браслет (
Authorization: Bearer <token>), и если всё ок — тебе наливают. - Браслет сел (Обновление). Через полчаса браслет потух. Ты не бежишь снова паспорт показывать, а идёшь ко мне и говоришь: «Слушай, а вот у меня ещё фотка в твоей базе есть, выдай новый браслет» (
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 в базе. Старая фотка в сейфе больше недействительна.
- Валидируй всё подряд. Срок, подпись, кто выдал, кому предназначен — всё проверяй, не доверяй никому. В мире полно мудаков, которые попробуют подсунуть тебе фальшивку.