Jak utworzyć niestandardowy atrybut AuthorizeAttribute w programie ASP.NET Core?


428

Próbuję utworzyć niestandardowy atrybut autoryzacji w programie ASP.NET Core. W poprzednich wersjach można było zastąpić bool AuthorizeCore(HttpContextBase httpContext). Ale to już nie istnieje AuthorizeAttribute.

Jakie jest obecne podejście do tworzenia niestandardowego atrybutu autoryzacji?

Co próbuję osiągnąć: otrzymuję identyfikator sesji w Autoryzacji nagłówka. Z tego identyfikatora będę wiedział, czy dana akcja jest poprawna.


Nie jestem pewien, jak to zrobić, ale MVC jest oprogramowaniem typu open source. Możesz pobrać repozytorium github i poszukać implementacji IAuthorizationFilter. Jeśli będę miał dzisiaj czas, poszukam cię i opublikuję prawdziwą odpowiedź, ale żadnych obietnic. github repo: github.com/aspnet/Mvc
bopapa_1979

OK, poza czasem, ale poszukaj zastosowań AuthorizationPolicy w repozytorium MVC, które używa AuthorizeAttribute, w repozytorium aspnet / Security, tutaj: github.com/aspnet/Security . Alternatywnie, poszukaj w repozytorium MVC przestrzeni nazw, w której wydają się znajdować zabezpieczenia, na których Ci zależy, czyli Microsoft.AspNet.Authorization. Przepraszam, nie mogę być bardziej pomocny. Powodzenia!
bopapa_1979

Odpowiedzi:


446

Podejście zalecane przez zespół ASP.Net Core polega na wykorzystaniu nowego projektu polityki, który jest tutaj w pełni udokumentowany . Podstawową ideą nowego podejścia jest użycie nowego atrybutu [Autoryzuj], aby wyznaczyć „zasadę” (np. Gdy [Authorize( Policy = "YouNeedToBe18ToDoThis")]zasada jest zarejestrowana w Startup.cs aplikacji w celu wykonania jakiegoś bloku kodu (tj. Zapewnienia, że ​​użytkownik ma roszczenie wiekowe w wieku 18 lat lub starszych).

Projekt polityki jest doskonałym dodatkiem do frameworka, a zespół ASP.Net Security Core powinien być pochwalony za jego wprowadzenie. To powiedziawszy, nie jest odpowiednie dla wszystkich przypadków. Wadą tego podejścia jest to, że nie zapewnia ono wygodnego rozwiązania najczęstszej potrzeby zwykłego stwierdzenia, że ​​dany kontroler lub działanie wymaga określonego typu roszczenia. W przypadku, gdy aplikacja może mieć setki dyskretnych uprawnień rządzących operacjami CRUD na poszczególnych zasobach REST („CanCreateOrder”, „CanReadOrder”, „CanUpdateOrder”, „CanDeleteOrder” itp.), Nowe podejście wymaga powtarzalnych czynności jeden do jednego jedno odwzorowanie między nazwą polisy a nazwą roszczenia (npoptions.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));) lub napisanie kodu w celu wykonania tych rejestracji w czasie wykonywania (np. odczytanie wszystkich typów oświadczeń z bazy danych i wykonanie wyżej wspomnianego wywołania w pętli). Problem z tym podejściem w większości przypadków polega na tym, że jest to niepotrzebny narzut.

Chociaż zespół ASP.Net Core Security zaleca, aby nigdy nie tworzyć własnego rozwiązania, w niektórych przypadkach może to być najbardziej rozważna opcja, od której można zacząć.

Poniżej przedstawiono implementację, która wykorzystuje IAuthorizationFilter w celu zapewnienia prostego sposobu wyrażenia wymogu dotyczącego roszczenia dla danego kontrolera lub działania:

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

