Как создать собственный AuthorizeAttribute в ASP.NET Core?

429

Я пытаюсь сделать пользовательский атрибут авторизации в ASP.NET Core. В предыдущих версиях можно было переопределить bool AuthorizeCore(HttpContextBase httpContext). Но это больше не существует в AuthorizeAttribute.

Каков текущий подход к созданию пользовательского AuthorizeAttribute?

Что я пытаюсь сделать: я получаю идентификатор сеанса в авторизации заголовка. По этому идентификатору я узнаю, является ли конкретное действие действительным.

jltrem
источник
Я не уверен, как это сделать, но MVC с открытым исходным кодом. Вы можете получить репозиторий github и искать реализации IAuthorizationFilter. Если у меня будет время сегодня, я буду искать вас и публиковать фактический ответ, но без обещаний. репозиторий github: github.com/aspnet/Mvc
bopapa_1979
Хорошо, вне времени, но ищите использование AuthorizationPolicy в Repo MVC, который использует AuthorizeAttribute, в репозитории aspnet / Security, здесь: github.com/aspnet/Security . В качестве альтернативы, посмотрите в репозитории MVC пространство имен, в котором, по-видимому, находятся ваши средства обеспечения безопасности, а именно Microsoft.AspNet.Authorization. Извините, я не могу быть более полезным. Удачи!
bopapa_1979

Ответы:

446

Подход, рекомендованный основной группой ASP.Net, заключается в использовании нового дизайна политики, который полностью описан здесь . Основная идея нового подхода заключается в использовании нового атрибута [Authorize] для обозначения «политики» (например, [Authorize( Policy = "YouNeedToBe18ToDoThis")]когда политика зарегистрирована в Startup.cs приложения для выполнения некоторого блока кода (т. Е. Обеспечения того, чтобы у пользователя было требование о возрасте). где возраст 18 лет и старше).

Разработка политики является отличным дополнением к этой структуре, и команда разработчиков ASP.Net Security Core заслуживает похвалы за ее внедрение. Тем не менее, это не подходит для всех случаев. Недостатком этого подхода является то, что он не обеспечивает удобного решения для наиболее распространенной необходимости простого утверждения, что для данного контроллера или действия требуется данный тип заявки. В случае, когда приложение может иметь сотни отдельных разрешений, управляющих операциями CRUD на отдельных ресурсах REST («CanCreateOrder», «CanReadOrder», «CanUpdateOrder», «CanDeleteOrder» и т. Д.), Новый подход требует либо повторяющихся однозначных одно сопоставление между именем политики и именем заявки (например,options.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));) или написание некоторого кода для выполнения этих регистраций во время выполнения (например, прочитать все типы утверждений из базы данных и выполнить вышеупомянутый вызов в цикле). Проблема с этим подходом для большинства случаев состоит в том, что это ненужные накладные расходы.

Хотя группа ASP.Net Core Security рекомендует никогда не создавать свое собственное решение, в некоторых случаях это может быть наиболее разумным вариантом для начала.

Ниже приведена реализация, которая использует IAuthorizationFilter, чтобы предоставить простой способ выражения требования заявки для данного контроллера или действия:

public class ClaimRequirementAttribute : TypeFilterAttribute
{
    public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
    {
        Arguments = new object[] {new Claim(claimType, claimValue) };
    }
}

public class ClaimRequirementFilter : IAuthorizationFilter
{
    readonly Claim _claim;

    public ClaimRequirementFilter(Claim claim)
    {
        _claim = claim;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
        if (!hasClaim)
        {
            context.Result = new ForbidResult();
        }
    }
}


