Изучение ПО промежуточного слоя для проверки подлинности файлов cookie в ASP.NET Core
Это вторая статья из серии, посвященной аутентификации и авторизации в 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:
- Если cookie не существует, т. е. пользователь еще не вошел в систему, метод вернет
AuthenticateResult.Skip()
, указывая на этот статус. - Если файл cookie существует и действителен, он возвращает десериализованный файл с
AuthenticationTicket
использованиемAuthenticateResult.Success(ticket)
. - Если файл 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 объединяет все эти аспекты, чтобы выполнить всю тяжелую работу за вас.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.