78
Należy to oznaczyć jako PRAWIDŁOWĄ ODPOWIEDŹ. Tutaj widzisz, jak ludzie w Microsoft oceniają opinie programistów. Nie rozumiem powodu, dla którego są tak „zamknięci w umyśle”, ponieważ bardzo często zdarza się, że ma się wiele różnych uprawnień, a kodowanie jednej polityki dla każdego z nich jest całkowitą przesadą. Szukałem tego przez tak długi czas ... (zadałem już to pytanie prawie dwa lata temu, kiedy vNext wciąż był tutaj zakładem: stackoverflow.com/questions/32181400/... ale wciąż tam utknęliśmy)
Vi100

3
To dobre rzeczy. Mamy oprogramowanie pośredniczące do uwierzytelniania w interfejsie API sieci Web, ale zapewniamy większe bezpieczeństwo uprawnień do autoryzacji według roli; więc po prostu wrzuć atrybut, taki jak: [MyAuthorize (MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] wygląda bardzo dobrze.
Mariano Peinador,

4
@Derek Greer: To najlepsza odpowiedź. Wdrażasz jednak filtr ActionFilter, który działa po autoryzacji filtru akcji. Czy istnieje sposób na wdrożenie i autoryzację filtru akcji?
Jacob Phan,

6
@JacobPhan Masz rację, lepiej byłoby to zaimplementować przy użyciu interfejsu IAuthorizationFilter. Zaktualizowałem kod, aby odzwierciedlić zmiany.
Derek Greer,

3
więc new ForbidResult()nie działa (powoduje wyjątek / 500), ponieważ nie ma powiązanego schematu autoryzacji. Z czego skorzystałbym w tej sprawie?
Sinaesthetic

252

Jestem osobą zabezpieczającą asp.net. Po pierwsze, przepraszam, że nic z tego nie jest jeszcze udokumentowane poza próbkami lub testami jednostkowymi w sklepie muzycznym, a wszystko to jest wciąż udoskonalane pod względem odsłoniętych interfejsów API. Szczegółowa dokumentacja jest tutaj .

Nie chcemy, abyś pisał niestandardowe atrybuty autoryzacji. Jeśli musisz to zrobić, zrobiliśmy coś złego. Zamiast tego powinieneś pisać wymagania dotyczące autoryzacji .

Autoryzacja działa na Tożsamości. Tożsamości są tworzone przez uwierzytelnianie.

W komentarzach mówisz, że chcesz sprawdzić identyfikator sesji w nagłówku. Twój identyfikator sesji będzie podstawą tożsamości. Jeśli chcesz użyć Authorizeatrybutu, napiszesz oprogramowanie pośredniczące do uwierzytelniania, aby pobrać ten nagłówek i przekształcić go w uwierzytelniony ClaimsPrincipal. Następnie sprawdziłbyś to w ramach wymogu autoryzacji. Wymagania dotyczące autoryzacji mogą być tak skomplikowane, jak chcesz, na przykład tutaj, która przyjmuje datę urodzenia na podstawie bieżącej tożsamości i autoryzuje, jeśli użytkownik ma ukończone 18 lat;

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

Następnie w swojej ConfigureServices()funkcji załączyłbyś go

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

Na koniec zastosuj go do kontrolera lub metody akcji za pomocą

[Authorize(Policy = "Over18")]

84
Zastanawiam się ... w jaki sposób można wprowadzić w ten sposób drobnoziarnistą kontrolę dostępu? Powiedzmy, że ManageStorewymagana jest próbka z Music Store. Ponieważ jest to w próbce, istnieje tylko „albo zezwól na wszystko albo nic”. Czy musimy stworzyć nową politykę dla każdej możliwej permutacji? tzn. „Użytkownicy / Odczytaj”, „Użytkownicy / Utwórz”, „Użytkownicy / AssignRole”, „Użytkownicy / Usuń”, jeśli chcemy uzyskać szczegółowe wnioski? Wygląda na to, że praca nad konfiguracją jest wystarczająca, a obfitość zasad tylko po to, by zarządzać roszczeniami, a nie [ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]atrybutem?
Tseng

83
Muszę to skomentować, wszystko to jest bardziej złożone niż wdrożenie niestandardowej metody autoryzacji. Wiem, jak chcę dokonać autoryzacji. Mógłbym po prostu napisać ją w MVC 5, w MVC 6 dodają dużo „wykonanego” kodu, który jest w rzeczywistości bardziej skomplikowany do zrozumienia niż samo wdrożenie podstawowej „rzeczy”. Sprawia, że ​​siedzę przed stroną, próbując coś wymyślić, zamiast pisać od razu kod, co również jest dużym problemem dla osób korzystających z RDBMS innych niż Microsoft (lub No-Sql).
Felype,

17
Z mojego punktu widzenia nie rozwiązuje to wszystkich scenariuszy. Przed MVC 6 użyłem niestandardowego atrybutu autoryzacji, aby zaimplementować własny „system uprawnień”. Mógłbym dodać atrybut Autoryzuj do wszystkich akcji i przekazać jedno konkretne potrzebne uprawnienie (jako wartość wyliczenia). Samo pozwolenie zostało zmapowane na grupy / użytkowników w bazie danych. Więc nie widzę sposobu, aby sobie z tym poradzić dzięki zasadom !?
Gerwald

43
Podobnie jak wielu innych w tych komentarzach, jestem bardzo rozczarowany, że używanie atrybutów do autoryzacji zostało tak bardzo zneutralizowane w stosunku do tego, co było możliwe w Web API 2. Przepraszam, ale abstrakcja „wymagań” nie obejmuje żadnego przypadku, w którym moglibyśmy wcześniej użyć przypisać parametry konstruktora, aby poinformować podstawowy algorytm autoryzacji. Kiedyś mózg był tak prosty, że zrobił coś takiego [CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]. Mógłbym użyć jednego niestandardowego atrybutu na nieskończoną liczbę sposobów, po prostu modyfikując parametry konstruktora.
NathanAldenSr

61
Jestem również zszokowany, że samozwańczy „Główny facet ds. Bezpieczeństwa ASP.NET” faktycznie sugeruje użycie magicznych ciągów (włamanie się do znaczenia IAuthorizeData.Policy) i niestandardowych dostawców polityk, aby przezwyciężyć ten rażący nadzór, zamiast zajmować się nim w ramach. Myślałem, że nie powinniśmy tworzyć własnych wdrożeń? Kilku z nas nie pozostawiło innego wyboru, jak tylko ponownie wdrożyć autoryzację od zera (tym razem), tym razem bez korzyści ze starego Authorizeatrybutu interfejsu API sieci Web . Teraz musimy to zrobić na poziomie filtra akcji lub oprogramowania pośredniego.
NathanAldenSr

104

Wydaje się, że dzięki ASP.NET Core 2 możesz ponownie odziedziczyć AuthorizeAttribute, wystarczy zaimplementować IAuthorizationFilter(lub 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;
        }
    }
}

