Для наиболее распространенного сценария — JavaScript-код в веб-странице обращается к сервису Web API на том же сайте — обсуждать защиту ASP.NET Web API, в общем-то, излишне. При условии, что вы аутентифицируете своих пользователей и авторизуете доступ к Web Forms/Views, содержащим JavaScript, который работает с вашими сервисами, можно считать, что вся необходимая защита сервисов у вас есть. Это результат того, что ASP.NET посылает файлы cookie и информацию об аутентификации, используемые для проверки запросов страниц, как часть любого JavaScript-запроса на клиентской стороне, адресованного методам вашего сервиса. Однако имеется одно важное исключение: ASP.NET не защищает вас автоматически от атак с подделкой кросс-сайтовых запросов (Cross-Site Request Forgery, CSRF/XSRF) (подробнее об этом — позже).
Помимо CSRF, существует два сценария, в которых действительно имеет смысл обсуждать защиту сервисов Web API. Первый сценарий — сервис используется клиентом, отличным от страницы того сайта, где находятся ваши ApiController. Эти клиенты не проверяются средствами аутентификации на основе форм (Forms Authentication) и не получают файлы cookie и маркеры, используемые ASP.NET для управления доступом к вашим сервисам.
Второй сценарий — вам нужно добавить дополнительную авторизацию доступа к вашим сервисам, выходящую за рамки той, которая предоставляется средствами защиты ASP.NET. По умолчанию ASP.NET обеспечивает авторизацию по ASP.NET-идентификации, назначенной запросу в ходе аутентификации. Вам может понадобиться расширить эту идентификацию для авторизации доступа на основе чего-то другого — не имени или роли.
В Web API предлагается несколько вариантов решений для обоих сценариев. По сути, хотя я буду рассматривать защиту в контексте приема запросов Web API, следует учитывать, что Web API основан на том же фундаменте ASP.NET, что и Web Forms/MVC, а значит, средства, о которых я буду рассказывать в этой статье, должны быть знакомы любому, кому известно внутреннее устройство защиты в Web Forms или MVC.
Один подвох: Web API предоставляет вам несколько вариантов аутентификации и авторизации, но защита начинается либо с IIS, либо с хоста, который вы создаете при наличии резидентной среды хостинга (self hosting). Если вы, например, хотите гарантировать приватность коммуникаций между сервисом Web API и клиентом, то должны, как минимум, включить SSL. Однако это обязанность не столько разработчика, сколько администратора сайта. В этой статье я проигнорирую все, что относится к хосту, и сосредоточусь на том, что может и должен сделать разработчик для защиты сервиса Web API (средства, рассматриваемые здесь, будут работать независимо от того, включен SSL или нет).
Предотвращение атак с подделкой кросс-сайтовых запросов
Когда пользователь обращается к сайту ASP.NET, применяющему аутентификацию на основе форм, ASP.NET генерирует cookie, который указывает, что данный пользователь аутентифицирован. Браузер будет посылать этот cookie при каждом последующем запросе к сайту независимо от того, откуда исходит этот запрос. Это делает ваш сайт уязвимым к атакам CSRF, как и любая другая схема аутентификации, где браузер автоматически отправляет полученную ранее аутентификационную информацию. Если пользователь после такой аутентификации на вашем сайте посетит какой-нибудь злонамеренный сайт, тогда этот сайт сможет посылать запросы вашему сервису, цепляясь за cookie аутентификации, ранее полученный браузером.
Чтобы предотвратить атаки CSRF, вам потребуется генерировать на сервере маркеры, стойкие к подделке, и встраивать их в страницу, которая будет использоваться при вызовах с клиентской стороны. Microsoft предоставляет класс AntiForgery с методом GetToken. Этот метод генерирует маркеры, специфичные для пользователя, который выдал запрос (конечно, таковым может быть анонимный пользователь). Его код создает два маркера и помещает их в ASP.NET MVC ViewBag, откуда их можно использовать в View:
[Authorize(Roles="manager")]
public ActionResult Index()
{
string cookieToken;
string formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
ViewBag.cookieToken = cookieToken;
ViewBag.formToken = formToken;
return View("Index");
}
Любые JavaScript-вызовы сервера должны возвращать эти маркеры как часть запроса (у CSRF-сайта нет этих маркеров, и он не сможет вернуть их). Этот код (он находится в View) динамически генерирует JavaScript-вызов, который добавляет маркеры в заголовки запроса:
$.ajax("http://phvis.com/api/Customers",{
type: "get",
contentType: "application/json",
headers: {
'formToken': '@ViewBag.formToken',
'cookieToken': '@ViewBag.cookieToken' }});
Чуть более сложное решение позволит вам использовать незаметный JavaScript-код за счет встраивания маркеров в скрытые поля в View. Первый шаг в этом процессе — добавление маркеров в словарь ViewData:
ViewData["cookieToken"] = cookieToken;
ViewData["formToken"] = formToken;
Теперь в View можно встроить данные в скрытые поля. Для генерации подходящего тега нужно просто передать методу Hidden в HtmlHelper значение ключа в ViewDate:
@Html.Hidden("formToken")
Конечный тег input будет использовать ключ ViewData для атрибутов name и id и помещать данные, извлеченные из словаря ViewData, в атрибут value. Тег input, сгенерированный на основе предыдущего кода, выглядел бы так:
<input id="formToken" name="formToken" type="hidden" value="...token..." />
Затем ваш JavaScript-код (который хранится в файле, отдельном от View) может получать значения из тегов input и использовать их в вызовах ajax:
$.ajax("http://localhost:49226/api/Customers", {
type: "get",
contentType: "application/json",
headers: {
'formToken': $("#formToken").val(),
'cookieToken': $("#cookieToken").val()}});
Вы можете добиться тех же целей на сайте с ASP.NET Web Forms, используя метод RegisterClientScriptBlock объекта ClientScriptManager (извлекается из свойства ClientScript объекта Page), чтобы вставить JavaScript-код с помощью встраиваемых маркеров:
string CodeString = "function CallService(){" +
"$.ajax('http://phvis.com/api/Customers',{" +
"type: 'get', contentType: 'application/json'," +
"headers: {'formToken': '" & formToken & "',” +
"'cookieToken': '" & cookieToken & "'}});}"
this.ClientScript.RegisterClientScriptBlock(
typeOf(this), "loadCustid", CodeString, true);
Наконец, вам понадобится проверять маркеры на сервере при их возврате JavaScript-вызовом. Разработчики, обновившие Visual Studio 2012 пакетом ASP.NET and Web Tools 2012.2, обнаружат, что новый шаблон SinglePage Application (SPA) включает фильтр ValidateHttpAntiForgeryToken, который можно использовать в методах Web API. В отсутствие этого фильтра вы должны получать маркеры и передавать их методу Validate класса AntiForgery (этот метод будет генерировать исключение, если маркер недопустим или был создан для другого пользователя). Код на рис. 1, используемый в методе сервиса Web API, получает маркеры из заголовков и проверяет их.
Рис. 1. Проверка CSRF-маркеров в методе сервиса
public HttpResponseMessage Get(){
if (Request.Headers.TryGetValues("cookieToken", out tokens))
{
string cookieToken = tokens.First();
Request.Headers.TryGetValues("formToken", out tokens);
string formToken = tokens.First();
AntiForgery.Validate(cookieToken, formToken);
}
else
{
HttpResponseMessage hrm =
new HttpResponseMessage(HttpStatusCode.Unauthorized);
hrm.ReasonPhrase = "CSRF tokens not found";
return hrm;
}
// ...код, обрабатывающий запрос...
Применение ValidateHttpAntiForgeryToken (вместо кода в методе) переносит эту обработку на более ранние этапы в цикле (например, до связывания модели), что, безусловно, хорошо.
Почему нет ни слова об OAuth?
В этой статье OAuth игнорируется намеренно. Спецификация OAuth определяет, как клиент может получать маркеры от стороннего сервера для передачи сервису, который в свою очередь проверяет их с помощью стороннего сервера. Описание того, как обращаться к провайдеру маркеров OAuth из клиента или сервиса, выходит за рамки данной статьи.
Кроме того, начальная версия OAuth не слишком хорошо подходит для Web API. Очевидно, что одна из основных причин появления Web API — использование «облегченных» запросов на основе REST и JSON. Эта цель делает первую версию OAuth непривлекательным вариантом для сервисов Web API. Маркеры, формируемые первой версией OAuth, слишком объемистые и основаны на XML. К счастью, в OAuth 2.0 ввели спецификацию для облегченных маркеров JSON, более компактных по сравнению с маркерами из прошлых версий. Методики, рассматриваемые в этой статье, можно было бы использовать и для обработки любых OAuth-маркеров, посылаемых сервису.
Базовая аутентификация
Первая из двух основных задач в защите сервиса Web API — аутентификация (вторая задача — авторизация). Я исхожу из того, что другие задачи, например защита конфиденциальных данных, обрабатываются на хосте.
В идеале, аутентификация и авторизация будут выполняться в конвейере Web API как можно раньше, что избежать траты вычислительных ресурсов на запрос, который вы отклоните. Решения по аутентификации, обсуждаемые в этой статье, используются на самых ранних стадиях конвейера — практически в тот момент, когда поступает запрос. Эти методики также позволяют интегрировать аутентификацию с любыми списками пользователей, которые вы уже поддерживаете. Рассматриваемые методы авторизации могут применяться во многих местах конвейера (включая такие поздние стадии, как в самом методе сервиса) и работать совместно с аутентификацией, чтобы авторизовать запросы на основе некоторых других критериев, отличных от имени или роли пользователя.
Вы можете поддерживать клиенты, не проходившие аутентификацию на основе форм, предоставив свой метод аутентификации в пользовательском HTTP-модуле (я по-прежнему исхожу из того, что вы осуществляете аутентификацию не по учетным записям в Windows, а по собственному списку пользователей). Применение HTTP-модуля дает два основных преимущества: модуль участвует в HTTP-протоколировании и аудите, и его можно вызывать на самых ранних стадиях конвейера. Хотя эти возможности важны, за них приходится платить: модули являются глобальными и применяются ко всем запросам сайта, а не только к запросам Web API; кроме того, чтобы использовать модули аутентификации, вы должны разместить сервис в IIS. Позднее в этой статье я опишу применение обработчиков делегирования, вызываемых только для запросов Web API и независимых от хоста.
В этом примере использования HTTP-модуля я исхожу из того, что IIS использует базовую аутентификацию и удостоверения, применяемые для аутентификации пользователей, содержат имя и пароль пользователя и посылаются клиентом (в этой статье я буду игнорировать сертификацию в Windows, но рассмотрю применение клиентских сертификатов). Кроме того, я предполагаю, что сервис Web API защищается с использованием атрибута Authorize, например:
public class CustomersController : ApiController
{
[Authorize(Users="Peter")]
public Customer Get()
{
Первый шаг в создании собственного HTTP-модуля авторизации — добавление в проект сервиса класса, который реализует интерфейсы IHttpModule и IDisposable. В методе Init этого класса вы должны подключать два события от объекта HttpApplication, передаваемые методу. Метод, подключаемый к событию AuthenticateRequest, будет вызываться при получении удостоверений клиента. Но вы также должны подключить метод EndRequest, чтобы генерировать сообщение, заставляющее клиент посылать вам свои удостоверения. Кроме того, мне необходим метод Dispose, но вам не нужно ничего помещать в него для поддержки кода, используемого здесь:
public class PHVHttpAuthentication : IHttpModule, IDisposable
{
public void Init(HttpApplication context)
{
context.AuthenticateRequest += AuthenticateRequests;
context.EndRequest += TriggerCredentials;
}
public void Dispose()
{
}
HttpClient будет посылать удостоверения в ответ на заголовок WWW-Authenticate, включаемые вами в HTTP-ответ. Вы должны включать этот заголовок, когда запрос приводит к генерации кода 401 (ASP.NET будет генерировать код ответа 401, когда доступ клиента к защищенному сервису отклоняется). Заголовок должен содержать указание на то, какой метод аутентификации используется, и область, в которой будет применяться аутентификация (областью может быть любая произвольная строка, и она используется как флаг при просмотре различных областей на сервере). Код, посылающий это сообщение, помещается в метод, подключаемый к событию EndRequest. В следующем примере генерируется сообщение, которое указывает, что используется базовая аутентификация в области PHVIS:
private static void TriggerCredentials(object sender, EventArgs e)
{
HttpResponse resp = HttpContext.Current.Response;
if (resp.StatusCode == 401)
{
resp.Headers.Add("WWW-Authenticate", @"Basic realm='PHVIS'");
}
}
В методе, где вы подключились к методу AuthenticateRequest, вы должны получить заголовки Authorization, которые клиент посылает в результате приема вашего сообщения 401/WWW-Authenticate:
private static void AuthenticateRequests(object sender,
EventArgs e)
{
string authHeader =
HttpContext.Current. Request.Headers["Authorization"];
if (authHeader != null)
{
Определив, что клиент передал элементы заголовка Authorization (мы по-прежнему предполагаем, что сайт использует базовую аутентификацию), вы должны разобрать данные, содержащие имя и пароль пользователя. Имя и пароль кодируются по основанию Base64 и разделяются двоеточием. Следующий код извлекает имя и пароль в строковый массив:
AuthenticationHeaderValue authHeaderVal =
AuthenticationHeaderValue.Parse(authHeader);
if (authHeaderVal.Parameter != null)
{
byte[] unencoded = Convert.FromBase64String(
authHeaderVal.Parameter);
string userpw =
Encoding.GetEncoding("iso-8859-1").GetString(unencoded);
string[] creds = userpw.Split(':');
Как демонстрирует этот код, имя и пароль передаются открытым текстом. Поэтому, если вы не включили SSL, ваши имя и пароль можно легко перехватить (этот код работает, даже если SSL включен).
Следующий шаг — проверка имени и пароля с помощью любого подходящего вам механизма. Независимо от того, как именно вы проверяете запрос (мой код в примере ниже, конечно же, слишком прост), на последнем этапе создается идентификация для пользователя, которая будет задействована в процессах авторизации на более поздних стадиях конвейера ASP.NET.
Чтобы передать информацию идентификации по конвейеру, вы создаете объект GenericIdentity с именем идентификации, которую вы хотите назначить пользователю (в коже ниже я предполагаю, что это имя пользователя, переданное в заголовке). Создав объект GenericIdentity, вы должны поместить его в свойство CurrentPrincipal класса Thread. Кроме того, ASP.NET поддерживает второй контекст защиты в объекте HttpContext, и, если вашим хостом является IIS, вы тоже должны поддерживать его, сохраняя объект GenericIdentity в свойстве User свойства Current объекта HttpContext:
if (creds[0] == "Peter" && creds[1] == "pw")
{
GenericIdentity gi = new GenericIdentity(creds[0]);
Thread.CurrentPrincipal = new GenericPrincipal(gi, null);
HttpContext.Current.User = Thread.CurrentPrincipal;
}
Если вам нужно поддерживать защиту на основе ролей, тогда вы должны передавать массив имен ролей как второй параметр конструктора GenericPrincipal. В этом примере каждому пользователю назначаются роли manager и admin:
string[] roles = "manager,admin".Split(',');
Thread.CurrentPrincipal = new GenericPrincipal(gi, roles);
Для интеграции HTTP-модуля в конвейер обработки на сайте используйте в файле web.config своего проекта тег add в элементе modules. Атрибуту type тега add должна быть присвоена строка, состоящая из полного имени класса, за которым следует имя сборки вашего модуля:
<modules>
<add name="myCustomerAuth"
type="SecureWebAPI.PHVHttpAuthentication, SecureWebAPI"/>
</modules>
Созданный вами объект GenericIdentity будет работать с ASP.NET-атрибутом Authorize. Кроме того, вы можете обращаться к GenericIdentity из метода сервиса для выполнения операций, связанных с авторизацией. Например, вы могли бы предоставлять разные сервисы для зарегистрированных и анонимных пользователей, определяя, аутентифицирован ли данный пользователь. Для этого проверяйте свойство IsAuthenticated (IsAuthenticated возвращает false для анонимного пользователя):
if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
{
Вы можете получить объект GenericIdentity более простым способом через свойство User:
if (User.Identity.IsAuthenticated)
{
Создание совместимого клиента
Чтобы работать с сервисами, защищаемыми этим модулем, клиент, отличный от JavaScript, должен предоставить правильные имя и пароль пользователя. Для передачи этих удостоверений через .NET-объект HttpClient вы сначала создаете объект HttpClientHandler и указываете в его свойстве Credentials объект NetworkCredential, содержащий имя и пароль пользователя (или устанавливаете свойство UseDefaultCredentials объекта HttpClientHandler в true, чтобы задействовать Windows-удостоверения текущего пользователя). Затем вы создаете объект HttpClient, передавая объект HttpClientHandler:
HttpClientHandler hch = new HttpClientHandler();
hch.Credentials = new NetworkCredential ("Peter", "pw");
HttpClient hc = new HttpClient(hch);
Закончив конфигурирование, вы можете отправить запрос сервису. HttpClient не предоставит удостоверения, пока доступ к сервису не будет отклонен и не будет получено сообщение WWW-Authenticate. Если удостоверения, переданные HttpClient, не принимаются, сервис возвращает HttpResponseMessage со StatusCode его Result, установленным в «unauthenticated».
Следующий код вызывает сервис, используя метод GetAsync, проверяет, получен ли результат, и, если результата нет, отображает код состояния, возвращенный сервисом:
hc.GetAsync("http://phvis.com/api/Customers").ContinueWith(r =>
{
HttpResponseMessage hrm = r.Result;
if (hrm.IsSuccessStatusCode)
{
// ...обработка ответа...
}
else
{
MessageBox.Show(hrm.StatusCode.ToString());
}
});
Если предположить, что вы обходите ASP.NET-процесс входа для клиентов, отличных от JavaScript, как это сделал я, то никакие cookie аутентификации не создаются и каждый запрос от клиента проверяется индивидуально. Чтобы уменьшить издержки постоянных проверок удостоверений клиента, подумайте о кешировании удостоверений (и применении собственного метода Dispose для удаления удостоверений из кеша).
Работа с клиентскими сертификатами
В HTTP-модуле вы получаете объект клиентского сертификата (и проверяете, что он допустим и вообще имеется) с помощью такого кода:
System.Web.HttpClientCertificate cert =
HttpContext.Current.Request.ClientCertificate;
if (cert!= null && cert.IsPresent && cert.IsValid)
{
Далее по конвейеру обработки, например в методе сервиса, вы получаете объект сертификата (проверив, что он имеется) с помощью следующего кода:
X509Certificate2 cert = Request.GetClientCertificate();
if (cert!= null)
{
Если сертификат есть и он допустим, вы можете дополнительно проверить специфические значения в свойствах сертификата (скажем, субъект или эмитент).
Чтобы передать сертификаты с HttpClient, первым делом создайте объект WebRequestHandler вместо HttpClientHandler (WebRequestHandler поддерживает больше конфигурационных параметров, чем HttpClientHandler):
WebRequestHandler wrh = new WebRequestHandler();
Вы можете сделать так, чтобы HttpClient автоматически вел поиск в хранилищах сертификатов клиента, установив свойство ClientCertificateOptions объекта WebRequestHandler в Automatic (значение и перечисления ClientCertificateOption):
wrh.ClientCertificateOptions = ClientCertificateOption.Manual;
Однако по умолчанию клиент должен явно присоединять сертификаты к WebRequestHandler из кода. Вы можете получить сертификат от одного из хранилищ сертификатов клиента так, как это делается в следующем примере, где сертификат извлекается из хранилища CurrentUser по имени эмитента:
X509Store certStore;
X509Certificate x509cert;
certStore = new X509Store(StoreName.My,
StoreLocation.CurrentUser);
certStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
x509cert = certStore.Certificates.Find(
X509FindType.FindByIssuerName, "PHVIS", true)[0];
store.Close();
Если пользователю был отправлен клиентский сертификат, который по какой-то причине не добавляется в хранилище сертификатов этого пользователя, то вы можете создать объект X509Certificate на основе файла сертификата:
x509cert = new X509Certificate2(@"C:\PHVIS.pfx");
Независимо от того, как создается X509Certificate, последние операции на клиентской стороне заключаются в добавлении сертификата к набору ClientCertificates объекта WebRequestHandler и последующем использовании сконфигурированного WebRequestHandler для создания HttpClient:
wrh.ClientCertificates.Add(x509cert);
hc = new HttpClient(wrh);
Авторизация в резидентной среде
Хотя использовать HttpModule в резидентной среде (self-hosted environment) нельзя, процесс защиты запросов на ранних стадиях конвейера обработки в такой среде все тот же: получаем удостоверения из запроса, используем эту информацию для аутентификации запроса и создаем идентификацию, передаваемую в свойство CurrentPrincipal текущего потока. Простейший механизм — создать средство проверки (validator) имени и пароля пользователя. Чтобы делать нечто большее, чем проверять комбинацию имени и пароля пользователя, можно создать делегирующий обработчик (delegating handler). Для начала рассмотрим интеграцию средства проверки имени и пароля пользователя.
Чтобы создать средство проверки (по-прежнему предполагается, что вы применяете базовую аутентификацию), вы должны создать класс, производный от UserNamePasswordValidator (вам потребуется добавить в проект ссылку на библиотеку System.IdentityModel). Единственный метод базового класса, который вам нужно переопределить, — Validate, который будет передавать имя и пароль пользователя, отправленные клиентом сервису. Как и раньше, проверив имя и пароль, вы должны создать объект GenericPrincipal и использовать его для задания свойства CurrentPrincipal класса Thread (поскольку вы не используете IIS в качестве хоста, вам не нужно устанавливать свойство User объекта HttpContext):
public class PHVValidator :
System.IdentityModel.Selectors.UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (userName == "Peter" && password == "pw")
{
GenericIdentity gi = new GenericIdentity(username, null);
Thread.CurrentPrincipal = gi;
}
Следующий код создает хост для контроллера Customers с конечной точкой http://phvis.com/MyServices и указывает новое средство проверки:
partial class PHVService : ServiceBase
{
private HttpSelfHostServer shs;
protected override void OnStart(string[] args)
{
HttpSelfHostConfiguration hcfg =
new HttpSelfHostConfiguration("http://phvis.com/MyServices");
hcfg.Routes.MapHttpRoute("CustomerServiceRoute",
"Customers", new { controller = "Customers" });
hcfg.UserNamePasswordValidator = new PHVValidator;
shs = new HttpSelfHostServer(hcfg);
shs.OpenAsync();
Обработчики сообщений
Чтобы делать нечто большее простой проверки имени и пароля пользователя, вы можете создать обработчик сообщений Web API. Такие обработчики имеют несколько преимуществ в сравнении с HTTP-модулем: 1) обработчики сообщений не привязаны к IIS, поэтому защита, применяемая в обработчике, будет работать с любым хостом, 2) обработчики сообщений используются только Web API, а значит, они обеспечивают простой способ выполнения авторизации (и назначения идентификаций) для ваших сервисов, применяющих процесс, который отличается от используемого страницами вашего веб-сайта, и 3) вы также можете назначать обработчики сообщений конкретным маршрутам (routes), чтобы ваш код защиты вызывался, только когда в этом есть реальная необходимость.
Первый шаг в создании обработчика сообщений — написание класса, производного от DelegatingHandler, переопределение его метода SendAysnc:
public class PHVAuthorizingMessageHandler: DelegatingHandler
{
protected override System.Threading.Tasks.Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken)
{
Внутри этого метода (я исхожу из того, что вы создаете индивидуальный обработчик для каждого маршрута) можно настроить свойство InnerHandler объекта DelegatingHandler так, чтобы этот обработчик можно было связать в один конвейер с другими обработчиками:
HttpConfiguration hcon = request.GetConfiguration();
InnerHandler = new HttpControllerDispatcher(hcon);
В этом примере я предполагаю, что корректный запрос должен иметь простой маркер в своей строке запроса (пару «имя-значения» в виде «authToken=xyx»). Если маркер отсутствует или ему не присвоено значение xyx, возвращается код состояния 403 (Forbidden).
Сначала я преобразовываю строку запроса в набор пар «имя-значение», вызывая метод GetQueryNameValuePairs объекта HttpRequestMessage, переданного методу. Затем с помощью LINQ извлекаю маркер (или null, если маркера нет). Если маркер отсутствует или недопустим, я создаю HttpResponseMessage с подходящим кодом состояния HTTP, обертываю его в объект TaskCompletionSource и возвращаю:
string usingRegion = (from kvp in request.GetQueryNameValuePairs()
where kvp.Key == "authToken"
select kvp.Value).FirstOrDefault();
if (usingRegion == null || usingRegion != "xyx")
{
HttpResponseMessage resp =
new HttpResponseMessage(HttpStatusCode.Forbidden);
TaskCompletionSource tsc =
new TaskCompletionSource<HttpResponseMessage>();
tsc.SetResult(resp);
return tsc.Task;
}
Если маркер присутствует и имеет правильное значение, я создаю объект GenericPrincipal и использую его для настройки свойства CurrentPrincipal класса Thread (чтобы этот обработчик сообщений мог работать и в IIS, я также задаю свойство User объекта HttpContext, если этот объект не равен null):
Thread.CurrentPrincipal = new GenericPrincipal(
Thread.CurrentPrincipal.Identity.Name, null);
if (HttpContext.Current != null)
{
HttpContext.Current.User = Thread.CurrentPrincipal;
}
После аутентификации запроса через маркер и присваивания идентификации обработчик сообщений вызывает метод базового класса для продолжения обработки:
return base.SendAsync(request, cancellationToken);
Если ваш обработчик сообщений должен использоваться в каждом контроллере, вы можете добавить его в конвейер обработки Web API, как и любой другой обработчик сообщений. Однако, чтобы ограничить ваш обработчик использованием лишь на определенных маршрутах, вы должны добавить его через метод MapHttpRoute. Сначала создайте экземпляр своего класса, а затем передайте его как пятый параметр в MapHttpRoute (приведенный ниже код требует выражения Imports/using для System.Web.Http):
routes.MapHttpRoute(
"ServiceDefault",
"api/Customers/{id}",
new { id = RouteParameter.Optional },
null,
new PHVAuthorizingMessageHandler());
Вместо того чтобы настраивать InnerHandler в DelegatingHandler, можно настроить это свойство на диспетчер по умолчанию при определении вашего маршрута:
routes.MapHttpRoute(
"ServiceDefault",
"api/{controller}/{id}",
new { id = RouteParameter.Optional },
null,
new PHVAuthorizingMessageHandler
{InnerHandler = new HttpControllerDispatcher(
GlobalConfiguration.Configuration)});
Теперь ваш InnerHandler вместо распределения среди нескольких DelegatingHandler управляется вами из единого места, где вы определяете свои маршруты.
Расширение участника системы безопасности
Если авторизации по имени и роли недостаточно, можно расширить процесс авторизации, создав собственный класс участника системы безопасности (principal); при этом вы должны реализовать интерфейс IPrincipal. Однако, чтобы задействовать преимущества своего класса, вам придется создать пользовательский атрибут авторизации или добавить код в методы вашего сервиса.
Например, если у вас есть набор сервисов, к которым могут обращаться пользователи только из определенного региона, вы могли бы создать простой класс участника системы безопасности, реализующий интерфейс IPrincipal и добавляющий свойство Region, как показано на рис. 2.
Рис. 2. Создание собственного участника системы безопасности с дополнительными свойствами
public class PHVPrincipal: IPrincipal
{
public PHVPrincipal(string Name, string Region)
{
this.Name = Name;
this.Region = Region;
}
public string Name { get; set; }
public string Region { get; set; }
public IIdentity Identity
{
get
{
return new GenericIdentity(this.Name);
}
set
{
this.Name = value.Name;
}
}
public bool IsInRole(string role)
{
return true;
}
Для использования преимуществ этого нового класса (который будет работать с любым хостом) достаточно создать его экземпляр, а затем с его помощью задать свойства CurrentPrincipal и User. Следующий код ищет значение в строке запроса, сопоставленное с именем «region». Получив это значение, код настраивает свойство Region участника системы безопасности и для этого передает данное значение конструктору класса:
string region = (from kvp in request.GetQueryNameValuePairs()
where kvp.Key == "region"
select kvp.Value).FirstOrDefault();
Thread.CurrentPrincipal = new PHVPrincipal(userName, region);
Если вы работаете с Microsoft .NET Framework 4.5, то вместо реализации интерфейса IPrincipal вы должны наследовать от нового класса ClaimsPrincipal. Этот класс поддерживает обработку на основе заявок и интеграцию с Windows Identity Foundation (WIF). Однако этот вопрос выходит за рамки моей статьи (на эту тему мы поговорим в будущей статье, которая будет посвящена защите на основе заявок).
Авторизация собственного участника системы безопасности
Располагая новым объектом участника системы безопасности, можно создать атрибут авторизации, использующий преимущества новых данных, которые содержатся в этом объекте. Сначала создайте класс, производный от System.Web.Http.AuthorizeAttribute, и переопределите его метод IsAuthorized (этот процесс отличается от того, который принят в ASP.NET MVC, где вы создаете новые атрибуты Authorization расширением System.Web.Http.Filters.AuthorizationFilterAttribute). Методу IsAuthorized передается HttpActionContext, свойства которого можно использовать в процессе авторизации. Однако в этом примере достаточно извлечь объект участника системы безопасности из свойства CurrentPrincipal класса Thread, привести его к собственному типу участника и проверить свойство Region. Если авторизация проходит успешно, код возвращает true. В ином случае вы должны помещать в свойство Response объекта ActionContext свой ответ перед возвратом false, как показано на рис. 3.
Рис. 3. Фильтрация собственного объекта участника системы безопасности
public class RegionAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
public string Region { get; set; }
protected override bool IsAuthorized(HttpActionContext actionContext)
{
PHVPrincipal phvPcp = Thread.CurrentPrincipal as PHVPrincipal;
if (phvPcp != null && phvPcp.Region == this.Region)
{
return true;
}
else
{
actionContext.Response =
new HttpResponseMessage(
System.Net.HttpStatusCode.Unauthorized)
{
ReasonPhrase = "Invalid region"
};
return false;
}
}
}
Ваш атрибут авторизации можно использовать точно так же, как ASP.NET-фильтр Authorize по умолчанию. Поскольку в этом фильтре есть свойство Region, ему должен быть присвоен регион, принимаемый методом сервиса:
[RegionAuthorize(Region = "East")]
public HttpResponseMessage Get()
{
В этом примере я предпочел наследование от AuthorizeAttribute, так как мой код авторизации использует только ресурсы процессора. Если бы моему коду требовался доступ к какому-нибудь сетевому ресурсу (или выполнение любого ввода-вывода), то лучше было бы выбрать реализацию интерфейса IAuthorizationFilter, поскольку он поддерживает асинхронные вызовы.
Как я сказал в начале статьи: в типичном сценарии Web API дополнительная авторизация не нужна ни для чего, кроме защиты от атак CSFR. Но когда у возникает потребность в расширении исходной системы безопасности, Web API предоставляет множество точек в конвейере обработки, где можно интегрировать любую необходимую защиту. А выбор — это всегда хорошо.