如何在 .net 核心中使用自定义策略模式实现基于 jwt 令牌的身份验证以进行授权?

How to implement jwt token base authentication with custom policy schema for authorization in .net core?

我正在尝试通过尝试实现基于 jwt 令牌的身份验证来在 .NET Core 3.1 中创建一个 playground 应用程序,并希望使用我的自定义策略模式(可怕吗?) .我能够使用从 .NET Framework 中的 AuthorizeAttribute 派生的自定义过滤器属性来完成这个非常舒适的 table,但是在使用 .NET Core 时遇到了困难。因为我正在使用 OnAuthorization 挂钩并正在捕获 HttpActionContext、解析令牌和检查角色策略等......但现在我正在使用 IAuthorizationHandler 我没有机会到目前为止,让它以我想要的方式工作。我阅读了很多示例和文章,但我找不到与我正在尝试做的事情相同的方法。

(PS:当我搜索了几个小时却找不到类似的方法时,这也让我感到非常紧张,因为我可能会走完全错误的路线,或者试图重新发明轮子..看看我是不是..)

我也寻找 IdendityServer4(但许多人发现它更容易管理 - 但对我来说似乎对我正在尝试做的事情有点矫枉过正。如果我错了,请怪我。)

到目前为止我所做的是,我能够在用户登录时成功创建令牌。这是代码: (如果你想知道我到底在问什么,请直接滚动到最后但如果你要回答请通读)

在幕后,我正在使用 salted-hash password in db as well as key-strecthing with 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身份tables)

一般工作流程是:

所以 "basically",我想获取令牌(将令牌与请求一起发送或在声明中发送),解决它,检查用户是否有我正在寻找的东西(特定政策等..)并以此为基础进行。

并且在 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 ..

需要策略属性:

 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);
}

令人惊叹的问题:

花了几个小时后,我终于完成了这项工作。所以我只是想 post 这作为我问题的答案,但我的“ 令人惊叹的问题 ”(请参阅​​上面我的问题的末尾)仍然存在。所以请注意,此解决方案不能保证任何这些问题。

我在上面的问题 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))));

因为示例代码将它们作为硬编码字符串一个接一个地添加,这从一开始就困扰着我,因为我想使用枚举而不是硬编码值。而且我不想在 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 方法中将我的属性用作 "requirement"。

所以我创建的属性是:

/// <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);
}

它就像我想要的那样工作。但同样,令人惊叹的问题 到现在仍然存在!