[Route("api/resource")]
public class MyController : Controller
{
    [ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
    [HttpGet]
    public IActionResult GetResource()
    {
        return Ok();
    }
}
Дерек Грир
источник
80
Это должно быть помечено как ПРАВИЛЬНЫЙ ОТВЕТ. Здесь вы видите, как сотрудники Microsoft учитывают отзывы разработчиков. Я не понимаю причину, по которой они так «закрыты», потому что очень часто встречается множество разных разрешений, когда приходится кодировать одну политику для каждой, что является полным излишним. Я искал это так долго ... (Я уже задавал этот вопрос почти два года назад, когда vNext все еще держал пари здесь: stackoverflow.com/questions/32181400/… но мы все еще застряли там)
Vi100
3
Это хорошая вещь. У нас есть промежуточное программное обеспечение для аутентификации в Web API, но более детальная защита разрешений авторизации по ролям; поэтому просто добавить атрибут вроде: [MyAuthorize (MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] выглядит очень хорошо.
Мариано Пейнадор
4
@Derek Greer: это лучший ответ. Однако вы реализуете ActionFilter, который запускается после Authorize Action Filter. Есть ли способ реализовать и авторизовать фильтр действий?
Джейкоб Фэн
6
@JacobPhan Вы правы, это было бы лучше реализовать с помощью интерфейса IAuthorizationFilter. Я обновил код, чтобы отразить изменения.
Дерек Грир,
3
так new ForbidResult()не работает (вызывает исключение / 500), потому что у него нет связанной схемы авторизации. Что бы я использовал для этого случая?
Синастетическая
253

Я сотрудник службы безопасности asp.net. Во-первых, позвольте мне извиниться, что ничего из этого еще не задокументировано за пределами образца музыкального магазина или модульных тестов, и все это все еще дорабатывается с точки зрения открытых API. Подробная документация здесь .

Мы не хотим, чтобы вы писали пользовательские атрибуты авторизации. Если вам нужно сделать это, мы сделали что-то не так. Вместо этого вы должны писать требования авторизации .

Авторизация действует в соответствии с идентификационными данными. Личности создаются путем аутентификации.

В комментариях вы говорите, что хотите проверить идентификатор сессии в заголовке. Ваш идентификатор сессии будет основой для идентификации. Если вы хотите использовать Authorizeатрибут, вы должны написать промежуточное программное обеспечение для аутентификации, чтобы взять этот заголовок и превратить его в аутентифицированный ClaimsPrincipal. Затем вы проверите это внутри требования авторизации. Требования к авторизации могут быть настолько сложными, насколько вам нравится, например, вот тот, который берет заявление о дате рождения на текущую личность и разрешает, если пользователь старше 18 лет;

public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
        public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
        {
            if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
            {
                context.Fail();
                return;
            }

            var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
            int age = DateTime.Today.Year - dateOfBirth.Year;
            if (dateOfBirth > DateTime.Today.AddYears(-age))
            {
                age--;
            }

            if (age >= 18)
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
        }
    }
}

Тогда в вашей ConfigureServices()функции вы бы подключить его

services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", 
        policy => policy.Requirements.Add(new Authorization.Over18Requirement()));
});

И наконец, примените его к контроллеру или методу действия с

