.Net Core Api - пользовательский JSON Resolver на основе значений запроса - PullRequest
0 голосов
/ 13 ноября 2018

Мне нужно, чтобы все OkObjectResult ответы, исходящие из моего API, запускались через собственный распознаватель JSON, который у меня есть.Средство распознавания опирается на некоторые специфичные для запроса данные, а именно на роли пользователя.По сути, он похож на атрибут Authorize на контроллере, но для объектов передачи данных, передаваемых из API в пользовательский интерфейс.

Я могу добавить преобразователь в Configure Services через 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")]
}

Ответы [ 2 ]

0 голосов
/ 14 ноября 2018

Ответ Itminus охватывает все, что нужно, но для тех, кто заинтересован, я немного расширил его для легкого повторного использования.

Сначала в библиотеке классов

Мой RequireRoleViewAttribute, который допускает несколько ролей (ИЛИ, а не И):

[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
}
0 голосов
/ 14 ноября 2018

Предположим, у вас есть пользовательский 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>>();

       // ...
    }
}

Таким образом, мы можем получить HttpContext и User, как нам нравится. Если вы используете Identity, вы также можете получить службу и роли UserManager.

и теперь мы можем последовать совету @ dbc , чтобы контролировать 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 здесь является вспомогательным методом для получения ролей с использованием текущей HttpContext и службы UserManger:

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 в деталях:

Хитрость в том, как настроить значение по умолчанию MvcJwtOptions с IServiceProvider.

Не настраивайте JsonOptions:

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

, поскольку это не позволяет нам добавить параметр IServiceProvider.

Мы можем создать подкласс MvcJsonOptions:

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

А у текущего пользователя есть роли: Admin и HR:

Результат будет:

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

Скриншот тестирования методом действия:

enter image description here

...