api_auth.md 19 KB

Предыдущая лекция Следующая лекция
API. REST API. Создание сервера ASP.NET Core. Swagger. Содержание HTTP запросы в C#. Получение списка материалов выбранного продукта

Аутентификация и авторизация

Введение в аутентификацию и авторизацию

Теория взята отсюда

Важное место в приложении занимает аутентификация и авторизация. Аутентификация представляет процесс определения пользователя. Авторизация представляет процесс определения, имеет ли пользователь право доступа к некоторому ресурсу. То есть, если аутентификация отвечает на вопрос "Кем является пользователь?", то авторизация отвечает на вопрос "Какие права пользователь имеет в системе?" ASP.NET Core имеет встроенную поддержку аутентификации и авторизации.

Аутентификация

Для выполнения аутентификации в конвейере обработки запроса отвечает специальный компонент middleware - AuthenticationMiddleware. Для встраивания этого middleware в конвейер применяется метод расширения UseAuthentication()

Следует отметить, что метод UseAuthentication() должен встраиваться в конвейер до любых компонентов middleware, которые используют аутентификацию пользователей (но после Swagger).

Для выполнения аутентификации этот компонент использует сервисы аутентификации, в частности, сервис IAuthenticationService, которые регистрируются в приложении с помощью метода AddAuthentication():

В качестве параметров метод AddAuthentication() может принимать схему аутентификации в виде строки и делегат, который устанавливает опции аутентификации - объект AuthenticationOptions.

Наиболее расcпространенные схемы аутентификации:

  • Basic: аутентификация на основе имени и пароля, мы в дальнейшем будем использовть его, т.к. он используется на соревнованиях. Но в реальной разработке он уже не используется и в ASP.NET Core не имеет реализации. Есть сторонние пакеты, но мы напишем свою реализацию.
  • Cookies: аутентификация на основе куки. Хранится в константе CookieAuthenticationDefaults.AuthenticationScheme
  • Bearer: аутентификация на основе jwt-токенов. Хранится в константе JwtBearerDefaults.AuthenticationScheme (вообще токены могут быть любые, а не только JWT)

Схема аутентификации позволяет выбирать определенный обработчик аутентификации. Обработчик аутентификации собственно и выполняет непосредственную аутентификацию пользователей на основе данных запроса и исходя из схемы аутентификации.

Например, для аутентификации с помощью куки передается схема "Cookies". Соответственно для аутентификации пользователя будет выбираться встроенный обработчик аутентификации - класс Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler, который на основе полученных в запросе cookie выполняет аутентификацию.

А если используется схема "Bearer", то это значит, что для аутентификации будет использоваться jwt-токен, а в качестве обработчика аутентификации будет применяться класс Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler. Стоит отметить, что для аутентификации с помощью jwt-токенов необходимо добавить в проект через Nuget пакет Microsoft.AspNetCore.Authentication.JwtBearer `` При чем в ASP.NET Core мы не ограничены встроенными схемами аутентификации и можем создавать свои кастомные схемы и под них своих обработчиков аутентификации.

Кроме применения схемы аутентификации необходимо подключить аутентификацию определенного типа. Для этого можно использовать следуюшие методы:

  • AddCookie(): подключает и конфигурирует аутентификацию с помощью куки.

  • AddJwtBearer(): подключает и конфигурирует аутентификацию с помощью jwt-токенов (для этого метода необходим Nuget-пакет Microsoft.AspNetCore.Authentication.JwtBearer)

Оба метода реализованы как методы расширения для типа AuthenticationBuilder, который возвращается методом AddAuthentication():

var builder = WebApplication.CreateBuilder();

// добавление сервисов аутентификации
// схема аутентификации - с помощью jwt-токенов
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer();
 
var app = builder.Build();
 
// добавление middleware аутентификации 
app.UseAuthentication();

Авторизация

Авторизация представляет процесс определения прав пользователя в системе, к каким ресурсам приложения он имеет право доступа и при каких условиях.

Хотя авторизация представляет отдельный независимый процесс, тем не менее для нее также необходимо, чтобы приложение также применяло аутентификацию.

Для подключения авторизации необходимо встроить компонент Microsoft.AspNetCore.Authorization.AuthorizationMiddleware. Для этого применяется встроенный метод расширения UseAuthorization()

Кроме того, для применения авторизации необходимо зарегистрировать сервисы авторизации с помощью метода AddAuthorization()

Одна из версий метода принимает делегат, который с помощью параметра AuthorizationOptions позволяет сконфигурировать авторизацию.

Ключевыми элементами механизма авторизации в ASP.NET Core являются методы AllowAnonymous и RequireAuthorization которые позволяет ограничить доступ к ресурсам приложения. Например:

var builder = WebApplication.CreateBuilder();
 
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer();
builder.Services.AddAuthorization();
 
var app = builder.Build();
 
app.UseAuthentication();
app.UseAuthorization();
 

app.MapGet("/private", () => "Ресурс только для авторизованных пользователей!")
    .RequireAuthorization();

app.MapGet("/", () => "Общедоступный ресурс")
    .AllowAnonymous();
 
app.Run();

Здесь в приложении определены две конечных точки: / и /private. Конечная точка / должна быть доступна всем пользователям, поэтому к ней применяется метод AllowAnonymous (разрешить не авторизованным), а к конечной точке /private применяется метод RequireAuthorization (только авторизованным).

Если мы обратимся к конечной точке /, то у нас не возникнет никаких проблем.

Однако если мы обратимся к ресурсу /private, то мы получим ошибку 401, которая говорит о том, что пользователь не авторизован для доступа к этому ресурсу.

Дальше всю теорию я расписывать не буду, но вы можете почитать подробнее тут