[Authorize(Policy = "Over18")]
blowdart
источник
84
Интересно ... как бы с этим реализовать тонкозернистый контроль доступа? Допустим, ManageStoreобразец требования от Music Store. Как видно из примера, существует только один способ «разрешить все или ничего». Должны ли мы создавать новую политику для каждой возможной перестановки? то есть «Пользователи / Чтение», «Пользователи / Создать», «Пользователи / AssignRole», «Пользователи / Удалить», если мы хотим детализированные утверждения? Звучит так, как будто все работает, чтобы заставить его работать, и изобилие политик для управления претензиями, а не [ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]атрибутами?
Ценг
84
Я должен прокомментировать, что все это сложнее, чем реализация пользовательского метода авторизации. Я знаю, как я хочу, чтобы авторизация была выполнена, я мог просто пойти и написать ее в MVC 5, в MVC 6 они добавляют много «готового» кода, который на самом деле более сложен для понимания, чем реализация самой основной «вещи». Заставляю меня сидеть перед страницей, пытаясь что-то выяснить, вместо того, чтобы писать код насквозь, и это также большая боль для людей, которые используют СУБД, отличные от Microsoft (или No-Sql).
Felype
17
С моей точки зрения, это не решает все сценарии. До MVC 6 я использовал собственный атрибут авторизации для реализации своей собственной «системы разрешений». Я мог бы добавить атрибут Authorize ко всем действиям и передать одно конкретное необходимое разрешение (как Enum-Value). Само разрешение было сопоставлено группам / пользователям в БД. Итак, я не вижу способа справиться с этим с помощью политики !?
Гервальд
43
Я, как и многие другие в этих комментариях, очень разочарован тем, что использование атрибутов для авторизации было настолько сильно кастрировано по сравнению с тем, что было возможно в Web API 2. Извините, ребята, но ваша абстракция «требования» не покрывает любой случай, когда мы могли ранее использовать параметры конструктора атрибута для информирования об основном алгоритме авторизации. Раньше было просто умом делать что-то подобное [CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]. Я мог бы использовать один пользовательский атрибут бесконечным количеством способов, просто изменив параметры конструктора.
NathanAldenSr
61
Я также шокирован тем, что самопровозглашенный «ведущий специалист по безопасности ASP.NET» на самом деле предлагает использовать волшебные строки (взломать смысл IAuthorizeData.Policy) и настраиваемые поставщики политик для преодоления этого вопиющего упущения, а не решать его в рамках. Я думал, что мы не должны создавать наши собственные реализации? У некоторых из нас не было выбора, кроме как заново внедрить авторизацию с нуля (опять же), и на этот раз даже без преимущества старого Authorizeатрибута Web API . Теперь мы должны сделать это на уровне фильтра действий или промежуточного программного обеспечения.
NathanAldenSr
104

Кажется, что с ASP.NET Core 2 вы можете снова наследовать AuthorizeAttribute, вам просто нужно также реализовать IAuthorizationFilter(или IAsyncAuthorizationFilter):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private readonly string _someFilterParameter;

    public CustomAuthorizeAttribute(string someFilterParameter)
    {
        _someFilterParameter = someFilterParameter;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // you can also use registered services
        var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

        var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
        if (!isAuthorized)
        {
            context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }
    }
}
Gius
источник
4
То есть вы можете использовать это только для отказа в авторизации, а не для ее предоставления ?
MEMark
1
@MEMark Предоставляя , вы имеете в виду переопределение другого атрибута авторизации?
Гиус
2
AFAIK, доступ разрешен по умолчанию, поэтому вам нужно явно запретить его (например, добавив AuthorizeAttribute). Проверьте этот вопрос для более подробной информации: stackoverflow.com/questions/17272422/…
gius
16
Также обратите внимание, что в предложенном примере не нужно наследовать от AuthorizeAttribute. Вы можете наследовать от Attribute и IAuthorizationFilter . Таким образом, вы не получите следующее исключение, если используется какой-то нестандартный механизм аутентификации: InvalidOperationException: не был указан authenticationScheme, а DefaultChallengeScheme не найден.
Анатольевич
13
Обратите внимание, что если вашей OnAuthorizationреализации требуется ожидание асинхронного метода, вы должны реализовать ее, IAsyncAuthorizationFilterв IAuthorizationFilterпротивном случае ваш фильтр будет выполняться синхронно, а действие вашего контроллера будет выполняться независимо от результата фильтра.
Codemunkie
34

Основываясь на ответе Дерека Грира GREAT , я сделал это с помощью перечислений.

Вот пример моего кода:

public enum PermissionItem
{
    User,
    Product,
    Contact,
    Review,
    Client
}

public enum PermissionAction
{
    Read,
    Create,
}


public class AuthorizeAttribute : TypeFilterAttribute
{
    public AuthorizeAttribute(PermissionItem item, PermissionAction action)
    : base(typeof(AuthorizeActionFilter))
    {
        Arguments = new object[] { item, action };
    }
}

public class AuthorizeActionFilter : IAuthorizationFilter
{
    private readonly PermissionItem _item;
    private readonly PermissionAction _action;
    public AuthorizeActionFilter(PermissionItem item, PermissionAction action)
    {
        _item = item;
        _action = action;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        bool isAuthorized = MumboJumboFunction(context.HttpContext.User, _item, _action); // :)

        if (!isAuthorized)
        {
            context.Result = new ForbidResult();
        }
    }
}