4
Możesz więc użyć tego tylko do odmowy autoryzacji, a nie do jej udzielenia ?
PAMIĘĆ

1
@ UWAGI Przyznając , masz na myśli zastąpienie innego atrybutu autoryzacji?
gius

2
AFAIK, dostęp jest domyślnie dozwolony, więc musisz jawnie temu odmówić (np. Dodając AuthorizeAttribute). Sprawdź to pytanie, aby uzyskać więcej informacji: stackoverflow.com/questions/17272422/...
gius

16
Zauważ też, że w sugerowanym przykładzie nie trzeba dziedziczyć po AuthorizeAttribute. Możesz dziedziczyć po Attribute i IAuthorizationFilter . W ten sposób nie zostanie wyświetlony następujący wyjątek, jeśli zostanie użyty niestandardowy mechanizm uwierzytelniania: InvalidOperationException: Nie określono opcji authenticationScheme i nie znaleziono DefaultChallengeScheme.
Anatoljewicz

13
Zauważ, że jeśli twoja OnAuthorizationimplementacja musi poczekać na metodę asynchroniczną, powinieneś zaimplementować IAsyncAuthorizationFilterzamiast tego IAuthorizationFilterfiltr wykona się synchronicznie, a akcja kontrolera zostanie wykonana niezależnie od wyniku filtru.
Codemunkie

