【问题标题】:How can I create a custom parameter binding for a claim?如何为声明创建自定义参数绑定?
【发布时间】:2019-11-06 20:48:12
【问题描述】:

我希望能够直接在控制器的参数中提供当前用户的声明。这样我就可以在不触及 ClaimPrincipal 魔法的情况下编写单元测试。

像 [FromUri] 或 [FromBody],也许是 [FromClaim]?

我尝试按照 Microsoft 的以下文档中的说明实现 CustomModelProvider:https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-2.2

但我不知道如何提供 ClaimsPrincipal 或 List。 ValueProvider 也返回一个字符串,所以我不确定这实际上是否可行。

这是我对 ClaimModelBinder 的尝试

public class ClaimModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // TODO: Unsure, how to continue after this.

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value)) return Task.CompletedTask;

        int id = 0;
        if (!int.TryParse(value, out id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                                    modelName,
                                    "Author Id must be an integer.");
            return Task.CompletedTask;
        }

        // Model will be null if not found, including for 
        // out of range id values (0, -3, etc.)
        bindingContext.Result = ModelBindingResult.Success(null);
        return Task.CompletedTask;
    }
}

【问题讨论】:

  • 从顶部的描述来看,这听起来有点像 XY 场景,您正在尝试使用旨在解决不同问题的解决方案来解决一个问题。 ClaimsPrincipal 中没有“魔法”,实际上构建一个 ClaimsPrincipal 进行测试比您在这里尝试做的要容易和正确得多。如果您真的想完成上述操作,您可以注入 IHttpContextAccessor 并访问与请求关联的主体,但这是在操作中键入 User.Claims 的一种迂回方式
  • 嗯,我虽然将系统相关的依赖项移动到参数是一种摆脱这种情况的方法。由于一直使用 DateTime 弹出依赖项有助于使单元测试更加清晰。您能否提供“构建用于测试的 ClaimsPrincipal 比您尝试做的更容易、更正确”的来源?

标签: c# asp.net-core .net-core jwt asp.net-core-webapi


【解决方案1】:

您能否提供“为测试构建 ClaimsPrincipal 比您尝试做的更容易、更正确”的来源?

来源是我。至于我为什么这么说,是基于对how the ASP NET Core framework is written的理解,我在下面演示。

为了回答您的问题,Controller 有一个 User 属性来访问声明,当已经有一个 User 属性时,无需编写 Model Binder 来访问声明,除非您当然无法从该 User 属性访问声明,因为您的索赔逻辑不同。但你还没有提到过。

“我希望能够直接在控制器的参数中提供来自当前用户的声明。这样我就可以编写单元测试而无需触及 ClaimPrincipal 魔法。”

我将其解释为,

“我想为我的控制器编写单元测试,其中包含涉及 Claims Principal 的逻辑,但我不知道如何提供虚假的 Claims Principal 所以我将避免这种情况并改为传递方法参数”

ClaimsPrincipal 可以按如下方式不魔法。

  • Controller 有一个 User 属性,但它只能获取。 魔法
  • HttpContext 有一个 User 属性是 Get 和 Set(Nice),但 Controller.HttpContext 是 Get only(不是很好
  • Controller 有一个 ControllerContext 属性,即 Get 和 Set,ControllerContext 有一个 HttpContext 属性,即 Get 和 Set。 大奖!

这是ControllerBasesource code,这是ControllerApiController 的派生源,

public abstract class ControllerBase
{
    /* simplified below */
    public ControllerContext ControllerContext
    {
            get => _controllerContext;
            set => _controllerContext = value;
    }
    /* ... */
    public HttpContext HttpContext => ControllerContext.HttpContext;
    /* ... */
    public ClaimsPrincipal User => HttpContext?.User;
}

正如您在此处看到的,您访问的 User 是最终访问 ControllerContext.HttpContext.User 的便捷 Getter。了解这些信息后,您可以对使用 ClaimsPrincipal 的控制器进行单元测试,如下所示。

// Create a principal according to your requirements, following is exemplary
var principal = new ClaimsPrincipal(new ClaimsIdentity(new []
{
    // you might have to use ClaimTypes.Name even for JWTs issued as sub.
    new Claim(JwtRegisteredClaimNames.Sub, "1234"), 
    new Claim(JwtRegisteredClaimNames.Iss, "www.example.com"),
}, "Bearer"));

var httpContext = new DefaultHttpContext();
httpContext.User = principal;

// Fake anything you want
httpContext.Request.Headers = /* ... */

var controller = new ControllerUnderTest(...);
controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = httpContext;

// Test the action, no need to pass claims as parameters because the User property is set
var result = controller.ActionThatUsesUserClaims(...);
Assert.Something(result, expected);

这就是 ASP NET Core 每次收到真实 Web 请求时的工作方式。它确实执行上述操作以使控制器正常工作并准备好供您使用。

以上所有内容都是 public ASP NET Core api 的一部分,在没有主要版本缺陷的情况下不会发生重大更改,因此可以安全使用。事实上,这是将 ASP Net Core 与旧的 ASP NET MVC 区分开来的原因之一,旧的 ASP NET MVC 是一场噩梦,因为它没有公开上述任何内容。

说了这么多,由于某种原因我忽略了,如果你真的需要编写模型绑定器来提供声明,请注入HTTPContextAccessor。但这需要您检查方法参数和分支执行的类型。一个分支将绑定来自值提供者的属性,而另一个分支将绑定来自 HttpContext。但是,当您可以通过 0 重构完成上述操作时,为什么还要麻烦呢?

【讨论】:

  • 谢谢。我更喜欢避免弄乱代码,这个解释清楚的解决方案一次解决了两个问题;解决了问题本身,也展示了背后的逻辑。
猜你喜欢
  • 2019-10-17
  • 2014-06-07
  • 2011-02-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多