public class UserController : BaseController
{
    private readonly DbContext _context;

    public UserController( DbContext context) :
        base()
    {
        _logger = logger;
    }

    [Authorize(PermissionItem.User, PermissionAction.Read)]
    public async Task<IActionResult> Index()
    {
        return View(await _context.User.ToListAsync());
    }
}
bruno.almeida
источник
1
Спасибо за это. Я создал этот пост с немного другой реализацией и запросом на проверку stackoverflow.com/questions/49551047/…
Антон Сваневелдер
2
MumboJumboFunction <3
Марек Урбанович
31

Вы можете создать свой собственный AuthorizationHandler, который будет находить пользовательские атрибуты в ваших контроллерах и действиях и передавать их в метод HandleRequirementAsync.

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();

        var action = (context.Resource as AuthorizationFilterContext)?.ActionDescriptor as ControllerActionDescriptor;
        if (action != null)
        {
            attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
            attributes.AddRange(GetAttributes(action.MethodInfo));
        }

        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
    {
        return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
    }
}

Затем вы можете использовать его для любых пользовательских атрибутов, которые вам нужны на ваших контроллерах или действиях. Например, чтобы добавить требования разрешения. Просто создайте свой собственный атрибут.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : AuthorizeAttribute
{
    public string Name { get; }

    public PermissionAttribute(string name) : base("Permission")
    {
        Name = name;
    }
}

Затем создайте требование для добавления в вашу политику

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    //Add any custom requirement properties if you have them
}

Затем создайте AuthorizationHandler для своего пользовательского атрибута, унаследовав AttributeAuthorizationHandler, который мы создали ранее. Он будет передан IEnumerable для всех ваших пользовательских атрибутов в методе HandleRequirementsAsync, собранных из вашего контроллера и Action.

public class PermissionAuthorizationHandler : AttributeAuthorizationHandler<PermissionAuthorizationRequirement, PermissionAttribute>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement, IEnumerable<PermissionAttribute> attributes)
    {
        foreach (var permissionAttribute in attributes)
        {
            if (!await AuthorizeAsync(context.User, permissionAttribute.Name))
            {
                return;
            }
        }

        context.Succeed(requirement);
    }

    private Task<bool> AuthorizeAsync(ClaimsPrincipal user, string permission)
    {
        //Implement your custom user permission logic here
    }
}

И наконец, в вашем методе Startup.cs ConfigureServices добавьте свой собственный AuthorizationHandler в службы и добавьте свою Политику.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Permission", policyBuilder =>
            {
                policyBuilder.Requirements.Add(new PermissionAuthorizationRequirement());
            });
        });

Теперь вы можете просто украсить свои контроллеры и действия с помощью пользовательского атрибута.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}
Шон
источник
1
Я посмотрю на это как можно скорее.
NathanAldenSr
5
Это довольно сложная задача ... Я решил то же самое, используя простой AuthorizationFilterAttribute, который получает параметр. Для этого не нужно размышлять, оно кажется даже более искусным, чем «официальное» решение (которое я считаю довольно плохим).
Vi100
2
@ Vi100 Я не смог найти много информации об AuthorizationFilters в ASP.NET Core. На официальной странице документации написано, что они работают над этой темой. docs.microsoft.com/en-us/aspnet/core/security/authorization/…
Шон
4
@ Vi100 Можете ли вы поделиться своим решением, если есть более простой способ добиться этого, я хотел бы знать.
Шон
2
Стоит отметить, что использование UnderlyingSystemType выше не компилируется, но его удаление, похоже, работает.
чаепитие
25

Каков текущий подход к созданию пользовательского атрибута AuthorizeAttribute?

Легко: не создавай свой AuthorizeAttribute.

Для сценариев с чистой авторизацией (например, ограничение доступа только для определенных пользователей) рекомендуется использовать новый блок авторизации: https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#84 -L92

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<AuthorizationOptions>(options =>
        {
            options.AddPolicy("ManageStore", policy => policy.RequireClaim("Action", "ManageStore"));
        });
    }
}

