Как настроить ASP.NET Web API AuthorizeAttribute для необычных требований
Я наследую от System.Web.Http.AuthorizeAttribute, чтобы создать пользовательскую процедуру авторизации/проверки подлинности, чтобы удовлетворить некоторые необычные требования к веб-приложению, разработанному с использованием ASP.NET MVC 4. Это добавляет безопасность для веб-API, используемый для вызовов Ajax от веб-клиента. Требования:
- Пользователь должен войти в систему каждый раз, когда выполняет транзакцию для проверки
кто-то еще не подошел к рабочей станции, после того, как кто-то
вошел в систему и ушел.
- Роли не могут быть назначены методам веб-службы во время программы.
Они должны назначаться во время выполнения, чтобы администратор мог
настройте это. Эта информация хранится в системной базе данных.
Веб-клиент - это одностраничное приложение (SPA), поэтому типичная проверка подлинности форм не работает так хорошо, но я пытаюсь повторно использовать как часть ASP. Как я могу удовлетворить требованиям. Пользовательский AuthorizeAttribute отлично подходит для требования 2 по определению того, какие роли связаны с методом веб-службы. Я принимаю три параметра, имя приложения, имя ресурса и операцию, чтобы определить, какие роли связаны с методом.
public class DoThisController : ApiController
{
[Authorize(Application = "MyApp", Resource = "DoThis", Operation = "read")]
public string GetData()
{
return "We did this.";
}
}
Я переопределяю метод OnAuthorization, чтобы получить роли и аутентифицировать пользователя. Поскольку пользователь должен быть аутентифицирован для каждой транзакции, я сокращаю назад и вперед болтовню, выполняя аутентификацию и авторизацию на том же шаге. Я получаю учетные данные пользователей от веб-клиента, используя базовую аутентификацию, которая передает зашифрованные учетные данные в HTTP-заголовке. Поэтому мой метод OnAuthorization выглядит следующим образом:
public override void OnAuthorization(HttpActionContext actionContext)
{
string username;
string password;
if (GetUserNameAndPassword(actionContext, out username, out password))
{
if (Membership.ValidateUser(username, password))
{
FormsAuthentication.SetAuthCookie(username, false);
base.Roles = GetResourceOperationRoles();
}
else
{
FormsAuthentication.SignOut();
base.Roles = "";
}
}
else
{
FormsAuthentication.SignOut();
base.Roles = "";
}
base.OnAuthorization(actionContext);
}
GetUserNameAndPassword извлекает учетные данные из HTTP-заголовка. Затем я использую Membership.ValidateUser для проверки учетных данных. У меня есть пользовательский поставщик членства и поставщик роликов, подключенный к пользовательской базе данных. Если пользователь аутентифицирован, я получаю роли для ресурса и операции. Оттуда я использую базу OnAuthorization для завершения процесса авторизации. Здесь он разбивается.
Если пользователь аутентифицирован, я использую методы проверки подлинности стандартных форм для входа пользователя в систему (FormsAuthentication.SetAuthCookie), и если они не сработают, я выхожу из них (FormsAuthentication.SignOut). Но проблема заключается в том, что базовый класс OnAuthorization не имеет доступа к обновленному Принципиальному, поэтому для IsAuthenticated установлено правильное значение. Это всегда один шаг назад. И я предполагаю, что он использует некоторое кешированное значение, которое не обновляется до тех пор, пока не появится обратная связь с веб-клиентом.
Итак, все это приводит к моему конкретному вопросу, который есть, есть ли другой способ установить IsAuthenticated правильное значение для текущего Принципала без использования файлов cookie? Мне кажется, что куки файлы действительно не применяются в этом конкретном сценарии, где я должен аутентифицироваться каждый раз. Причина, по которой я знаю IsAuthenticated, не установлена в правильное значение. Я также переопределяю метод HandleUnauthorizedRequest:
protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
{
if (((System.Web.HttpContext.Current.User).Identity).IsAuthenticated)
{
filterContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
Это позволяет мне вернуть код состояния Forbidden для веб-клиента, если сбой произошел из-за авторизации вместо аутентификации, и он может ответить соответствующим образом.
Итак, каков правильный способ установить IsAuthenticated для текущего Принципа в этом сценарии?
Ответы
Ответ 1
Лучшее решение для моего сценария, похоже, полностью обходит базовую OnAuthorization. Поскольку я должен аутентифицироваться каждый раз, когда cookie и кеширование этого принципа не имеют большого значения. Итак, вот решение, которое я придумал:
public override void OnAuthorization(HttpActionContext actionContext)
{
string username;
string password;
if (GetUserNameAndPassword(actionContext, out username, out password))
{
if (Membership.ValidateUser(username, password))
{
if (!isUserAuthorized(username))
actionContext.Response =
new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
}
else
{
actionContext.Response =
new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
}
else
{
actionContext.Response =
new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
}
}
Я разработал свой собственный метод для проверки ролей под названием isUserAuthorized, и я больше не использую базовую OnAuthorization, так как он проверяет текущий Принцип, чтобы убедиться, что он isAuthenticated. IsAuthenticated только позволяет получать, поэтому я не уверен, как еще это установить, и мне, похоже, не нужен текущий Принцип. Протестировал это, и он отлично работает.
По-прежнему интересно, есть ли у кого-то лучшее решение или можно увидеть какие-либо проблемы с этим.
Ответ 2
Чтобы добавить к уже принятому ответу: проверяя текущий исходный код (aspnetwebstack.codeplex.com) на System.Web.Http.AuthorizeAttribute
, похоже, что документация устарела. Base OnAuthorization()
просто вызывает/проверяет private static SkipAuthorization()
(который просто проверяет, используется ли AllowAnonymousAttribute
в контексте, чтобы обойти остальную проверку проверки подлинности). Затем, если не пропустить, OnAuthorization()
вызывает общедоступный IsAuthorized()
, и если этот вызов терпит неудачу, он вызывает защищенный виртуальный HandleUnauthorizedRequest()
. И это все, что он делает...
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext == null)
{
throw Error.ArgumentNull("actionContext");
}
if (SkipAuthorization(actionContext))
{
return;
}
if (!IsAuthorized(actionContext))
{
HandleUnauthorizedRequest(actionContext);
}
}
Заглянув внутрь IsAuthorized()
, убедитесь, что принцип проверен на роли и пользователей. Таким образом, переопределение IsAuthorized()
с тем, что у вас было выше, а не OnAuthorization()
, - это путь. Опять же, вам все равно придется переопределить либо OnAuthorization()
, либо HandleUnauthorizedRequest()
в любом случае, чтобы решить, когда вернуть ответ 401 против 403.
Ответ 3
Чтобы добавить к абсолютно правильному ответу Кевина, я хотел бы сказать, что я могу немного изменить его, чтобы использовать существующий путь .NET framework для объекта ответа, чтобы обеспечить обратный код в рамках (или других пользователей) не отрицательно сказывается на какой-то странной идиосинкразии, которая не может быть предсказана.
В частности, это означает использование этого кода:
actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, REQUEST_NOT_AUTHORIZED);
а не:
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
Где REQUEST_NOT_AUTHORIZED
:
private const string REQUEST_NOT_AUTHORIZED = "Authorization has been denied for this request.";
Я вытащил этот string
из определения SRResources.RequestNotAuthorized
в .NET framework.
Отличный ответ Кевин! Я реализовал мой тот же самый путь, потому что выполнение OnAuthorization
в базовом классе не имело смысла, потому что я проверял HTTP-заголовок, который был обычным для нашего приложения, и на самом деле не хотел вообще проверять Принципала, потому что не было ни одного.