为什么 ASP.NET MVC 应用在修改后不能立即识别用户角色更改?

Why ASP.NET MVC app doesn't recognize user role changes right after modification?

我有一个 ASP.NET MVC 混合应用程序,它有一个 ApiController 除了 MVC 控制器。我在 MVC 控制器和 ApiController 中都在控制器级别和有时在方法级别使用基于角色的授权属性。我在基于模型的设计中使用 Entity Framework 6。

Controller级别权限:

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

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

当我从控制器级别授权转移时,因为它可供未登录的用户访问:

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

或者因为我软化了授权("User" 比"RegularUser" 权限低):

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

用户注册后,通常会立即获得 "User" 和 "RegularUser" 角色。我可以通过查询数据库的 AspNetUserRoles table 来确认这一点,或者我什至有一个供管理员控制的管理视图,它甚至可以通过相同的 ASP.NET MVC 应用程序显示角色。但是,当新创建的用户尝试访问 MVC 控制器上的端点或视图时,它会被框架的授权规则拒绝并获得 401 Unauthorized。就像一些内部部件(我不知道它是否使用 RoleManager 或引擎盖下的东西)"didn't get the message" 用户已经在角色中。

奇怪的是,ApiController 端点可以正常工作并识别用户的角色。在 MVC 控制器抛出 401 之后,用户被重定向到登录页面(带有重定向提示)。在用户登录的同时,菜单栏反映了这一点(即使重定向到登录页面 - 这很混乱)。一旦用户服从并突然重新登录,精神分裂症行为就会消失,MVC 控制器端点也开始识别用户的角色。不用说了,这就是unacceptable这种方式。

我的包裹:

<?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>

ApiController[Authorize(Roles="...")] 属性正在使用 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)),

                // 
                // 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 控制器,因为我真的需要它。我猜 auth 子系统具有与 MVC 控制器使用的常规实体上下文不同的数据库上下文(在基础 class 中实例化)。

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

并且每个 MVC 控制器都是该基础 class 的后裔。尽管我可能有不同的上下文,但我肯定确认我在数据库中添加了正确的角色,它们是持久的。 auth 子系统上下文是否会与 DB 的状态不同步?如何同步?


@阿里,当前代码:

                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 之后扮演角色 addition/removals,但到目前为止没有帮助。实际SignInAsyncAccountController的一个方法,由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。也许这是通往解决方案的线索。

我想当你创建一个新用户时,在你的注册操作中你先登录他,然后再向新创建的用户添加 "User" 和 "RegularUser" 角色,这样为登录用户创建的会话不会'包含他的那些角色

当用户重新登录时,将创建新会话,现在它包含角色。

如果不明白我的回答,请显示您的注册操作,我可以提供更多帮助。

如果您希望每个请求立即命中 DB 以检查角色,您应该在 Startup.Auth.

中将 validateInterval: TimeSpan.FromMinutes(30) 更改为 validateInterval: TimeSpan.FromSeconds(0)

How do I forcefully propagate role changes to users with ASP.NET Identity 2.0.1?

由于您使用的是 cookie,因此需要确保在分配新角色后使用新角色重新创建 cookie(否则,过时的 cookie 会一直存在,直到过期)。授予新角色后,您可以使用身份验证管理器将用户注销,然后再次登录,从而使用新添加的角色重新创建他们的 cookie。我包含了一个片段,但您必须针对您的代码进行自定义:

IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignOut("ApplicationCookie");
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);

显示此类错误的另一种可能性是在 Web.config 中设置 authorization 覆盖控制器的授权。

例如

<configuration>
    ...
    <location path="projects">
        <system.web>
            <authorization>
                <allow roles="Administrator" />
                <deny users="*" />
            </authorization>
        </system.web>
    </location>
</configuration>