public class StoreController : Controller
{
    [Authorize(Policy = "ManageStore"), HttpGet]
    public async Task<IActionResult> Manage() { ... }
}

Для аутентификации лучше всего работать на уровне промежуточного программного обеспечения.

Чего именно вы пытаетесь достичь?

Кевин Шале
источник
1
Я получаю идентификатор сеанса в авторизации заголовка. По этому идентификатору я узнаю, является ли конкретное действие действительным.
jltrem
1
Тогда это не проблема авторизации. Я предполагаю, что ваш «идентификатор сессии» на самом деле является токеном, содержащим личность вызывающего абонента: это определенно должно быть сделано на уровне промежуточного программного обеспечения.
Кевин Шале
3
Это не аутентификация (определение того, кто пользователь), а авторизация (определение того, должен ли пользователь иметь доступ к ресурсу). Так где ты предлагаешь мне посмотреть, чтобы решить это?
jltrem
3
@jltrem, согласен, ты имеешь ввиду авторизацию, а не аутентификацию.
bopapa_1979
2
@ Точно не знаю. Я запрашиваю другую систему для этой информации. Эта система аутентифицирует (определяет пользователя) и авторизует (говорит мне, что этот пользователь может получить доступ). Прямо сейчас я взломал его для работы, вызвав метод в каждом действии контроллера, чтобы другая система проверила сеанс. Я хотел бы, чтобы это автоматически происходило через атрибут.
jltrem
4

Если кто-то просто хочет проверить токен на этапе авторизации, используя текущие методы обеспечения безопасности, которые вы можете,

добавьте это в свой Startup / ConfigureServices

    services.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();

    services.AddAuthorization(options => options.AddPolicy("Bearer",
        policy => policy.AddRequirements(new BearerRequirement())
        )
    );

и это в вашей кодовой базе,

public class BearerRequirement : IAuthorizationRequirement
{
    public async Task<bool> IsTokenValid(SomeValidationContext context, string token)
    {
        // here you can check if the token received is valid 
        return true;
    }
}

public class BearerAuthorizationHandler : AuthorizationHandler<BearerRequirement> 
{

    public BearerAuthorizationHandler(SomeValidationContext thatYouCanInject)
    {
       ...
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BearerRequirement requirement)
    {
        var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
        string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.Contains("Bearer"))
        {
            var token = authHeader.Replace("Bearer ", string.Empty);
            if (await requirement.IsTokenValid(thatYouCanInject, token))
            {
                context.Succeed(requirement);
            }
        }
    }
}

Если код не доходит, context.Succeed(...)он все равно потерпит неудачу (401).

И тогда в ваших контроллерах вы можете использовать

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Габриэль П.
источник
Почему вы решили выполнить собственную проверку токена, если промежуточное ПО JwtBearer уже позаботилось об этом? Он также помещает правильное содержимое в заголовок ответа WWW-Authenticate для ошибки проверки подлинности / истечения срока действия токена. Если вы хотите получить доступ к конвейеру аутентификации, есть определенные события, которые вы можете использовать в опциях AddJwtBearer (OnAuthenticationFailed, OnChallenge, OnMessageReceived и OnTokenValidated).
Даррен Льюис
Это бесконечно проще, чем любое другое решение, которое я видел. Специально для простых случаев использования ключей API. Одно обновление: для 3.1 приведение к AuthorizationFilterContext больше не действует из-за маршрутизации конечной точки. Вам нужно получить контекст через HttpContextAccessor.
Джейсон Кодер
2

Современный способ - АутентификацияHandlers

в startup.cs добавить

services.AddAuthentication("BasicAuthentication").AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserService _userService;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IUserService userService)
            : base(options, logger, encoder, clock)
        {
            _userService = userService;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Missing Authorization Header");

            User user = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                var username = credentials[0];
                var password = credentials[1];
                user = await _userService.Authenticate(username, password);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }

            if (user == null)
                return AuthenticateResult.Fail("Invalid User-name or Password");

            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }

