Содержание
В предыдущей части мы разработали небольшое приложение, позволяющее аутентифицировать пользователя с использованием токенов JWT. Также мы протестировали работу приложения с использованием сервиса ReqBin и убедились, что токен работает — получили доступ к конечной точке, требующей авторизации пользователя. Однако, при разработке приложений (если это не обычный API для получения доступа к данным от сторонних клиентов) необходимо также предусмотреть и авторизацию пользователей с использованием полученного JWT-токена. И сегодня мы рассмотрим один из возможных вариантов того как происходит авторизация клиента с помощью JWT в ASP.NET Core
Как известно, любую задачу в программировании можно решить несколькими способами. Поле получения JWT-токена мы можем поступать с ним как угодно — например, написать клиент на JavaScript для передачи токена в заголовке Authorization
. Также, мы можем воспользоваться возможностями ASP.NET Core и, например, сохранять полученный токен в cookies или использовать сессии. Рассмотрим один из вариантов работы с токеном через cookies.
На данный момент, в нашем приложении определена конечная точка для входа пользователя в систему:
app.MapGet("/login", (string? login, string? password, HttpContext context) => { if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) return Results.BadRequest(new { Error = "Не задан логин или пароль" }); var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password)); if (user == null) return Results.BadRequest(new { Error = "Не верно введен логин или пароль" }); JwtSecurityToken token = new( issuer: authOptions?.Issuer, audience: authOptions?.Audience, expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) ); JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); string tokenString = handler.WriteToken(token); return Results.Ok(new { token = tokenString, user = login }); });
Здесь мы можем сделать следующее — передать cookie с токеном клиенту и, затем, получать значение токена при каждом запросе. Допишем код конечной точки следующим образом:
app.MapGet("/login", (string? login, string? password, HttpContext context) => { //здесь код формирования токена JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); string tokenString = handler.WriteToken(token); context.Response.Cookies.Append("token", tokenString);//отпраляем клиенту куку с токеном return Results.Ok(new { token = tokenString, user = login }); });
Теперь, как только пользователь введет верный логин и пароль, он получит токен в виде куки.
Авторизация клиента с помощью JWT
Чтобы клиент смог авторизоваться по этому токену необходимо каким-либо образом значение куки передать в заголовок авторизации. Сделать это можно, например, написав свой компонент middleware, который будет встраиваться в конвейер перед всеми конечными точками для которых необходима авторизация.
Компонент будет выглядеть достаточно просто:
app.Use(async (context, next) => { if (context.Request.Cookies.TryGetValue("token", out string? token)) context.Request.Headers.Authorization = $"Bearer {token}"; await next(); });
по сути, всё, что мы здесь делаем — это пробуем получить куку с именем «token» и, если таковая находится, то её значение передается в заголовок авторизации:
context.Request.Headers.Authorization = $"Bearer {token}";
Встраиваем этот middleware в начало конвейера запросов (сразу после строки var app = builder.Build();
):
var app = builder.Build(); app.Use(async (context, next) => { if (context.Request.Cookies.TryGetValue("token", out string? token)) context.Request.Headers.Authorization = $"Bearer {token}"; await next(); });
Протестируем наше приложение
получение токена по логину и паролю
получаем доступ к ресурсу для которого требуется авторизация:
так как мы ничего не меняли в приложении с прошлого раза, то в ответ нам, как и предполагалось, вернулись настройки генерации токена, что говорит нам о том, что наш способ авторизации клиента работает. Кроме этого, если подождать пару минут и повторить запрос, то получим ошибку доступа так как токен «живет» две минуты
Исходный код проекта
Ниже представлен весь исходный код проекта на данный момент
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Text; namespace AspAuth { public class AuthOptions { public string Issuer { get; set; } //издатель токена public string Audience { get; set; } //потребитель токена public string SecretKey { get; set; } //секретный ключ для подписи public bool ValidateAudience { get; set; } //проверять потребителя public bool ValidateIssuer { get; set; } //проверять издателя public bool ValidateLifetime { get; set; } //проверять время жизни токена public bool ValidateIssuerSigningKey { get; set; } //проверять подпись public int TokenLifetime { get; set; } //время жизни токена в минутах } public class User { public string Login { get; set; } public string Password { get; set; } } public class Program { public static void Main(string[] args) { //условная база данных с учетными записями пользователей List<User> UserDatabase = new() { new User() { Login = "user", Password ="12345" }, new User() { Login = "admin", Password = "root" }, }; var builder = WebApplication.CreateBuilder(args); var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>(); SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey)); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.RequireHttpsMetadata = true; options.SaveToken = true; options.IncludeErrorDetails = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = authOptions.ValidateIssuer, ValidateAudience = authOptions.ValidateAudience, ValidateLifetime = authOptions.ValidateLifetime, ValidateIssuerSigningKey = authOptions.ValidateIssuerSigningKey, ValidIssuer = authOptions.Issuer, ValidAudience = authOptions.Audience, IssuerSigningKey = key }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.Use(async (context, next) => { if (context.Request.Cookies.TryGetValue("token", out string? token)) context.Request.Headers.Authorization = $"Bearer {token}"; await next(); }); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/", () => "Hello World!"); app.MapGet("/auth", [Authorize] () => { return Results.Ok(authOptions); }); app.MapGet("/login", (string? login, string? password, HttpContext context) => { if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) return Results.BadRequest(new { Error = "Не задан логин или пароль" }); var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password)); if (user == null) return Results.BadRequest(new { Error = "Не верно введен логин или пароль" }); JwtSecurityToken token = new( issuer: authOptions?.Issuer, audience: authOptions?.Audience, expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) ); JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); string tokenString = handler.WriteToken(token); context.Response.Cookies.Append("token", tokenString);//отпраляем клиенту куку с токеном return Results.Ok(new { token = tokenString, user = login }); }); app.Run(); } } }
Итого
Сегодня мы рассмотрели один из возможных вариантов авторизации клиента по JWT-токену. Для сохранения и передачи токена на сервер мы использовали Cookies, а для формирования заголовка авторизации — написали свой компонент middleware, который встроили в самое начало конвейера запросов.