【问题标题】:.Net Core Api - Custom JSON Resolver based on Request Values.Net Core Api - 基于请求值的自定义 JSON 解析器
【发布时间】:2019-04-16 17:40:36
【问题描述】:

我希望所有来自我的 api 的 OkObjectResult 响应都通过我拥有的自定义 JSON 解析器运行。解析器依赖于一些特定于请求的数据——即用户的角色。它实际上类似于控制器上的 Authorize 属性,但用于从 API 传递到 UI 的数据传输对象。

我可以通过 AddJsonOptions 在配置服务中添加解析器,但它无权访问那里的用户信息。

如何将基于请求的值传递给此解析器?我在看某种自定义中间件还是其他什么?

作为示例,如果我有一个带有一些自定义属性装饰器的对象,如下所示:

public class TestObject
{
    public String Field1 => "NoRestrictions";
    [RequireRoleView("Admin")]
    public String Field2 => "ViewRequiresAdmin";
}

并以不同的角色调用我的自定义序列化程序,如下所示:

var test = new TestObject();
var userRoles = GetRoles(); // "User" for the sake of this example
var outputJson = JsonConvert.SerializeObject(test, 
                    new JsonSerializerSettings { 
                        ContractResolver = new MyCustomResolver(userRoles) 
                    });

然后输出 JSON 将跳过用户无法访问的任何内容,如下所示:

{
    "Field1":"NoRestrictions",
    // Note the absence of Field2, since it has [RequireRoleView("Admin")]
}

【问题讨论】:

  • 构造时可以将userRoles 传递给TestObject 吗?如果应用了RequireRoleViewAttribute 的所有对象都可以将userRoles 作为内部属性,那么您可以制作一个自定义合同解析器,在自定义JsonProperty.ShouldSerialize 谓词中进行必要的检查。
  • 是的,我对 ShouldSerialize 的犹豫可能会在未来发生名称更改和类似的事情 - 更改属性意味着我们需要记住更改 ShouldSerialize 等。我可能可以将 MyCustomResolver 更新为检查正在序列化的对象上的该属性,而不是在调用时需要传递给它的角色。我会四处寻找,看看情况如何。谢谢!
  • 为了清楚起见,我的意思是自定义合约解析器本身可以根据当前用户角色和RequireRoleView 的值添加合成的ShouldSerialize 谓词。 IE。逻辑类似于来自this answer 的合约解析器,但不是直接在CreateProperty 中进行检查,CreateProperty() 将添加一个ShouldSerialize 谓词来检查对象的角色。

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


【解决方案1】:

假设你有一个自定义的RequireRoleViewAttribute

[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequireRoleViewAttribute : Attribute
{
    
    public string Role;

    public RequireRoleViewAttribute(string role){
        this.Role = role;
    }
}

如何将基于请求的值传递给此解析器?

您可以在自定义解析器中注入IServiceProvider

public class RoleBasedContractResolver : DefaultContractResolver
{
    public IServiceProvider ServiceProvider { get; }
    public RoleBasedContractResolver( IServiceProvider sp)
    {
        this.ServiceProvider = sp;
    }
    
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
        var context = contextAccessor.HttpContext;
        var user = context.User;
        
       // if you're using the Identity, you can get the userManager :
       var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();

       // ...
    }
}

因此我们可以随意获取HttpContextUser。如果您使用身份,您还可以获得UserManager 服务和角色。

现在我们可以关注@dbc's advice来控制ShouldSerialize

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>() ;
        var context = contextAccessor.HttpContext;
        var user = context.User;

        // if you use the Identitiy, you can get the usermanager
        //UserManager<IdentityUser> 
        var userManager = context.RequestServices.GetRequiredService<UserManager<IdentityUser>>();

        JsonProperty property = base.CreateProperty(member, memberSerialization);

        // get the attributes
        var attrs=member.GetCustomAttributes<RequireRoleViewAttribute>();
        
        // if no [RequireResoveView] decorated, always serialize it
        if(attrs.Count()==0) {
            property.ShouldDeserialize = instance => true;
            return property;
        }

        // custom your logic to dertermine wether should serialize the property
        // I just use check if it can statisify any the condition :
        var roles = this.GetIdentityUserRolesAsync(context,userManager).Result;
        property.ShouldSerialize = instance => {
            var resource = new { /* any you need  */ };
            return attrs.Any(attr => {
                var rolename = attr.Role;
                return roles.Any(r => r == rolename ) ;
            }) ? true : false;
        };
        return property;
    }

这里的函数GetIdentityUserRolesAsync 是使用当前HttpContextUserManger 服务检索角色的辅助方法:

private async Task<IList<string>> GetIdentityUserRolesAsync(HttpContext context, UserManager<IdentityUser> userManager)
{
    var rolesCached= context.Items["__userRoles__"];
    if( rolesCached != null){
        return (IList<string>) rolesCached;
    }
    var identityUser = await userManager.GetUserAsync(context.User);
    var roles = await userManager.GetRolesAsync(identityUser);
    context.Items["__userRoles__"] = roles;
    return roles;
}

