如何使用自定义策略模式实现 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 核心中进行授权?的主要内容,如果未能解决你的问题,请参考以下文章