Почему приложение ASP.NET MVC не распознает изменения роли пользователя сразу после модификации?

У меня есть гибридное приложение ASP.NET MVC, в котором помимо контроллеров MVC есть ApiController. Я использую атрибуты авторизации на основе ролей как в контроллерах MVC, так и в ApiController как на уровне контроллера, так и иногда на уровне метода. Я использую Entity Framework 6 с дизайном на основе модели.

Полномочия на уровне контроллера:

[Authorize(Roles = "Administrator,RegularUser")]
public class EngineController : ApiController
{

or

[System.Web.Mvc.Authorize(Roles = "Administrator,RegularUser")]
public class ProjectsController : Controller
{

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

    [AllowAnonymous]
    [HttpPost]
    public async Task<CheckCouponReturnValueModel> CheckCoupon([FromBody] CouponCodeRequestModel requestModel)

или потому что я смягчаю авторизацию ("Пользователь" менее привилегирован, чем "Обычный пользователь"):

    [OverrideAuthorization()]
    [Authorize(Roles = "User")]
    [HttpPost]
    public TopicReturnValueModel GetTopic([FromBody]TopicReferenceModel requestModel)

Сразу после регистрации пользователь обычно получает роли «Пользователь» и «Обычный пользователь». Я могу подтвердить это, запросив таблицу AspNetUserRoles базы данных, или у меня даже есть представление управления для администраторов, чтобы контролировать это, и оно показывает роли даже через одно и то же приложение ASP.NET MVC. Однако, когда вновь созданный пользователь пытается получить доступ к конечным точкам или представлениям на контроллерах MVC, правила авторизации платформы отклоняют его и он получает 401 Unauthorized. Это похоже на то, что некоторые внутренние части (я не знаю, использует ли он RoleManager или что под капотом) «не получили сообщение», что пользователь уже находится в ролях.

Как ни странно, конечные точки ApiController работают и распознают роли пользователя. После того, как контроллеры MVC выдают 401, пользователь перенаправляется на страницу входа (с подсказкой перенаправления). В то же время, когда пользователь вошел в систему, строка меню отражает это (даже при перенаправлении на страницу входа - это сбивает с толку). Как только пользователь подчиняется и повторно входит в систему, внезапно шизофреническое поведение исчезает, и конечные точки контроллера MVC также начинают распознавать роли пользователя. Излишне говорить, что в таком виде это неприемлемо.

Мои пакеты:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="animate.css" version="3.3.0.0" targetFramework="net461" />
  <package id="Antlr" version="3.5.0.2" targetFramework="net45" />
  <package id="bootstrap" version="3.3.7" targetFramework="net461" />
  <package id="Bootstrap.Datepicker" version="1.6.4" targetFramework="net461" />
  <package id="EntityFramework" version="6.1.3" targetFramework="net461" />
  <package id="FontAwesome" version="4.7.0" targetFramework="net461" />
  <package id="free-jqGrid" version="4.14.0" targetFramework="net461" />
  <package id="jQuery" version="2.2.4" allowedVersions="[2,3)" targetFramework="net461" />
  <package id="jquery.datatables" version="1.10.12" targetFramework="net461" />
  <package id="jQuery.InputMask" version="3.3.4" targetFramework="net461" />
  <package id="jquery.noty" version="2.3.5" targetFramework="net461" />
  <package id="jQuery.UI.Combined" version="1.12.1" targetFramework="net461" />
  <package id="jQuery.Validation" version="1.16.0" targetFramework="net461" />
  <package id="JSZip" version="3.1.3" targetFramework="net461" />
  <package id="knockoutjs" version="3.4.2" targetFramework="net461" />
  <package id="KnockoutJS.Validation" version="3.0.0" targetFramework="net45" />
  <package id="Microsoft.AspNet.Cors" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net461" />
  <package id="Microsoft.AspNet.Identity.EntityFramework" version="2.2.1" targetFramework="net461" />
  <package id="Microsoft.AspNet.Identity.Owin" version="2.2.1" targetFramework="net461" />
  <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.Web.Optimization" version="1.1.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi.Cors" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.jQuery.Unobtrusive.Validation" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Host.SystemWeb" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Cookies" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Facebook" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Google" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.MicrosoftAccount" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.OAuth" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Twitter" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net45" />
  <package id="MimeTypeMap.List" version="1.1.0" targetFramework="net461" />
  <package id="Modernizr" version="2.8.3" targetFramework="net45" />
  <package id="Moment.js" version="2.18.1" targetFramework="net461" />
  <package id="morelinq" version="2.3.0" targetFramework="net461" />
  <package id="mousetrap" version="1.3" targetFramework="net461" />
  <package id="Mvc.JQuery.DataTables" version="1.5.31" targetFramework="net461" />
  <package id="Mvc.JQuery.DataTables.Common" version="1.5.31" targetFramework="net461" />
  <package id="Mvc.JQuery.Datatables.Templates" version="1.5.31" targetFramework="net461" />
  <package id="MvcSiteMapProvider.MVC5" version="4.6.22" targetFramework="net45" />
  <package id="MvcSiteMapProvider.MVC5.Core" version="4.6.22" targetFramework="net45" />
  <package id="MvcSiteMapProvider.Web" version="4.6.22" targetFramework="net45" />
  <package id="Nager.Date" version="1.6.0" targetFramework="net461" />
  <package id="Newtonsoft.Json" version="10.0.2" targetFramework="net461" />
  <package id="Owin" version="1.0" targetFramework="net45" />
  <package id="pdfmake" version="0.1.18" targetFramework="net461" />
  <package id="PDFsharp" version="1.32.3057.0" targetFramework="net461" />
  <package id="QueryInterceptor" version="0.2" targetFramework="net45" />
  <package id="ReCaptcha-AspNet" version="1.4.0" targetFramework="net461" />
  <package id="Respond" version="1.4.2" targetFramework="net461" />
  <package id="Sendgrid" version="9.1.1" targetFramework="net461" />
  <package id="SendGrid.CSharp.HTTP.Client" version="3.3.0" targetFramework="net461" />
  <package id="Spin.js" version="2.3.2.1" targetFramework="net461" />
  <package id="Stripe.net" version="8.2.0" targetFramework="net461" />
  <package id="System.Linq.Dynamic.Core" version="1.0.6.13" targetFramework="net461" />
  <package id="System.Net.Http" version="4.0.0" targetFramework="net461" allowedVersions="[4,4.0.0]" />
  <package id="WebActivatorEx" version="2.2.0" targetFramework="net461" />
  <package id="WebGrease" version="1.6.0" targetFramework="net45" />
</packages>

Атрибуты [Authorize(Roles="...")] ApiController используют System.Web.Http.AuthorizeAttribute, в то время как мои контроллеры MVC используют System.Web.Mvc.AuthorizeAttribute. Я думал, что ApiController правильно распределяет роли, но, видимо, я заменил все объявления авторизации в контроллерах MVC на System.Web.Http.AuthorizeAttribute, и это тоже не решило проблему.


Startup.Auth спросил @solidau:

    public void ConfigureAuth(IAppBuilder app)
    {
        // Configure the db context, user manager and role manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        // Configure the sign in cookie
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            ExpireTimeSpan = new System.TimeSpan(8, 0, 0),    // Uncomment this to enable 8 hour inactivity/idle expiration
            SlidingExpiration = true,
            Provider = new CookieAuthenticationProvider
            {
                // Enables the application to validate the security stamp when the user logs in.
                // This is a security feature which is used when you change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),

                // https://stackoverflow.com/questions/20149750/unauthorised-webapi-call-returning-login-page-rather-than-401
                // http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/
                // http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/
                OnApplyRedirect = ctx =>
                {
                    if (!IsAjaxRequest(ctx.Request))
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                    }
                }
            }
        });

        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

        // Enables the application to remember the second login verification factor such as phone or email.
        // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
        // This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

    }

    private static bool IsAjaxRequest(IOwinRequest request)
    {
        IReadableStringCollection queryXML = request.Query;
        if ((queryXML != null) && (queryXML["X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }

        IReadableStringCollection queryJSON = request.Query;
        if ((queryJSON != null) && (queryJSON["Content-Type"] == "application/json"))
        {
            return true;
        }

        IHeaderDictionary headersXML = request.Headers;
        var isAjax = ((headersXML != null) && (headersXML["X-Requested-With"] == "XMLHttpRequest"));

        IHeaderDictionary headers = request.Headers;
        var isJson = ((headers != null) && (headers["Content-Type"] == "application/json"));

        return isAjax || isJson;
    }

Да, есть один трюк, который делает сеанс доступным для ApiController, а не только для контроллера MVC, потому что мне это действительно нужно. Я предполагаю, что подсистема аутентификации имеет другой контекст БД, чем обычный контекст сущностей, используемый контроллерами MVC (созданными в базовом классе).

public abstract class WorkflowControllersBase : Controller
{
    protected Entities _context = new Entities();

и каждый контроллер MVC является потомком этого базового класса. Хотя у меня могут быть разные контексты, я определенно подтверждаю, что добавляю правильные роли в БД, они сохраняются. Может ли контекст подсистемы аутентификации рассинхронизироваться с состоянием БД? Как его синхронизировать?


@Ali, текущий код:

                IdentityResult result = await UserManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    await UserManager.AddToRoleAsync(user.Id, "User");
                    await UserManager.AddToRoleAsync(user.Id, model.AccountType);
                    await SignInAsync(user, isPersistent: true);
                    if (model.AccountType != "QuickDeal")
                    {
                        if (User.IsInRole("QuickDeal"))  // Remove from QuickDeal if the user upgraded
                            await UserManager.RemoveFromRoleAsync(user.Id, "QuickDeal");
                        await UserManager.AddToRoleAsync(user.Id, "RegularUser");
                    }

Пробовал выполнять добавление/удаление роли после SignInAsync, но пока не помогло. Фактический SignInAsync — это метод AccountController, предоставляемый шаблоном ASP.NET MVC:

    private async Task SignInAsync(ApplicationUser user, bool isPersistent)
    {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
        AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, await user.GenerateUserIdentityAsync(UserManager));
    }

<configSections>
  <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
</configSections>
<connectionStrings>
  <add name="DefaultConnection" connectionString="Server=tcp:xyx.database.windows.net,1433;Initial Catalog=XYZ;Persist Security Info=False;User ID=csaba;Password=*************;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" providerName="System.Data.SqlClient" />
  <add name="Entities" connectionString="metadata=res://*/Models.EntityModel.csdl|res://*/Models.EntityModel.ssdl|res://*/Models.EntityModel.msl;provider=System.Data.SqlClient;provider connection string='Server=tcp:zyx.database.windows.net,1433;Initial Catalog=XYZ;Persist Security Info=False;User ID=csaba;Password=***************;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;'" providerName="System.Data.EntityClient" />
</connectionStrings>

Обратите внимание, что строка по умолчанию, предоставляемая Azure, не включает MARS. Но таким образом я получил ошибку, поэтому я поставил MultipleActiveResultSets=True. Возможно, это путь к решению.


person Csaba Toth    schedule 29.04.2017    source источник
comment
не могли бы вы показать свой код конфигурации аутентификации при запуске?   -  person solidau    schedule 04.05.2017


Ответы (3)


Поскольку вы используете файлы cookie, вам необходимо убедиться, что файл cookie воссоздан с новыми ролями после назначения новой роли (в противном случае устаревший файл cookie остается, пока не истечет срок его действия). После того, как вы предоставите новую роль, вы можете использовать диспетчер аутентификации для выхода пользователя из системы, а затем снова войти в систему, таким образом воссоздав их файл cookie с новыми добавленными ролями. Я включил фрагмент, но вам придется настроить его для своего кода:

IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignOut("ApplicationCookie");
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
person solidau    schedule 04.05.2017
comment
Интересно, сегодня вечером тоже попробую. Я никогда не сталкивался с этим. - person Csaba Toth; 05.05.2017
comment
Я бы предпочел сначала попробовать это, а не устанавливать validateInterval: в TimeSpan.FromSeconds(0). Однако, если вы посмотрите на код, который я процитировал, await SignInAsync(user, isPersistent: true); выполняет выход и подпись, я включу раздел кода в свой пост. - person Csaba Toth; 05.05.2017
comment
Я использовал вариант вашего совета: поскольку исходный код использует DefaultAuthenticationTypes.ExternalCookie, теперь я очищаю оба (я использую DefaultAuthenticationTypes.ApplicationCookie вместо "ApplicationCookie"). И это, кажется, решает проблему до сих пор. Если я подтвержу это в живой среде (скоро получится), я приму ваш ответ. - person Csaba Toth; 05.05.2017

Я думаю, что когда вы создаете нового пользователя, в своем действии регистрации вы входите в него, прежде чем добавлять роли «Пользователь» и «Обычный пользователь» для вновь созданного пользователя, поэтому сеанс, созданный для вошедшего в систему пользователя, не содержит эти роли для него

И когда пользователь повторно войдет в систему, будет создана новая сессия, и теперь она будет содержать роли.

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

если вы хотите, чтобы каждый запрос немедленно попадал в БД для проверки ролей, вы должны изменить validateInterval: TimeSpan.FromMinutes(30) на validateInterval: TimeSpan.FromSeconds(0) в своем Startup.Auth.

См. Как принудительно передать изменения ролей пользователям с ASP.NET Identity 2.0.1?

person Ali Zeinali    schedule 04.05.2017
comment
Я изменил заказ, но все еще сталкиваюсь с отказом в авторизации 401. Прежде чем войти в систему, для тогдашнего анонимного пользователя все еще установлен сеанс. Я не был уверен, что настоящий SignIn заменит этот сеанс другим? В любом случае: ошибка 401 также возникает, когда пользователь переходит на более продвинутый план подписки. В таком случае пользователь уже вошел в систему и аутентифицирован, и я удаляю и добавляю роль. Видимо какая-то часть системы не знает об этом до повторного входа в систему. - person Csaba Toth; 04.05.2017
comment
а также подсистема авторизации не просматривает БД для проверки ролей, она просматривает сеанс, который был создан при входе в систему, и только подсистема аутентификации просматривает БД для ролей и создает правильный сеанс для подсистемы авторизации, потому что когда вы добавляете/удаляете роль пользователя, вошедшего в систему, не получит изменений, и ему нужно будет снова войти в систему, чтобы получить изменения - person Ali Zeinali; 04.05.2017
comment
Я попробую это сегодня вечером - person Csaba Toth; 05.05.2017
comment
Таким образом, validateInterval в TimeSpan.FromSeconds(0) - это один из способов, но он имеет два последствия (вы ссылаетесь на статью SO): 1. влияние на производительность, поскольку код всегда будет достигать базы данных, 2. выход из системы может быть сложным. Меня сейчас не беспокоит сценарий, когда администраторы насильно меняют роли пользователей. Поэтому решение @solidau выглядит лучше, когда я выхожу из системы и вхожу в систему в нескольких конкретных точках программного обеспечения. Спасибо за вашу помощь, я проголосовал за это, так как это тоже способ, особенно в сценарии с администратором. - person Csaba Toth; 05.05.2017

Другая возможность, которая показывает подобную ошибку, состоит в том, что authorization настроен в Web.config, который переопределяет авторизацию контроллера.

e.g.

<configuration>
    ...
    <location path="projects">
        <system.web>
            <authorization>
                <allow roles="Administrator" />
                <deny users="*" />
            </authorization>
        </system.web>
    </location>
</configuration>
person noelicus    schedule 29.08.2017
comment
Под ошибкой вы имеете в виду, когда я получил 401 только от ApiController или только от контроллера MVC? - person Csaba Toth; 30.08.2017
comment
Возможно, это может помочь некоторым людям, но я не вижу раздела authorization в своем файле web.config. - person Csaba Toth; 30.08.2017
comment
Привет. Под «подобной ошибкой» я подразумеваю какой-то отказ в авторизации. То, что вы описали, действительно похоже на проблему, с которой я столкнулся, и в итоге я прочитал ее здесь. Когда я нашел решение, я подумал, что должен добавить его как ответ для кого-то еще ????. <location> входит внутрь <configuration> (отредактировал ответ, чтобы показать это). - person noelicus; 30.08.2017