34

Na podstawie Dereka Greera WIELKIEGO , zrobiłem to z wyliczeniami.

Oto przykład mojego kodu:

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

1
Dzięki za to. Stworzyłem ten post z nieco inną implementacją i prośbą o walidację stackoverflow.com/questions/49551047/...
Anton Swanevelder

2
MumboJumboFunction <3
Marek Urbanowicz

31

Możesz utworzyć własny obiekt AuthorizationHandler, który znajdzie niestandardowe atrybuty na kontrolerach i akcjach oraz przekaże je do metody 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>();
    }
}

Następnie możesz użyć go do dowolnych niestandardowych atrybutów na kontrolerach lub działaniach. Na przykład, aby dodać wymagania dotyczące uprawnień. Wystarczy utworzyć niestandardowy atrybut.

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

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

Następnie utwórz Wymaganie, aby dodać do swojej Polityki

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

Następnie utwórz obiekt AuthorizationHandler dla niestandardowego atrybutu, dziedzicząc obiekt AttributeAuthorizationHandler, który utworzyliśmy wcześniej. Zostanie przekazany IEnumerable dla wszystkich niestandardowych atrybutów w metodzie HandleRequirementsAsync, zebranych z kontrolera i działania.

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

Na koniec w metodzie Startup.cs ConfigureServices dodaj niestandardowy moduł obsługi autoryzacji do usług i dodaj swoją politykę.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

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

Teraz możesz po prostu ozdobić swoje kontrolery i akcje niestandardowym atrybutem.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}

1
Rzuć okiem na ten ASAP.
NathanAldenSr

5
Jest to dość przepracowane ... Rozwiązałem to samo za pomocą prostego AuthorizationFilterAttribute, który otrzymuje parametr. Nie potrzebujesz do tego refleksji, wydaje się to nawet bardziej sztuczne niż „oficjalne” rozwiązanie (które uważam za dość ubogie).
Vi100,

2
@ Vi100 Nie mogłem znaleźć wielu informacji na temat AuthorizationFilters w ASP.NET Core. Oficjalna strona dokumentacji mówi, że obecnie pracują nad tym tematem. docs.microsoft.com/en-us/aspnet/core/security/authorization/…
Shawn

4
@ Vi100 Czy możesz podzielić się swoim rozwiązaniem, jeśli istnieje prostszy sposób na osiągnięcie tego, chciałbym wiedzieć.
Shawn

2
Należy zauważyć, że powyższe użycie UnderlyingSystemType się nie kompiluje, ale wydaje się, że usunięcie go działa.
teatime

25

Jakie jest obecne podejście do tworzenia niestandardowego atrybutu autoryzacji

Łatwe: nie twórz własnych AuthorizeAttribute .

W przypadku scenariuszy obejmujących wyłącznie autoryzację (takich jak ograniczenie dostępu tylko do określonych użytkowników) zalecanym podejściem jest użycie nowego bloku autoryzacji: https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#L84 -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() { ... }
}

W przypadku uwierzytelniania najlepiej postępować na poziomie oprogramowania pośredniego.

Co dokładnie próbujesz osiągnąć?


1
Otrzymuję identyfikator sesji w Autoryzacji nagłówka. Z tego identyfikatora będę wiedział, czy dana akcja jest poprawna.
jltrem

1
To nie dotyczy autoryzacji. Wydaje mi się, że „identyfikator sesji” jest w rzeczywistości tokenem zawierającym tożsamość osoby dzwoniącej: zdecydowanie należy to zrobić na poziomie oprogramowania pośredniego.
Kévin Chalet

3
Nie jest to uwierzytelnianie (ustalanie, kim jest użytkownik), ale autoryzacja (określanie, czy użytkownik powinien mieć dostęp do zasobu). Więc gdzie sugerujesz, żebym to rozwiązał?
jltrem

3
@jltrem, uzgodniono, że chodzi o autoryzację, a nie uwierzytelnianie.
bopapa_1979

