Изучение ПО промежуточного слоя для проверки подлинности файлов cookie в ASP.NET Core

  • Михаил
  • 12 мин. на прочтение
  • 127
  • 05 Dec 2022
  • 05 Dec 2022

Это вторая статья из серии, посвященной аутентификации и авторизации в ASP.NET Core. В предыдущем посте я рассказал об аутентификации в целом и о том, как работает аутентификация на основе утверждений. В этом посте я собираюсь более подробно рассказать о том, как AuthenticationMiddlewareреализовано в ASP.NET Core, на CookieAuthenticationMiddlewareпримере примера. Обратите внимание, что основное внимание уделяется тому, «как создается промежуточное программное обеспечение», а не «как использовать его в вашем приложении».

Аутентификация в ASP.NET Core

Напомним, что аутентификация — это процесс определения того, кем является пользователь, а авторизация вращается вокруг того, что ему разрешено делать. В этом посте мы имеем дело исключительно со стороной аутентификации конвейера.

Надеюсь, вы хорошо разбираетесь в аутентификации на основе утверждений в ASP.NET Core. Если нет, я рекомендую вам проверить мой предыдущий пост . Мы закончили этот пост, выполнив вход пользователя с вызовом AuthenticationManager.SignInAsync, в котором я заявил, что это вызовет промежуточное программное обеспечение cookie в нашем приложении.

ПО промежуточного слоя аутентификации файлов cookie

В этом посте мы рассмотрим часть этого кода в CookieAuthenticationMiddleware, чтобы увидеть, как он работает внутри, и лучше понять конвейер аутентификации в ASP.NET Core. В настоящее время мы рассматриваем только сторону безопасности аутентификации и просто пытаемся показать основную механику того, что происходит, вместо того, чтобы подробно рассматривать, как создаются файлы cookie, как они шифруются и т. д. Мы просто смотрим как промежуточное ПО и обработчики взаимодействуют с платформой ASP.NET Core.

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

app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
    AuthenticationScheme = "MyCookieMiddlewareInstance",
    LoginPath = new PathString("/Account/Unauthorized/"),
    AccessDeniedPath = new PathString("/Account/Forbidden/"),
    AutomaticAuthenticate = true,
    AutomaticChallenge = true
});

Как видите, мы установили ряд свойств CookieAuthenticationOptionsпри настройке нашего промежуточного программного обеспечения, к большинству из которых мы вернемся позже.

Так что же на самом деле делает промежуточное ПО для файлов cookie? Что ж, просматривая код , на самом деле он на удивление небольшой — он устанавливает некоторые параметры по умолчанию и является производным от базового класса AuthenticationMiddleware<T>. Этот класс просто требует, чтобы вы возвращали AuthenticationHandler<T>из перегруженного метода CreateHandler(). Именно в этом обработчике происходит все волшебство. Мы вернемся к самому промежуточному ПО позже, а пока сосредоточимся на обработчике.

AuthenticateResult и AuthenticationTicket

Прежде чем мы перейдем к содержательным вещам, есть пара вспомогательных классов, которые мы будем использовать в обработчике аутентификации, которые мы должны понимать: AuthenticateResultи AuthenticationTicket, описанные ниже:

public class AuthenticationTicket
{
    public string AuthenticationScheme { get; }
    public ClaimsPrincipal Principal{ get; }
    public AuthenticationProperties Properties { get; }
}

AuthenticationTicket— это простой класс, который возвращается после успешной аутентификации. Он содержит аутентифицированный ClaimsPrinciple, AuthenticationSchemeуказывающий, какое промежуточное ПО использовалось для аутентификации запроса, и AuthenticationPropertiesобъект, содержащий необязательные дополнительные значения состояния для сеанса аутентификации.

public class AuthenticateResult
{
    public bool Succeeded
    {
        get
        {
            return Ticket != null;
        }
    }
    public AuthenticationTicket Ticket { get; }
    public Exception Failure { get; }
    public bool Skipped { get; }
    
    public static AuthenticateResult Success(AuthenticationTicket ticket)
    {
        return new AuthenticateResult() { Ticket = ticket };
    }

    public static AuthenticateResult Skip()
    {
        return new AuthenticateResult() { Skipped = true };
    }

    public static AuthenticateResult Fail(Exception failure)
    {
        return new AuthenticateResult() { Failure = failure };
    }
}

содержит результат попытки аутентификации и AuthenticateResultсоздается путем вызова одного из статических методов Successили . Если аутентификация прошла успешно, то должно быть предоставлено успешное.SkipFailAuthenticationTicket

The CookieAuthenticationHandler

Именно CookieAuthenticationHandlerздесь фактически выполняется вся работа по аутентификации. Он является производным от AuthenticationHandlerбазового класса, поэтому в принципе требуется реализация только одного метода HandleAuthenticateAsync():

protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();

