Подтвердить что ты не робот

Как настроить 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 для текущего Принципа в этом сценарии?

4b9b3361

Ответ 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-заголовок, который был обычным для нашего приложения, и на самом деле не хотел вообще проверять Принципала, потому что не было ни одного.