2
@Pinpoint Nie jestem. Pytam inny system o te informacje. Ten system uwierzytelnia (określa użytkownika) i autoryzuje (mówi mi, do czego użytkownik może uzyskać dostęp). W tej chwili włamałem się do pracy, wywołując metodę w każdej akcji kontrolera, aby drugi system zweryfikował sesję. Chciałbym, aby stało się to automatycznie poprzez atrybut.
jltrem

4

Jeśli ktoś chce tylko zweryfikować token nośnika w fazie autoryzacji, korzystając z bieżących praktyk bezpieczeństwa, możesz:

dodaj to do Startup / ConfigureServices

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

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

i to w twojej bazie kodów,

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

Jeśli kod nie osiągnie context.Succeed(...) , i tak się nie powiedzie (401).

A potem możesz użyć kontrolerów

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Dlaczego zdecydowałbyś się przeprowadzić własną weryfikację tokena, skoro już zajmuje się tym oprogramowanie pośrednie JwtBearer? Umieszcza również poprawną treść w nagłówku odpowiedzi uwierzytelniania WWW w przypadku niepowodzenia weryfikacji / wygaśnięcia uwierzytelnienia / tokena. Jeśli chcesz uzyskać dostęp do potoku uwierzytelniania, możesz skorzystać z określonych zdarzeń w opcjach AddJwtBearer (OnAuthenticationFailed, OnChallenge, OnMessageReceived i OnTokenValidated).
Darren Lewis

Jest to nieskończenie prostsze niż jakiekolwiek inne rozwiązanie, jakie widziałem. Specjalnie dla prostych przypadków użycia klucza API. Jedna aktualizacja: dla wersji 3.1 rzutowanie na AuthorizationFilterContext nie jest już ważne z powodu routingu punktów końcowych. Musisz pobrać kontekst przez HttpContextAccessor.
JasonCoder

2

Współczesny sposób to AuthenticationHandlers

w startup.cs dodaj

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 to usługa, którą wykonujesz, gdzie masz nazwę użytkownika i hasło. w zasadzie zwraca klasę użytkownika, której używasz do mapowania swoich roszczeń.

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

Następnie możesz zapytać o te roszczenia, a ona dowolne dane, które zmapowałeś, jest ich sporo, spójrz na klasę ClaimTypes

możesz użyć tego w metodzie rozszerzenia i uzyskać dowolne mapowanie

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

Myślę, że ten nowy sposób jest lepszy niż

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

Ta genialna odpowiedź działa jak urok! Dziękuję za to i życzę, aby uzyskała uznanie, ponieważ jest to najlepsza odpowiedź, jaką znalazłem po około sześciu godzinach przeszukiwania blogów, dokumentacji i stosu uwierzytelniania podstawowego oraz autoryzacji roli.
Piotr Śródka

@ PiotrŚródka, proszę bardzo, proszę zauważyć, że odpowiedź jest trochę „uproszczona”, sprawdź, czy w tekście jest „:”, ponieważ złośliwy użytkownik może spróbować zawiesić twoją usługę, po prostu nie grając w fajne zakończenie w indeksie wyjątku zakresu. jak zawsze sprawdzaj, co otrzymujesz od źródeł zewnętrznych
Walter Vehoeven

2

W tym piśmie uważam, że można to osiągnąć za pomocą interfejsu IClaimsTransformation w wersji asp.net core 2 i nowszych. Właśnie wdrożyłem dowód koncepcji, który można udostępnić tutaj.

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

Aby użyć tego w swoim kontrolerze, po prostu dodaj odpowiedni [Authorize(Roles="whatever")]do swoich metod.

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

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

W naszym przypadku każde żądanie zawiera nagłówek autoryzacji, który jest JWT. To jest prototyp i wierzę, że zrobimy coś bardzo zbliżonego do tego w naszym systemie produkcyjnym w przyszłym tygodniu.

