如何使用自定义策略模式实现 jwt 令牌基础身份验证以在 .net 核心中进行授权?
Posted
技术标签:
【中文标题】如何使用自定义策略模式实现 jwt 令牌基础身份验证以在 .net 核心中进行授权?【英文标题】:How to implement jwt token base authentication with custom policy schema for authorization in .net core? 【发布时间】:2020-08-01 06:54:13 【问题描述】:我正在尝试通过实现基于 jwt 令牌的身份验证来在 .NET Core 3.1 中创建一个 Playground 应用程序,并且希望使用我的自定义策略架构(可怕吗?)。我能够使用从 .NET Framework 中的 AuthorizeAttribute
派生的自定义过滤器属性来完成这项工作,但在使用 .NET Core 时遇到了困难。因为我正在使用OnAuthorization
钩子并正在捕获HttpActionContext
,解析令牌并检查角色策略等......但现在我正在使用IAuthorizationHandler
,我没有机会让它在我想要的情况下工作到目前为止。我阅读了许多示例、文章,但我找不到相同的方法来解决我正在尝试做的事情。
(PS:当我搜索几个小时并找不到类似的方法时,这也让我感到非常紧张,因为我可能会走完全错误的路线,或者试图重新发明***。让我们看看我是否上午..)
我还寻找了IdendityServer4(但很多人发现它更容易管理 - 但对于我正在尝试做的事情对我来说似乎有点矫枉过正。如果我错了,请怪我。)
到目前为止我所做的是,当用户登录时,我能够成功创建令牌。这是代码: (如果你想看看我到底在问什么,请直接滚动到最后,如果你要回答,请仔细阅读)
在幕后,我在 db 中使用 salted-hash password 以及 key-strecthing 和 PBKDF2 algorithm(我非常感谢任何安全问题)
我的 GenerateToken 函数:
[HttpPost("token")]
public IActionResult GenerateToken(UserCredentialDto userCredentialDto)
bool isValidUser = _appUserManager.IsValidCredentials(userCredentialDto);
if (!isValidUser)
return BadRequest("invalid user/pass combination");
// assume I am getting all the roles that user has and add them in claims.
var claims = _appUserManager.GetUserClaims(userCredentialDto);
var key = new SymmetricSecurityKey(_jwtSettings.Key);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.Now.AddMinutes(StaticAFUConfigHelper.TokenExpirationInMinutes), signingCredentials: creds);
return Ok(new
token = new JwtSecurityTokenHandler().WriteToken(token)
);
一般背景:(我没有使用 aspnet 身份表)
每个用户都有角色(我有一个表作为角色,外部参照表作为 UserRole) 角色有策略(我有一个表作为策略,外部参照表作为 角色政策)一般工作流程是:
用户请求令牌(简单 - 登录时输入用户名和密码 页) 如果凭据有效,则为用户生成一个令牌,其中包括声明中的用户角色。 一旦用户向 API 方法发出请求,检查是否有 any 用户拥有的role
,包括必需 policy
能够
提出这个要求!
所以“基本上”,我想获取令牌(将令牌与请求一起发送或在声明中发送),解决它,检查用户是否有我正在寻找的内容(具体政策等)并根据那个。
而在startup.cs
//Authentication
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options = >
options.TokenValidationParameters = new TokenValidationParameters
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "my issuer",
ValidAudience = "my audience",
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String("assume this is my secret key"))
;
options.SaveToken = true;
);
// this part I'm also not sure as if I have 100s of policies,
// would all of them has to be defined here?
// and how I specifically assign this to an api method! Anyways please keep reading if you dont mind
services.AddAuthorization(options =>
options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));
然后我创建了 TokenValidationHandler
,它是从 AuthorizationHandler 派生的,带有我的自定义策略属性 NeedsPolicyAttribute
..
NeedsPolicyAttribute:
public class NeedsPolicyAttribute: IAuthorizationRequirement
public PolicyEnum RequiredPolicy
get;
public NeedsPolicyAttribute(PolicyEnum requiredPolicy)
RequiredPolicy = requiredPolicy;
HandleRequirementAsync 是:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NeedsPolicyAttribute requirement)
var myToken = "1234567889"; // just hardcoded for example - assume I got the JWT from the context.
SecurityToken validatedToken;
var handler = new JwtSecurityTokenHandler();
// assume there was no exception and I was able to validate the token which is a valid token...
var user = handler.ValidateToken(myToken, _jwtSettings.TokenValidationParameters, out validatedToken);
// ************************........ATTENTION HERE....... *****************
// I would like to check if the user has a role and which includes the policy which was required by the api method.
//if so then,
context.Succeed(requirement);
//if not then
context.Fail();
// and finally
return Task.CompletedTask;
我的示例 API 方法被修饰为:
[HttpGet][Route("get/id")]
[Authorize("CanReadData")] // THIS JUST LETS ME TRIGGER MY CUSTOM ATTRIBUTE TO BE CAPTURED BY HandleRequirementAsync BUT I CAN ONLY PROVIDE HARDCODE STRING
public ActionResult < AppUserDto > GetAppUser(int id)
return _appUserManager.Get(id);
我真正想要做的是,用所需的策略装饰我的 API 方法,并验证给定的令牌是否具有包含所需策略的角色的声明。
如下所示:
[HttpGet][Route("get/id")]
[MyPolicyAttrubute(MyPolicyEnum.CanDoBlaBla)] // I want to capture this in HandleRequirementAsync if possible and compare with my user claims..
public ActionResult < AppUserDto > GetAppUser(int id)
return _appUserManager.Get(id);
令人惊叹的问题:
我想重新发明***吗? 即使我是,这种方法有什么明显的不良做法吗? 还有其他/更好的建议吗?【问题讨论】:
【参考方案1】:花了几个小时后,我能够完成这项工作。所以我只是想将其发布为我的问题的答案,但我的“令人叹为观止的问题”(请参阅上面问题的末尾)仍然存在。因此请注意,此解决方案不能保证任何这些问题。
在上面的问题中,我对StartUp.cs
中的代码段的担忧之一是:
// if I have 100s of policies, would all of them have to be defined here?
services.AddAuthorization(options =>
options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));
因为示例代码将它们一一添加为硬编码字符串,这从一开始就困扰着我,因为我想使用 Enum 而不是硬编码值。而且我不想在Startup.cs
中添加很多行,每次我向应用程序添加新策略时也需要更新这些行。
其实这很容易。我所做的只是:
我编写了一个扩展来获取所有枚举值,如下所示:
public static class EnumUtils
public static IEnumerable < T > GetAllEnumValues < T > ()
return System.Enum.GetValues(typeof(T)).Cast < T > ();
所以我可以像下面这样使用它。因此,我现在可以在 API 方法之上使用任何新创建的 Policy 枚举值作为属性,而无需触及 StartUp.cs
。
services.AddAuthorization(options =>
// add all the policies to option to be able to use in ExtendedAuthorizeAttribute on api methods.
foreach(var policyEnum in EnumUtils.GetAllEnumValues < PolicyEnum > ())
options.AddPolicy(policyEnum.ToString(), policy => policy.Requirements.Add(new ExtendedAuthorizeAttribute(policyEnum)));
);
然后我添加了用户拥有的策略:
public List < Claim > GetUserClaims(AuthRequestDto authRequestDto)
var userRoles = _unitOfWork.Roles.GetUserRoles(authRequestDto.UserId);
var policies = userRoles.SelectMany(x = >x.RolePolicies.Where(p = >p.Policy.IsActive).Select(y = >y.Policy.Name)).Distinct().ToList();
var claims = new List < Claim > ();
policies.ForEach(policy = >claims.Add(new Claim("UserPolicy", policy)));
claims.Add(new Claim("Id", authRequestDto.UserId.ToString()));
return claims;
并将它们附加到我的令牌上,因此一旦用户使用该令牌发出请求,我就可以解决它并检查 api 方法所需的策略。
然后我创建了新的Attribute
为ExtendedAuthorizeAttribute
,它派生自AuthroizeAttribute
并实现了IAuthorizationRequirement
这里有两件事:
我从AuthroizeAttribute
派生了我的自定义属性,因为我希望它自动触发以进行授权,以检查用户是否具有该 api 方法所需的策略。
我实现了IAuthorizationRequirement
,因为这让我可以在HandleRequirementAsync
方法中使用我的属性作为“要求”。
所以我创建的属性是:
/// <summary>
/// Extended Authorize Attribute is derived from Authorize Attribute
/// also implements IAuthorizationRequirement.
/// Deriving from AuthorizeAttribute accepts only string for policy names
/// By using this extension class, it let's me use Policy Enum then it converts it to string
/// before passing it to AuthorizeAttribute which was not possible in controller.
/// </summary>
public class ExtendedAuthorizeAttribute: AuthorizeAttribute,
IAuthorizationRequirement
public ExtendedAuthorizeAttribute(PolicyEnum policyEnum = PolicyEnum.General) : base(policyEnum.ToString())
而TokenValidationHandler
变成了下面这样:
public class TokenValidationHandler: AuthorizationHandler < ExtendedAuthorizeAttribute >
private readonly JwtSettings _jwtSettings;
private readonly IHttpContextAccessor _contextAccessor;
public TokenValidationHandler(JwtSettings jwtSettings, IHttpContextAccessor contextAccessor)
_jwtSettings = jwtSettings;
_contextAccessor = contextAccessor;
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtendedAuthorizeAttribute requirement)
// injected the IHttpContextAccessor to get the token from the request.
var rawToken = !_contextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization") ? string.Empty: _contextAccessor ? .HttpContext ? .Request ? .Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(rawToken))
context.Fail();
return Task.CompletedTask;
var token = ScrubToken(rawToken);
var handler = new JwtSecurityTokenHandler();
try
// validates the given token and returns claims principal for user if validated.
var user = handler.ValidateToken(token, _jwtSettings.TokenValidationParameters, out SecurityToken _);
// Check if UserPolicies claims include the required the policy
if (IsRequiredPolicyExistOnUser(user.Claims ? .ToList(), requirement))
context.Succeed(requirement);
else
context.Fail();
catch(Exception e)
// TODO: Logging!
context.Fail();
return Task.CompletedTask;
private bool IsRequiredPolicyExistOnUser(List < Claim > userClaims, ExtendedAuthorizeAttribute requirement)
return userClaims != null && userClaims.Any() && userClaims.Where(x = >x.Type == "UserPolicy").Any(c = >c.Value == requirement.Policy.ToString());
private string ScrubToken(string rawToken)
return rawToken.Replace("Bearer ", "");
最后我可以在我的 api 方法上使用它,如下所示:
[HttpGet]
[Route("get/id")]
[ExtendedAuthorize(PolicyEnum.CanReadData)]
public ActionResult < AppUserDto > GetAppUser(int id)
return _appUserManager.Get(id);
它就像我想要的那样工作。但同样,令人叹为观止的问题至今仍然存在!
【讨论】:
以上是关于如何使用自定义策略模式实现 jwt 令牌基础身份验证以在 .net 核心中进行授权?的主要内容,如果未能解决你的问题,请参考以下文章
使用 IdentityServer 与创建基于 JWT 的自定义身份验证
如何自定义 ASP .NET Web API JWT 令牌响应?