IUserService - это сервис, который вы делаете, где у вас есть имя пользователя и пароль. в основном он возвращает класс пользователя, который вы используете для сопоставления ваших утверждений.

var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            }; 

Затем вы можете запросить эти утверждения и любые сопоставленные вами данные, а их немало, взгляните на класс ClaimTypes.

Вы можете использовать это в методе расширения, чтобы получить любое из отображений

public int? GetUserId()
{
   if (context.User.Identity.IsAuthenticated)
    {
       var id=context.User.FindFirst(ClaimTypes.NameIdentifier);
       if (!(id is null) && int.TryParse(id.Value, out var userId))
            return userId;
     }
      return new Nullable<int>();
 }

Этот новый способ, я думаю, лучше, чем

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.Headers.Authorization != null)
        {
            var authToken = actionContext.Request.Headers.Authorization.Parameter;
            // decoding authToken we get decode value in 'Username:Password' format
            var decodeauthToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
            // spliting decodeauthToken using ':'
            var arrUserNameandPassword = decodeauthToken.Split(':');
            // at 0th postion of array we get username and at 1st we get password
            if (IsAuthorizedUser(arrUserNameandPassword[0], arrUserNameandPassword[1]))
            {
                // setting current principle
                Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(arrUserNameandPassword[0]), null);
            }
            else
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            }
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }

    public static bool IsAuthorizedUser(string Username, string Password)
    {
        // In this method we can handle our database logic here...
        return Username.Equals("test") && Password == "test";
    }
}
Уолтер Веховен
источник
Этот блестящий ответ просто работает как шарм! Спасибо вам за это, и я желаю вам, чтобы за него проголосовали, так как это лучший ответ, который я нашел после шести часов поиска в блогах, документации и стеке для обычной аутентификации плюс авторизация на основе ролей.
Петр Срудка
@ PiotrŚródka, пожалуйста, обратите внимание, что ответ немного «упрощен», проверьте, есть ли в тексте символ «:», так как злоумышленник может попытаться сбить ваш сервис, просто не сыграв в конце с хорошим индексом. исключение диапазона. как всегда проверяйте то, что вам дают внешние источники
Уолтер Веховен
2

На момент написания этой статьи я считаю, что это можно сделать с помощью интерфейса IClaimsTransformation в asp.net core 2 и выше. Я только что реализовал подтверждение концепции, которое достаточно доступно для публикации здесь.

public class PrivilegesToClaimsTransformer : IClaimsTransformation
{
    private readonly IPrivilegeProvider privilegeProvider;
    public const string DidItClaim = "http://foo.bar/privileges/resolved";

    public PrivilegesToClaimsTransformer(IPrivilegeProvider privilegeProvider)
    {
        this.privilegeProvider = privilegeProvider;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity is ClaimsIdentity claimer)
        {
            if (claimer.HasClaim(DidItClaim, bool.TrueString))
            {
                return principal;
            }

            var privileges = await this.privilegeProvider.GetPrivileges( ... );
            claimer.AddClaim(new Claim(DidItClaim, bool.TrueString));

            foreach (var privilegeAsRole in privileges)
            {
                claimer.AddClaim(new Claim(ClaimTypes.Role /*"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" */, privilegeAsRole));
            }
        }

        return principal;
    }
}

Чтобы использовать это в вашем контроллере, просто добавьте соответствующие [Authorize(Roles="whatever")]методы.