Przyszli wyborcy, weź pod uwagę datę pisania podczas głosowania. Na dzień dzisiejszy to works on my machine.™ Prawdopodobnie będziesz potrzebować więcej obsługi błędów i logowania do swojej implementacji.


Co z ConfigureServices? Czy trzeba coś dodać?
Daniel

Jak omówiono w innym miejscu, tak.
Bez zwrotów Bez zwrotów

1

Do autoryzacji w naszej aplikacji. Musieliśmy zadzwonić do usługi na podstawie parametrów przekazanych w atrybucie autoryzacji.

Na przykład, jeśli chcemy sprawdzić, czy zalogowany lekarz może przeglądać wizyty pacjentów, przekazujemy „View_Appointment”, aby niestandardowy atrybut autoryzacji i sprawdzić to bezpośrednio w usłudze DB i na podstawie wyników autoryzujemy. Oto kod tego scenariusza:

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

A przy akcji API używamy go w następujący sposób:

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

    }

1
Należy pamiętać, że IActionFilter będzie problemem, jeśli chcesz użyć tego samego atrybutu dla metod Hub w SignalR.SignalR Huby oczekują IAuthorizationFilter
ilkerkaran

Dzięki za informację. Obecnie nie używam SignalR w mojej aplikacji, więc nie przetestowałem go z nim.
Abdullah,

Sądzę, że ta sama zasada, ponieważ nadal będziesz musiał użyć wpisu autoryzacji nagłówka, wdrożenie będzie się różnić
Walter Vehoeven

0

Przyjęta odpowiedź ( https://stackoverflow.com/a/41348219/4974715 ) nie jest realistycznie możliwa do utrzymania ani odpowiednia, ponieważ „CanReadResource” jest używany jako roszczenie (ale zasadniczo powinien być polityką w rzeczywistości, IMO). Podejście do odpowiedzi nie jest OK w sposobie, w jaki zostało użyte, ponieważ jeśli metoda działania wymaga wielu różnych konfiguracji oświadczeń, wówczas przy tej odpowiedzi musiałbyś wielokrotnie pisać coś takiego ...

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

Wyobraź sobie, ile to zajmie kodowanie. Idealnie, „CanReadResource” ma być polityką, która korzysta z wielu oświadczeń w celu ustalenia, czy użytkownik może odczytać zasób.

To, co robię, to tworzę swoje zasady jako wyliczenie, a następnie przeglądam i konfiguruję wymagania w ten sposób ...

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

Klasa DefaultAuthorizationRequirement wygląda jak ...

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

Pamiętaj, że powyższy kod może również umożliwić wstępne mapowanie użytkownika na zasady w magazynie danych. Tak więc, podczas tworzenia oświadczeń dla użytkownika, zasadniczo pobierasz zasady, które zostały wcześniej zmapowane bezpośrednio do użytkownika bezpośrednio lub pośrednio (np. Ponieważ użytkownik ma określoną wartość oświadczenia i ta wartość oświadczenia została zidentyfikowana i odwzorowana na polisę, taką jak że zapewnia automatyczne mapowanie dla użytkowników, którzy również mają tę wartość oświadczenia), i zapisuje zasady jako oświadczenia, tak że w module obsługi autoryzacji możesz po prostu sprawdzić, czy oświadczenia użytkownika zawierają wymóg. Polityka jako wartość elementu oświadczenia w ich roszczenia. Dotyczy to statycznego sposobu spełnienia wymogu polityki, np. Wymóg „Imię” ma charakter dość statyczny. Więc,

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

Wymaganie dynamiczne może dotyczyć sprawdzania przedziału wiekowego itp., A zasad korzystających z takich wymagań nie można wstępnie mapować na użytkowników.

Przykład dynamicznego sprawdzania roszczeń dotyczących zasad (np. W celu sprawdzenia, czy użytkownik ma więcej niż 18 lat) znajduje się już w odpowiedzi udzielonej przez @blowdart ( https://stackoverflow.com/a/31465227/4974715 ).

PS: Napisałem to na telefonie. Ułaskaw wszelkie literówki i brak formatowania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.