ASP.NET 基于策略的核心身份验证不起作用
ASP.NET Core policy-based authentication not working
我必须在我们的本地数据库中而不是在 Azure AD 中管理我们的用户角色。但是,我还需要对控制器进行基于策略的授权,因为我们同时拥有管理和客户区域。
为了处理这个问题,我添加了一个授权过滤器,它从 Session
或数据库加载用户角色,将 Identity
添加到 Principal
,然后继续。这 Identity
添加了适当的 Role
Claim
.
在离开授权过滤器之前,IsInRole
returns true
符合预期,并且有两个 Identities
.
我的授权过滤器如下所示:
public class MyAuthFilter : IAsyncAuthorizationFilter
{
private readonly IUserService userService;
public MyAuthFilter(IUserService userService)
{
this.userService = userService;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
AuthUserViewModel authUserViewModel;
var sessionViewModelJson = context.HttpContext.Session.GetString(user.AzureObjectId());
if (string.IsNullOrEmpty(sessionViewModelJson))
{
authUserViewModel = await ConstructSessionViewModel(context);
}
else
{
authUserViewModel = JsonConvert.DeserializeObject<AuthUserViewModel>(sessionViewModelJson);
}
user.AddIdentity(authUserViewModel?.Role);
}
}
private async Task<AuthUserViewModel> ConstructSessionViewModel(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
var parsedObjectId = Guid.Parse(user.AzureObjectId());
var findUserResult = await userService.FindByAzureObjectId(new FindByAzureObjectIdRequest
{
AzureObjectId = parsedObjectId
});
if (findUserResult.Success)
{
var userModel = findUserResult.User;
var viewModel = new AuthUserViewModel
{
AzureObjectId = parsedObjectId,
UserId = userModel.Id,
SchoolId = userModel.SchoolId.GetValueOrDefault(),
Name = userModel.Name,
Email = userModel.Email,
PhoneNumber = userModel.PhoneNumber,
Role = userModel.Role
};
context.HttpContext.Session.SetString(user.AzureObjectId(), JsonConvert.SerializeObject(viewModel));
return viewModel;
}
return null;
}
}
AddIdentity
扩展方法如下所示:
public static void AddIdentity(this ClaimsPrincipal principal, string role)
{
if (string.IsNullOrWhiteSpace(role) || principal.IsInRole(role))
{
return;
}
switch (role)
{
case Roles.School:
principal.AddIdentity(new SchoolIdentity());
break;
case Roles.Admin:
principal.AddIdentity(new AdminIdentity());
break;
}
}
在本例中,添加的是 SchoolIdentity
,它看起来像这样:
public class SchoolIdentity : ClaimsIdentity
{
public SchoolIdentity()
{
AddClaim(new SchoolPortalClaim());
}
}
最后,SchoolPortalClaim
看起来像这样:
public class SchoolPortalClaim : Claim
{
public SchoolPortalClaim() : base(ClaimTypes.Role, "School")
{
}
public SchoolPortalClaim(BinaryReader reader) : base(reader)
{
}
public SchoolPortalClaim(BinaryReader reader, ClaimsIdentity subject) : base(reader, subject)
{
}
protected SchoolPortalClaim(Claim other) : base(other)
{
}
protected SchoolPortalClaim(Claim other, ClaimsIdentity subject) : base(other, subject)
{
}
public SchoolPortalClaim(string type, string value) : base(type, value)
{
}
public SchoolPortalClaim(string type, string value, string valueType) : base(type, value, valueType)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer) : base(type, value, valueType, issuer)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer) : base(type, value, valueType, issuer, originalIssuer)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject) : base(type, value, valueType, issuer, originalIssuer, subject)
{
}
}
策略执行时出现问题:
services.AddAuthorization(options =>
{
options.AddPolicy(Policies.School,
policy => policy.RequireAssertion(
context => context.User.IsInRole(Roles.School)));
});
context.User
没有通过授权过滤器添加的 Identity
。
如何让它向下游移动?
有问题的 Controller
看起来像这样:
[Area(Areas.School)]
[Authorize(Policy = Policies.School)]
public class HomeController : BaseController
{
public HomeController(IUserService userService) :
base(userService)
{
}
public IActionResult Index()
{
return RedirectToAction("Index", "Presentation", new {Area = "School"});
}
}
这里的根本问题是 RequireAssertion
回调在 IAsyncAuthorizationFilter.OnAuthorizationAsync
之前被调用,这意味着您在 OnAuthorizationAsync
中添加的 ClaimsIdentity
当时还没有添加你需要它。
您可以转向 IClaimsTransformation
, which declares a TransformAsync
方法的自定义实现,而不是使用自定义 authz 过滤器。此方法采用当前 ClaimsPrincipal
并允许您根据需要 return 相同的 ClaimsPrincipal
或新的
这是一个框架示例:
public class MyClaimsTransformation : IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Your existing logic to add the relevant ClaimsIdentity.
// You might want to check if the ClaimsPrincipal already contains either
// SchoolIdentity or AdminIdentity here, as this operation may run
// more than once.
// ...
}
}
要注册这个实现,使用类似这样的东西,在 ConfigureServices
:
services.AddSingleton<IClaimsTransformation, MyClaimsTransformation>();
我必须在我们的本地数据库中而不是在 Azure AD 中管理我们的用户角色。但是,我还需要对控制器进行基于策略的授权,因为我们同时拥有管理和客户区域。
为了处理这个问题,我添加了一个授权过滤器,它从 Session
或数据库加载用户角色,将 Identity
添加到 Principal
,然后继续。这 Identity
添加了适当的 Role
Claim
.
在离开授权过滤器之前,IsInRole
returns true
符合预期,并且有两个 Identities
.
我的授权过滤器如下所示:
public class MyAuthFilter : IAsyncAuthorizationFilter
{
private readonly IUserService userService;
public MyAuthFilter(IUserService userService)
{
this.userService = userService;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
AuthUserViewModel authUserViewModel;
var sessionViewModelJson = context.HttpContext.Session.GetString(user.AzureObjectId());
if (string.IsNullOrEmpty(sessionViewModelJson))
{
authUserViewModel = await ConstructSessionViewModel(context);
}
else
{
authUserViewModel = JsonConvert.DeserializeObject<AuthUserViewModel>(sessionViewModelJson);
}
user.AddIdentity(authUserViewModel?.Role);
}
}
private async Task<AuthUserViewModel> ConstructSessionViewModel(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
var parsedObjectId = Guid.Parse(user.AzureObjectId());
var findUserResult = await userService.FindByAzureObjectId(new FindByAzureObjectIdRequest
{
AzureObjectId = parsedObjectId
});
if (findUserResult.Success)
{
var userModel = findUserResult.User;
var viewModel = new AuthUserViewModel
{
AzureObjectId = parsedObjectId,
UserId = userModel.Id,
SchoolId = userModel.SchoolId.GetValueOrDefault(),
Name = userModel.Name,
Email = userModel.Email,
PhoneNumber = userModel.PhoneNumber,
Role = userModel.Role
};
context.HttpContext.Session.SetString(user.AzureObjectId(), JsonConvert.SerializeObject(viewModel));
return viewModel;
}
return null;
}
}
AddIdentity
扩展方法如下所示:
public static void AddIdentity(this ClaimsPrincipal principal, string role)
{
if (string.IsNullOrWhiteSpace(role) || principal.IsInRole(role))
{
return;
}
switch (role)
{
case Roles.School:
principal.AddIdentity(new SchoolIdentity());
break;
case Roles.Admin:
principal.AddIdentity(new AdminIdentity());
break;
}
}
在本例中,添加的是 SchoolIdentity
,它看起来像这样:
public class SchoolIdentity : ClaimsIdentity
{
public SchoolIdentity()
{
AddClaim(new SchoolPortalClaim());
}
}
最后,SchoolPortalClaim
看起来像这样:
public class SchoolPortalClaim : Claim
{
public SchoolPortalClaim() : base(ClaimTypes.Role, "School")
{
}
public SchoolPortalClaim(BinaryReader reader) : base(reader)
{
}
public SchoolPortalClaim(BinaryReader reader, ClaimsIdentity subject) : base(reader, subject)
{
}
protected SchoolPortalClaim(Claim other) : base(other)
{
}
protected SchoolPortalClaim(Claim other, ClaimsIdentity subject) : base(other, subject)
{
}
public SchoolPortalClaim(string type, string value) : base(type, value)
{
}
public SchoolPortalClaim(string type, string value, string valueType) : base(type, value, valueType)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer) : base(type, value, valueType, issuer)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer) : base(type, value, valueType, issuer, originalIssuer)
{
}
public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject) : base(type, value, valueType, issuer, originalIssuer, subject)
{
}
}
策略执行时出现问题:
services.AddAuthorization(options =>
{
options.AddPolicy(Policies.School,
policy => policy.RequireAssertion(
context => context.User.IsInRole(Roles.School)));
});
context.User
没有通过授权过滤器添加的 Identity
。
如何让它向下游移动?
有问题的 Controller
看起来像这样:
[Area(Areas.School)]
[Authorize(Policy = Policies.School)]
public class HomeController : BaseController
{
public HomeController(IUserService userService) :
base(userService)
{
}
public IActionResult Index()
{
return RedirectToAction("Index", "Presentation", new {Area = "School"});
}
}
这里的根本问题是 RequireAssertion
回调在 IAsyncAuthorizationFilter.OnAuthorizationAsync
之前被调用,这意味着您在 OnAuthorizationAsync
中添加的 ClaimsIdentity
当时还没有添加你需要它。
您可以转向 IClaimsTransformation
, which declares a TransformAsync
方法的自定义实现,而不是使用自定义 authz 过滤器。此方法采用当前 ClaimsPrincipal
并允许您根据需要 return 相同的 ClaimsPrincipal
或新的
这是一个框架示例:
public class MyClaimsTransformation : IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Your existing logic to add the relevant ClaimsIdentity.
// You might want to check if the ClaimsPrincipal already contains either
// SchoolIdentity or AdminIdentity here, as this operation may run
// more than once.
// ...
}
}
要注册这个实现,使用类似这样的东西,在 ConfigureServices
:
services.AddSingleton<IClaimsTransformation, MyClaimsTransformation>();