[HttpGet]
[Route("poc")]
[Authorize(Roles = "plugh,blast")]
public JsonResult PocAuthorization()
{
    var result = Json(new
    {
        when = DateTime.UtcNow,
    });

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

В нашем случае каждый запрос включает заголовок авторизации, который является JWT. Это прототип, и я думаю, что мы сделаем что-то очень близкое к этому в нашей производственной системе на следующей неделе.

Будущие избиратели, при голосовании учитывайте дату написания. На сегодняшний день, это works on my machine.™ Вы, вероятно, захотите больше обработки ошибок и регистрации в вашей реализации.

Нет возврата Нет возврата
источник
Как насчет ConfigureServices? Нужно ли что-то добавить?
Даниил
Как обсуждалось в другом месте, да.
Нет возврата нет возврата
1

Для авторизации в нашем приложении. Мы должны были вызвать сервис на основе параметров, переданных в атрибуте авторизации.

Например, если мы хотим проверить, может ли вошедший в систему врач просматривать записи о пациентах, мы передадим «View_Appointment» для настраиваемого атрибута авторизации и проверим это право в службе БД, и на основании результатов мы будем проверять. Вот код для этого сценария:

    public class PatientAuthorizeAttribute : TypeFilterAttribute
    {
    public PatientAuthorizeAttribute(params PatientAccessRights[] right) : base(typeof(AuthFilter)) //PatientAccessRights is an enum
    {
        Arguments = new object[] { right };
    }

    private class AuthFilter : IActionFilter
    {
        PatientAccessRights[] right;

        IAuthService authService;

        public AuthFilter(IAuthService authService, PatientAccessRights[] right)
        {
            this.right = right;
            this.authService = authService;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var allparameters = context.ActionArguments.Values;
            if (allparameters.Count() == 1)
            {
                var param = allparameters.First();
                if (typeof(IPatientRequest).IsAssignableFrom(param.GetType()))
                {
                    IPatientRequest patientRequestInfo = (IPatientRequest)param;
                    PatientAccessRequest userAccessRequest = new PatientAccessRequest();
                    userAccessRequest.Rights = right;
                    userAccessRequest.MemberID = patientRequestInfo.PatientID;
                    var result = authService.CheckUserPatientAccess(userAccessRequest).Result; //this calls DB service to check from DB
                    if (result.Status == ReturnType.Failure)
                    {
                        //TODO: return apirepsonse
                        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
                    }
                }
                else
                {
                    throw new AppSystemException("PatientAuthorizeAttribute not supported");
                }
            }
            else
            {
                throw new AppSystemException("PatientAuthorizeAttribute not supported");
            }
        }
    }
}

И в действии API мы используем это так:

    [PatientAuthorize(PatientAccessRights.PATIENT_VIEW_APPOINTMENTS)] //this is enum, we can pass multiple
    [HttpPost]
    public SomeReturnType ViewAppointments()
    {

    }
Абдулла
источник
1
Обратите внимание, что IActionFilter будет проблемой, когда вы захотите использовать один и тот же атрибут для методов-концентраторов в SignalR.SignalR Хабы ожидают IAuthorizationFilter
ilkerkaran
Спасибо за информацию. Я не использую SignalR в своем приложении прямо сейчас, поэтому я не тестировал его с ним.
Абдулла
Я полагаю, тот же принцип, поскольку вам все равно придется использовать запись авторизации заголовка, реализация будет отличаться
Уолтер Веховен
0

Принятый ответ ( https://stackoverflow.com/a/41348219/4974715 ) не является реально поддерживаемым или подходящим, поскольку в качестве утверждения используется «CanReadResource» (но, по сути, это должна быть политика в действительности, IMO). Подход к ответу не подходит в том смысле, в котором он использовался, потому что если метод действия требует много разных настроек утверждений, то с этим ответом вам придется многократно писать что-то вроде ...

[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")] 
[ClaimRequirement(MyClaimTypes.AnotherPermision, "AnotherClaimVaue")]
//and etc. on a single action.

Итак, представьте, сколько потребуется кодирования. В идеале «CanReadResource» должен быть политикой, которая использует много утверждений, чтобы определить, может ли пользователь прочитать ресурс.

Что я делаю, я создаю свою политику как перечисление, а затем перебираю и устанавливаю требования следующим образом ...

services.AddAuthorization(authorizationOptions =>
        {
            foreach (var policyString in Enum.GetNames(typeof(Enumerations.Security.Policy)))
            {
                authorizationOptions.AddPolicy(
                    policyString,
                    authorizationPolicyBuilder => authorizationPolicyBuilder.Requirements.Add(new DefaultAuthorizationRequirement((Enumerations.Security.Policy)Enum.Parse(typeof(Enumerations.Security.Policy), policyWrtString), DateTime.UtcNow)));

      /* Note that thisn does not stop you from 
          configuring policies directly against a username, claims, roles, etc. You can do the usual.
     */
            }
        }); 

Класс DefaultAuthorizationRequirement выглядит так ...

public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
    public Enumerations.Security.Policy Policy {get; set;} //This is a mere enumeration whose code is not shown.
    public DateTime DateTimeOfSetup {get; set;} //Just in case you have to know when the app started up. And you may want to log out a user if their profile was modified after this date-time, etc.
}

public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
    private IAServiceToUse _aServiceToUse;

    public DefaultAuthorizationHandler(
        IAServiceToUse aServiceToUse
        )
    {
        _aServiceToUse = aServiceToUse;
    }

    protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
    {
        /*Here, you can quickly check a data source or Web API or etc. 
           to know the latest date-time of the user's profile modification...
        */
        if (_aServiceToUse.GetDateTimeOfLatestUserProfileModication > requirement.DateTimeOfSetup)
        {
            context.Fail(); /*Because any modifications to user information, 
            e.g. if the user used another browser or if by Admin modification, 
            the claims of the user in this session cannot be guaranteed to be reliable.
            */
            return;
        }

        bool shouldSucceed = false; //This should first be false, because context.Succeed(...) has to only be called if the requirement specifically succeeds.

        bool shouldFail = false; /*This should first be false, because context.Fail() 
        doesn't have to be called if there's no security breach.
        */

        // You can do anything.
        await doAnythingAsync();

       /*You can get the user's claims... 
          ALSO, note that if you have a way to priorly map users or users with certain claims 
          to particular policies, add those policies as claims of the user for the sake of ease. 
          BUT policies that require dynamic code (e.g. checking for age range) would have to be 
          coded in the switch-case below to determine stuff.
       */

        var claims = context.User.Claims;

        // You can, of course, get the policy that was hit...
        var policy = requirement.Policy

        //You can use a switch case to determine what policy to deal with here...
        switch (policy)
        {
            case Enumerations.Security.Policy.CanReadResource:
                 /*Do stuff with the claims and change the 
                     value of shouldSucceed and/or shouldFail.
                */
                 break;
            case Enumerations.Security.Policy.AnotherPolicy:
                 /*Do stuff with the claims and change the 
                    value of shouldSucceed and/or shouldFail.
                 */
                 break;
                // Other policies too.

            default:
                 throw new NotImplementedException();
        }

        /* Note that the following conditions are 
            so because failure and success in a requirement handler 
            are not mutually exclusive. They demand certainty.
        */

        if (shouldFail)
        {
            context.Fail(); /*Check the docs on this method to 
            see its implications.
            */
        }                

        if (shouldSucceed)
        {
            context.Succeed(requirement); 
        } 
     }
}

Обратите внимание, что приведенный выше код может также включить предварительное сопоставление пользователя с политикой в ​​вашем хранилище данных. Таким образом, при составлении заявок для пользователя вы в основном извлекаете политики, которые были предварительно сопоставлены с пользователем прямо или косвенно (например, потому что у пользователя есть определенное значение заявки, и это значение заявки было идентифицировано и сопоставлено с политикой, например, что он обеспечивает автоматическое сопоставление для пользователей, которые также имеют это значение утверждения), и включает политики в качестве утверждений, так что в обработчике авторизации вы можете просто проверить, содержат ли утверждения пользователя требование. Политика в качестве элемента значения утверждения в их претензии. Это для статического способа выполнения требования политики, например, требование «Имя» носит статический характер. Так,

[Authorize(Policy = nameof(Enumerations.Security.Policy.ViewRecord))] 

Динамическое требование может касаться проверки возрастного диапазона и т. Д., А политики, в которых используются такие требования, не могут быть предварительно сопоставлены пользователям.

Пример проверки заявок на динамическую политику (например, чтобы проверить, не старше ли пользователя 18 лет) уже находится на ответе, заданном @blowdart ( https://stackoverflow.com/a/31465227/4974715 ).

PS: я набрал это на моем телефоне. Простите за любые опечатки и отсутствие форматирования.

Olumide
источник