Реализация "Basic" авторизации

Данные об авторизации, если она нужна, передаются в заголовке запроса:

Authorization: <тип авторизации> <данные для авторизации>

При базовой аторизации

  • тип авторизации: Basic
  • данные для авторизации: строка в формате логин:пароль закодированная base64

    Authorization: Basic base64encodedloginandpassword
    

Код для реализации нарыл у GPT, в инете вменяемых не нашёл

  1. Создайте класс BasicAuthenticationHandler, который должен наследоваться от класса AuthenticationHandler

    Класс привожу с комментариями, можно использовать как есть

    public class BasicAuthenticationHandler:
        AuthenticationHandler<AuthenticationSchemeOptions>
    {
        // конструктор, реализация не нужна
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options, 
            ILoggerFactory logger, 
            UrlEncoder encoder, 
            ISystemClock clock) : base(options, logger, encoder, clock)
        {
        }
    
        // вся "движуха" происходит в этом методе
        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // если в заголовках нет аттрибута Authorization 
            // то результат авторизации Fail (не успешный)
            if (!Request.Headers.ContainsKey("Authorization"))
                return Task.FromResult(
                    AuthenticateResult.Fail(
                        "Missing Authorization header"));
    
            try
            {
                // получаем строку авторизации
                var authorizationHeader = Request
                    .Headers["Authorization"].ToString();
                    
                // я сделал логгирование всех этапов
                // в релизе можно вырезать
                Console.WriteLine(
                    $"authorizationHeader = {authorizationHeader}");
                    
                // достаем из строки "Basic base64data" вторую подстроку
                var base64EncodedUsernamePassword = authorizationHeader
                    .Split(' ')[1];
                    
                Console.WriteLine(
                    $"base64EncodedUsernamePassword = {base64EncodedUsernamePassword}");
                    
                // декодируем параметры
                var usernamePasswordString = Encoding.UTF8.GetString(
                    Convert.FromBase64String(base64EncodedUsernamePassword));
    
                Console.WriteLine(
                    $"usernamePasswordString = {usernamePasswordString}");
                    
                // разбиваем строку на логин и пароль
                var usernamePasswordArr = usernamePasswordString.Split(':');
    
                // Тут логин/пароль прибиты гвоздями, 
                // но в реальном приложении надо 
                // считывать из таблицы пользователей
                if (
                    usernamePasswordArr[0] == "admin" && 
                    usernamePasswordArr[1] == "password")
                {
                    // тут какая-то химия от мелкософта
                    var claims = new[] {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            usernamePasswordArr[0]),
                        new Claim(
                            ClaimTypes.Name, 
                            usernamePasswordArr[0])
                    };
    
                    var identity = new ClaimsIdentity(
                        claims, Scheme.Name);
    
                    var principal = new ClaimsPrincipal(identity);
    
                    var ticket = new AuthenticationTicket(
                        principal, Scheme.Name);
    
                    // успешная авторизация
                    return Task.FromResult(
                        AuthenticateResult.Success(ticket));
                }
    
                // логин/пароль не совпали
                return Task.FromResult(
                    AuthenticateResult.Fail("Invalid username or password"));
            }
            catch
            {
                // при любых ошибках разбора тоже ошибка
                return Task.FromResult(
                    AuthenticateResult.Fail("Invalid Authorization header"));
            }
        }
    }
    

  2. В основном файле (Program.cs) добавьте сервисы аутентификации и авторизации и подключите авторизацию к конечным точкам:

    var builder = WebApplication.CreateBuilder(args);
    
    // добавляем метод аутентификации "Basic"
    // и задаём обработчиком созданный ранее класс
    builder.Services.AddAuthentication("Basic")
        .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", options => { });
    
    // это скопировано как есть из интернета
    builder.Services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser().Build();
    });
    
    var app = builder.Build();
    
    // включаем авторизацию и аутентификацию
    app.UseAuthentication();
    app.UseAuthorization();
    
    // к конечным точкам дописываем RequireAuthorization 
    // или AllowAnonimous
    app.MapGet("/", () => "Hello World!")
        .RequireAuthorization();
    
  3. Настройка авторизации в Swagger

    Взял отсюда

    В Swagger можно настроить заполнение логина и пароля, чтобы соответствующий заголовок автоматически добавлялся в запросы. Для этого надо добавить параметры в метод AddSwaggerGen:

    builder.Services.AddSwaggerGen(options =>
    {
        // Метод AddSecurityDefinition добавляет
        // кнопку "Авторизовать" в верхней чести страницы
        // которая настраивает заголовок авторизации
        options.AddSecurityDefinition("Basic", new OpenApiSecurityScheme
        {
            Description = "Введите логин и пароль",
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.Http,
            Scheme = "Basic"
        });
        // Метод AddSecurityRequirement 
        // добавит заголовок авторизации 
        // к каждой конечной точке при отправке запроса:
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Basic"
                    }
                },
                new List<string>()
            }
        });
    });
    
  4. Swagger в этот проект я не прикручивал, вы можете сделать запрос из Postman-а или из плагина к VSCode (REST Client)

    В REST Client запрос выглядит так:

    GET http://localhost:8080/
    Authorization: Basic admin:password
    

    В логах сервера видно как выковыриваются данные для авторизации:

    authorizationHeader = Basic YWRtaW46cGFzc3dvcmQ=
    base64EncodedUsernamePassword = YWRtaW46cGFzc3dvcmQ=
    usernamePasswordString = admin:password
    

    Всё работает, можете использовать в своих проектах.


    Предыдущая лекция Следующая лекция
    API. REST API. Создание сервера ASP.NET Core. Swagger. Содержание HTTP запросы в C#. Получение списка материалов выбранного продукта