O ASP.NET Core, por padrão, oferece a configuração de acesso à API usando atributos, é possível restringir o acesso a usuários com uma reivindicação específica, você pode definir políticas e vincular-se a controladores, criando controladores para diferentes funções
Este sistema possui desvantagens, a maior delas, observando este atributo:
[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { }
Não recebemos informações sobre quais direitos o administrador possui.
Minha tarefa é exibir todos os usuários banidos para este mês (não basta ir ao banco de dados e filtrar, existem certas regras de contagem em algum lugar), faço CTRL + N no projeto e procuro BannedUserHandler ou IHasInfoAbounBannedUser ou GetBannedUsersForAdmin .
Acho que os controladores marcados com o atributo [Autorizar (funções = "Administrador")] , podem haver dois cenários:
Fazemos tudo no controlador
[Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService;
Distribuímos em manipuladores
[Route("api/[controller]/[action]")] public class AdminInfoController2 : ControllerBase { [HttpPatch("{id}")] public async Task<ActionResult<BanUserResult>> BanUser( [FromServices] IAsyncHandler<UserId, BanUserResult> handler, UserId userId) => await handler.Handle(userId, HttpContext.RequestAborted); [HttpPatch("{id}")] public async Task<ActionResult<RemoveBanUserResult>> RemoveBanUser( [FromServices] IAsyncHandler<UserId, RemoveBanUserResult> handler, UserId userId) => await handler.Handle(userId, HttpContext.RequestAborted); }
A primeira abordagem não é ruim, pois sabemos o acesso a quais recursos o Admin tem, quais dependências ele pode usar. Eu usaria essa abordagem em pequenas aplicações, sem uma área de assunto complexa.
O segundo não é o que fala, todas as dependências são resolvidas nos manipuladores, não consigo olhar para o construtor e entender que tipo de dependência eu preciso; essa abordagem se justifica quando o aplicativo é complexo e os controladores incham, torna-se impossível apoiá-los. A solução clássica para esse problema é o particionamento da solução em pastas / projetos, os serviços necessários são colocados em cada um, são fáceis de encontrar e usar
Tudo isso tem uma grande desvantagem, o código não diz ao desenvolvedor o que fazer, faz você pensar => perda de tempo => erros de implementação
E quanto mais você pensa, mais erros são cometidos.
Introdução ao Roteamento Suave
E se o roteamento for construído assim :
let webPart = choose [ path "/" >=> (OK "Home") path "/about" >=> (OK "About") path "/articles" >=> (OK "List of articles") path "/articles/browse" >=> (OK "Browse articles") path "/articles/details" >=> (OK "Content of an article") ]
''> => '' - o que é? Essa coisa tem um nome, mas seu conhecimento não aproximará nem um grama o leitor de como ela funciona; portanto, não há sentido em trazê-la; é melhor considerar como tudo funciona.
O pipeline do Suave está escrito acima, o mesmo é usado no Giraffe (com uma assinatura de funções diferente), existe uma assinatura:
type WebPart = HttpContext -> Async<HttpContext option>
O assíncrono nesse caso não desempenha um papel especial (para entender como funciona), omita-o
HttpContext -> HttpContext option
Uma função com essa assinatura aceita um HttpContext , processa (desserializa o corpo, consulta cookies, solicita cabeçalhos), forma uma resposta e, se tudo correu bem, envolve-a em Some , se algo der errado, retorna None , por exemplo ( função da biblioteca ):
// async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return
Essa função não pode "encapsular o fluxo de execução da solicitação", sempre lança uma nova resposta ainda mais, com corpo e status 200, mas esta pode:
let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None
A última função que você precisa é escolher - ela obtém uma lista de funções diferentes e seleciona a que retorna Some first:
let rec choose (webparts:(HttpContext) -> Async<HttpContext option>) list) context= async{ match webparts with | [head] -> return! head context | head::tail -> let! result = head context match result with | Some _-> return result | None -> return! choose tail context | [] -> return None }
Bem, a função de ligação mais importante (Async omitida):
type WebPartWithoutAsync = HttpContext -> HttpContext option let (>=>) (h1:WebPartWithoutAsync ) (h2:WebPartWithoutAsync) ctx : HttpContext option = let result = h1 ctx match result with | Some ctx' -> h2 ctx' | None -> None
Versão assíncrona type WebPart = HttpContext -> Async<HttpContext option> let (>=>) (h1:WebPart ) (h2:WebPart ) ctx : Async<HttpContext option>= async{ let! result = h1 ctx match result with | Some ctx' -> return! h2 ctx' | None -> return None }
"> =>" aceita dois manipuladores nos lados esquerdo e direito e httpContext , quando a solicitação chega, o servidor forma um objeto HttpContext e o passa para a função "> =>" executa o primeiro manipulador (esquerdo) se retornar algum ctx , passa ctx para a entrada do segundo manipulador.
E por que podemos escrever assim (combinar várias funções)?
GET >=> path "/api" >=> OK
Como "> =>" aceita duas funções WebPart e retorna uma função que aceita o HttpContext e retorna Async <opção HttpContext> , e qual função aceita o contexto e retorna Async <opção HttpContext> ?
Webpart .
Acontece que "> =>" pega o WebPart para o manipulador e retorna o WebPart , para que possamos escrever vários combinadores em uma linha, e não apenas dois.
Detalhes sobre o trabalho dos combinadores podem ser encontrados aqui.
O que a função e a restrição de acesso têm a ver com isso?
Voltando ao início do artigo, como podemos indicar explicitamente ao programador quais recursos podem ser acessados para uma função específica? É necessário inserir esses dados no pipeline para que os manipuladores tenham acesso aos recursos correspondentes, eu fiz assim:

O aplicativo é dividido em partes / módulos. As funções AdminPart e AccountPart permitem acesso a esses módulos de várias funções, todos os usuários têm acesso a AccountPart, apenas o administrador está acessando AdminPart, os dados são recebidos, preste atenção à função chooseP, preciso adicionar mais funções, porque as padrão estão anexadas aos tipos Suave e manipuladores dentro AdminPart e AccountPart agora têm assinaturas diferentes:
// AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option>
No interior, os novos recursos são completamente idênticos aos originais.
Agora, o manipulador tem acesso imediato aos recursos de cada função, apenas o principal precisa ser adicionado lá, para que você possa navegar facilmente, por exemplo, em AccountPart, você pode adicionar um apelido, email, função de usuário e lista de amigos, se for uma rede social, mas há um problema: na maioria dos manipuladores, preciso de uma lista de amigos, mas, de resto, não preciso disso, o que devo fazer? Distribua esses manipuladores em diferentes módulos (de preferência) ou torne o acesso lento (agrupar na unidade -> lista de amigos ), a principal coisa é não colocar IQueryable <Friend> lá , porque esse não é um serviço - é um conjunto de dados que define a função
Coloquei no AdminInfo informações sobre usuários aprovados e banidos pelo administrador atual. No contexto do meu "aplicativo", isso define o papel do Administrador:
type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string }
Qual é a diferença de Claim ? É possível criar User.Claims no controlador e obter a mesma coisa?
Ao digitar e falar: modules, o desenvolvedor não precisa procurar exemplos de código em manipuladores no mesmo contexto, ele cria um manipulador e o adiciona ao roteamento e faz tudo compilar
let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler
getUserInfo recebe dados para o módulo Account , tem acesso ao contexto para obter dados pessoais (este é exatamente esse usuário, administrador)
permissionHandler verifica o token jwt, descriptografa e verifica o acesso, retorna a WebPart original para manter a compatibilidade com o Suave
O código fonte completo pode ser encontrado no github
Obrigado pela atenção!