详细如何注入IServiceProvider

诀窍在于如何使用IServiceProvider 配置默认MvcJwtOptions

不要通过以下方式配置JsonOptions

services.AddMvc().
    .AddJsonOptions(o =>{
        // o. 
    });

因为它不允许我们添加 IServiceProvider 参数。

我们可以自定义MvcJsonOptions的子类:

// in .NET 3.1 and above, change this from MvcJsonOptions to MvcNewtonsoftJsonOptions
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
    IServiceProvider ServiceProvider;
    public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
    {
        this.ServiceProvider = serviceProvider;
    }
    public void Configure(MvcJsonOptions options)
    {
        options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
    }
}

并通过以下方式注册服务:

services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

// don't forget to add the IHttpContextAccessor
// in .NET 3.1 and above, change this from MvcJsonOptions to MvcNewtonsoftJsonOptions
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();
    

测试用例:

假设您有一个自定义 POCO:

public class TestObject
{
    public string Field1 => "NoRestrictions";

    [RequireRoleView("Admin")]
    public string Field2 => "ViewRequiresAdmin";

    [RequireRoleView("HR"),RequireRoleView("OP")]
    public string Field3 => "ViewRequiresHROrOP";

    [RequireRoleView("IT"), RequireRoleView("HR")]
    public string Field4 => "ViewRequiresITOrHR";

    [RequireRoleView("IT"), RequireRoleView("OP")]
    public string Field5 => "ViewRequiresITOrOP";
}

当前用户具有角色:AdminHR

结果将是:

{"Field1":"NoRestrictions","Field2":"ViewRequiresAdmin","Field3":"ViewRequiresHROrOP","Field4":"ViewRequiresITOrHR"}

使用操作方法进行测试的屏幕截图:

【讨论】:

  • 耶,它有效!我在使用 UserManager.GetRolesAsync 时遇到了一些问题(服务未注册等),所以我最终退回到通过解析声明来获取用户角色,但除此之外它非常巧妙。我还设置了一些扩展方法,所以使用它只需添加RequireRoleView属性并在Startup.ConfigureServices中调用services.AddRoleBasedContractResolver()即可。感谢你们两个:)我已经接受了你的答案,我会发布我自己的调整,以防将来有人发现它有用。
【解决方案2】:

Itminus 的答案涵盖了所需的一切,但对于任何感兴趣的人,我已经对其进行了一些扩展以便于重复使用。

首先,在类库中

我的 RequireRoleViewAttribute,它允许多个角色(OR,不是 AND):

[AttributeUsage(AttributeTargets.Property)]
public class RequireRoleViewAttribute : Attribute
{
    public List<String> AllowedRoles { get; set; }

    public RequireRoleViewAttribute(params String[] AllowedRoles) =>
        this.AllowedRoles = AllowedRoles.Select(ar => ar.ToLower()).ToList();
}

我的解析器几乎与 Itminus 的相同,但 CreateProperty 已调整为

IEnumerable<String> userRoles = this.GetIdentityUserRoles();

property.ShouldSerialize = instance =>
{
    // Check if every attribute instance has at least one role listed in the user's roles.
    return attrs.All(attr =>
                userRoles.Any(ur =>
                    attr.AllowedRoles.Any(ar => 
                        String.Equals(ar, ur, StringComparison.OrdinalIgnoreCase)))
    );
};

GetIdentityUserRoles 不使用 UserManager

private IEnumerable<String> GetIdentityUserRoles()
{
    IHttpContextAccessor contextAccessor = this.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
    HttpContext context = contextAccessor.HttpContext;
    ClaimsPrincipal user = context.User;
    Object rolesCached = context.Items["__userRoles__"];
    if (rolesCached != null)
    {
        return (List<String>)rolesCached;
    }
    var roles = ((ClaimsIdentity)user.Identity).Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
    context.Items["__userRoles__"] = roles;
    return roles;
}

我有一个扩展类,其中包含:

public static IServiceCollection AddRoleBasedContractResolver(this IServiceCollection services)
{
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddTransient<IConfigureOptions<MvcJsonOptions>, RoleBasedContractResolverOptions>();
    return services;
}

然后在我的 API 中

我引用了那个类库。在 Startup.cs -> ConfigureServices 中,我调用:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddRoleBasedContractResolver();
    ...
}

我的 DTO 被标记了属性:

public class Diagnostics
{
    public String VersionNumber { get; set; }

    [RequireRoleView("admin")]
    public Boolean ViewIfAdmin => true;

    [RequireRoleView("hr")]
    public Boolean ViewIfHr => true;

    [RequireRoleView("hr", "admin")]
    public Boolean ViewIfHrOrAdmin => true;
}

作为管理员的返回值为:

{
    "VersionNumber": "Debug",
    "ViewIfAdmin": true,
    "ViewIfHrOrAdmin": true
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多