У меня есть гибридное приложение 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
. Возможно, это путь к решению.