如何使用自定义策略模式实现 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 方法所需的策略。

然后我创建了新的AttributeExtendedAuthorizeAttribute,它派生自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 核心中进行授权?的主要内容,如果未能解决你的问题,请参考以下文章

使用 JWT 创建 Firebase 自定义身份验证令牌

使用 IdentityServer 与创建基于 JWT 的自定义身份验证

Laravel,如何从自定义表的 jwt 令牌获取登录用户

如何自定义 ASP .NET Web API JWT 令牌响应?

如何使 AzureAD 和自定义 JWT 令牌在 Web API 中并行工作?

ASP Core 3.0 API 令牌自定义令牌身份验证(不是 jwt!)