Этот метод отвечает за фактическую аутентификацию данного запроса, т. е. определяет, содержит ли данный запрос идентификатор ожидаемого типа, и если да, возвращает объект, AuthenticateResultсодержащий аутентифицированный ClaimsPrinciple. Как и следовало ожидать, CookieAuthenticationHandlerреализация зависит от ряда других методов, но мы вскоре рассмотрим каждый из них:

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
    var result = await EnsureCookieTicket();
    if (!result.Succeeded)
    {
        return result;
    }

    var context = new CookieValidatePrincipalContext(Context, result.Ticket, Options);
    await Options.Events.ValidatePrincipal(context);

    if (context.Principal == null)
    {
        return AuthenticateResult.Fail("No principal.");
    }

    if (context.ShouldRenew)
    {
        RequestRefresh(result.Ticket);
    }

    return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme));
}

Итак, прежде всего, вызывается обработчик, EnsureCookieTicket()который пытается создать AuthenticateResultфайл cookie из файла HttpContext. Здесь могут произойти три вещи, в зависимости от состояния файла cookie:

  1. Если cookie не существует, т. е. пользователь еще не вошел в систему, метод вернет AuthenticateResult.Skip(), указывая на этот статус.
  2. Если файл cookie существует и действителен, он возвращает десериализованный файл с AuthenticationTicketиспользованием AuthenticateResult.Success(ticket).
  3. Если файл cookie не может быть расшифрован (например, он поврежден или был подделан), если срок его действия истек или если используется состояние сеанса, а соответствующий сеанс не может быть найден, возвращается значение AuthenticateResult.Fail().

На этом этапе, если у нас нет действительного AuthenticationTicket, метод просто выручает. В противном случае мы теоретически счастливы, что запрос аутентифицирован. Однако на данный момент мы буквально только что поняли слово зашифрованного файла cookie. Возможно, что-то изменилось в серверной части вашего приложения с момента создания файла cookie — например, пользователь мог быть удален! Чтобы справиться с этим, CookieHandler вызывает ValidatePrincipal, который должен установить значение ClaimsPrincipal, nullесли он больше не действителен. Если вы используете CookieAuthenticationMiddlewareв своих собственных приложениях и не используете ASP.NET Core Identity, вам следует ознакомиться с документацией по обработке внутренних изменений во время проверки подлинности.

Вход и выход

Для простейшей аутентификации HandleAuthenticateAsyncтребуется только реализация. Однако на самом деле вам нужно будет переопределить другие методы AuthenticationHandler, чтобы иметь полезное поведение. Требуется CookieAuthenticationHandlerбольше поведения, чем просто этот метод — это HandleAuthenticateAsyncозначает, что мы можем читать и десериализовать и проверять подлинность билета в ClaimsPrinciple, но нам также нужна возможность установить файл cookie, когда пользователь входит в систему, и удалить файл cookie, когда пользователь выходит.

Метод HandleSignInAsync(SignInContext signin)создает новый AuthenticationTicket, шифрует его и записывает файл cookie в ответ. Он вызывается внутри как часть вызова SignInAsync(), который, в свою очередь, вызывается AuthenticationManager.SignInAsync(). Я не буду подробно рассматривать этот аспект в этом посте, но это то, AuthenticationManagerчто вы обычно вызываете из своего AccountControllerпосле того, как пользователь успешно вошел в систему. Как показано в моем предыдущем посте , вы должны создать ClaimsPrincipalс соответствующими утверждениями и передать это in to AuthenticationManager, который в конечном итоге достигнет CookieAuthenticationMiddlewareи позволит вам установить файл cookie. Наконец, если пользователь в данный момент находится на странице входа, он перенаправляет пользователя на обратный URL-адрес.

На другом конце процесса HandleSignOutAsyncудаляется файл cookie аутентификации из контекста, и, если пользователь находится на странице выхода, перенаправляет пользователя на обратный URL-адрес.

Несанкционированное vs Запрещенное

Последние два метода AuthenticationHandlerпереопределяются в CookieAuthenticationHandlerслучае, если аутентификация или авторизация не удалась. Эти два случая различны, но их легко спутать.

Пользователь не авторизован , если он еще не вошел в систему. Это соответствует 401, когда речь идет о HTTP-запросах. Пользователю запрещено , если он уже выполнил вход, но используемое им удостоверение не имеет разрешения на просмотр запрошенного ресурса, что соответствует ошибке 403 в HTTP.

Реализации по умолчанию HandleUnauthorizedAsyncи HandleForbiddenAsync в базе AuthenticationHandlerочень просты и выглядят так (для запрещенного случая):

protected virtual Task<bool> HandleForbiddenAsync(ChallengeContext context)
{
    Response.StatusCode = 403;
    return Task.FromResult(true);
}

Как видите, они просто устанавливают код состояния ответа и не останавливаются на этом. Несмотря на то, что это абсолютно правильно с точки зрения HTTP и безопасности, если оставить методы без изменений, это может плохо сказаться на пользователях, поскольку они просто увидят пустой экран:

Вместо этого CookieAuthenticationHandlerпереопределяет эти методы и перенаправляет пользователей на другую страницу. При неавторизованном ответе пользователь автоматически перенаправляется на тот, LoginPathкоторый мы указали при настройке промежуточного ПО. Затем пользователь может войти в систему и продолжить с того места, где он остановился.

Точно так же для ответа Forbidden пользователь перенаправляется на путь, указанный AccessDeniedPathпри добавлении промежуточного программного обеспечения в наш конвейер. В этом случае мы не перенаправляем на путь входа, поскольку пользователь уже прошел проверку подлинности, у него просто нет правильных утверждений или разрешений для просмотра запрошенного ресурса.

Настройка CookieHandler с помощьюCookieHandlerOptions

Мы уже рассмотрели пару свойств, CookieAuthenticationOptionsкоторые мы передали при создании промежуточного ПО , а именно LoginPathи AccessDeniedPath, но стоит также рассмотреть некоторые другие общие свойства.

Прежде всего это AuthenticationScheme. В предыдущем посте об аутентификации мы говорили, что при создании аутентифицированного ClaimsIdentityвы должны предоставить файл AuthenticationScheme. Предоставленное AuthenticationSchemeпри настройке промежуточного ПО передается в поле ClaimsIdentityпри его создании, а также в ряд других полей. Это становится особенно важным, когда у вас есть несколько промежуточных программ для аутентификации и авторизации (о которых я расскажу позже).

Затем свойство up AutomaticAuthenticate, но это требует от нас немного отступить, чтобы подумать о том, как работает промежуточное программное обеспечение аутентификации. Я предполагаю, что вы понимаете промежуточное ПО ASP.NET Core в целом, если нет, то, вероятно, стоит сначала прочитать об этом !

Middleware AuthenticationHandler

Обычно CookieAuthenticationMiddlewareнастраивается для запуска относительно рано в конвейере. Абстрактный базовый класс AuthentictionMiddleware<T>, от которого он происходит, имеет простой Invokeметод, который просто создает новый обработчик соответствующего типа, инициализирует его, запускает оставшееся промежуточное ПО в конвейере, а затем удаляет обработчик:

 public abstract class AuthenticationMiddleware<TOptions> 
    where TOptions : AuthenticationOptions, new()
{
    private readonly RequestDelegate _next;

    public string AuthenticationScheme { get; set; }
    public TOptions Options { get; set; }
    public ILogger Logger { get; set; }
    public UrlEncoder UrlEncoder { get; set; }

    public async Task Invoke(HttpContext context)
    {
        var handler = CreateHandler();
        await handler.InitializeAsync(Options, context, Logger, UrlEncoder);
        try
        {
            if (!await handler.HandleRequestAsync())
            {
                await _next(context);
            }
        }
        finally
        {
            try
            {
                await handler.TeardownAsync();
            }
            catch (Exception)
            {
                // Don't mask the original exception, if any
            }
        }
    }

    protected abstract AuthenticationHandler<TOptions> CreateHandler();
}

В рамках вызова InitializeAsyncобработчик проверяет, AutomaticAuthenticateистинно ли значение. Если это так, то обработчик немедленно запустит метод HandleAuthenticateAsync, поэтому все последующие middleware в pipeline увидят аутентифицированный ClaimsPrincipal. Напротив, если вы не установите AutomaticAuthenticateзначение true, тогда аутентификация будет происходить только в момент, когда требуется авторизация, например, когда вы нажимаете [Authorize]атрибут или что-то подобное.

Точно так же во время обратного пути через конвейер промежуточного программного обеспечения, если оно AutomaticChallengeравно true и код ответа равен 401, обработчик вызовет HandleUnauthorizedAsync. В случае CookieAuthenticationHandler, как обсуждалось, это автоматически перенаправит вас на указанную страницу входа.

Ключевым моментом здесь является то, что когда Automaticсвойства установлены, промежуточное ПО проверки подлинности всегда работает в настроенном месте конвейера. В противном случае обработчики запускаются только в ответ на прямую аутентификацию или запрос запроса. Если у вас возникли проблемы, когда вы возвращаете ошибку 401 с контроллера, и вас не перенаправляют на страницу входа, проверьте значение AutomaticChallengeи убедитесь, что оно верно.

В тех случаях, когда у вас есть только один компонент промежуточного ПО для аутентификации, имеет смысл установить для обоих значений значение true. Ситуация усложняется, если у вас есть несколько обработчиков аутентификации. В этом случае, как описано в документации , вы должны установить AutomaticAuthenticateзначение false. Я расскажу об особенностях использования нескольких обработчиков аутентификации в следующем посте, но документы дают хорошую отправную точку.

Резюме

В этом посте мы использовали в CookieAuthenticationMiddlewareкачестве примера того, как реализовать файл AuthenticationMiddleware. Мы показали некоторые методы, которые необходимо обрабатывать для реализации AuthenticationHandler, методы, вызываемые для входа и выхода пользователя, и то, как обрабатываются неавторизованные и запрещенные запросы.

Наконец, мы показали некоторые общие параметры, доступные при настройке CookieAuthenticationOptions, и эффекты, которые они имеют.

В последующих сообщениях я расскажу, как настроить ваше приложение для использования нескольких обработчиков проверки подлинности, как работает авторизация и различные способы ее использования, а также как ASP.NET Core Identity объединяет все эти аспекты, чтобы выполнить всю тяжелую работу за вас.