F#上的API。 基于访问角色的应用程序模块

标准的ASP.NET Core提供了使用属性配置对api的访问的权限,可以通过特定声明限制对用户的访问,可以定义策略并绑定到控制器,为不同角色创建控制器
该系统在查看此属性时有缺点,这是最大的缺点:


[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { } 

我们没有收到有关管理员拥有哪些权利的任何信息。


我的任务是显示该月所有被禁止的用户(不仅去数据库和过滤器,某些计数规则位于某处),我在项目上执行CTRL + N并查找BannedUserHandlerIHasInfoAbounBannedUserGetBannedUsersForAdmin


我发现标记有属性[Authorize(Roles =“ Administrator”)]的控制器,可能有两种情况:


我们在控制器中做所有事情


  [Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService; //    action public AdminInfoController1( IGetUserInfoService infoAboutActiveUsers, ICanBanUserService banUserService, ICanRemoveBanUserService removeBanUserService) { _getInfoAboutActiveUsers = infoAboutActiveUsers; _banUserService = banUserService; _removeBanUserService = removeBanUserService; } // actions //... //... } 

我们在处理程序上分发


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

第一种方法还不错,因为我们知道管理员拥有哪些资源,可以使用哪些依赖项,我将在小型应用程序中使用这种方法,而无需复杂的主题领域


第二个问题不是在谈论,所有的依赖关系都在处理程序中解决,我看不到构造函数,也不了解我需要什么样的依赖关系,当应用程序复杂且控制器膨胀时,这种方法证明了自己的正确性,因此无法支持它们。 解决此问题的经典解决方案是将解决方案分为文件夹/项目,将必要的服务放在每个文件夹/项目中,易于查找和使用


所有这些都有一个很大的缺点,代码没有告诉开发人员该怎么做,它使您认为=>浪费时间=>实现错误


而且您思考的越多,犯的错误就越多。


Suave路由简介


如果路由是这样构建的:


 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") ] 

''> =>''-这是什么? 这个东西有个名字,但是它的知识不会使读者更容易理解它的工作原理,所以没有必要提出它,最好考虑一下一切如何工作


Suave的管道写在上面,在长颈鹿中使用的管道也一样(具有不同的功能签名),有一个签名:


 type WebPart = HttpContext -> Async<HttpContext option> 

在这种情况下,异步不会发挥特殊作用(以了解其工作原理),请忽略它


 HttpContext -> HttpContext option 

具有此类签名的函数接受HttpContext ,进行处理(对主体进行反序列化,查看cookie,请求标头),形成响应,并且如果一切顺利,则将其包装在Some中 ,如果出现问题,则返回None ,例如( library function ):


  //    async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return 

此函数不能“包装请求执行流”,它总是进一步抛出新的响应,其正文和状态为200,但是此函数可以:


 let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None 

需要选择的最后一个函数-它会获得一系列不同的函数,并选择一个先返回Some的函数:


 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 } 

好了,最重要的绑定函数(省略了异步):


 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 

异步版本
 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 } 

“> =>”在左侧和右侧分别接受两个处理程序和httpContext ,当请求到达时,服务器形成一个HttpContext对象,并将其传递给函数,“> =>”如果返回Some ctx ,则执行第一个(左侧)处理程序,传递ctx到第二个处理程序的输入。


为什么我们可以这样写(结合几个函数)?


 GET >=> path "/api" >=> OK 

因为“> =>”接受两个WebPart函数并返回一个接受HttpContext并返回Async <HttpContext option>的函数,而哪个函数需要上下文并返回Async <HttpContext option>
Webpart


事实证明“> =>”将WebPart用作处理程序并返回WebPart ,因此我们可以连续编写多个组合器,而不仅仅是两个。
有关组合器工作的详细信息,请参见此处。


角色和访问限制与它有什么关系?


让我们回到文章的开头,我们如何向程序员明确指示可以为特定角色访问哪些资源? 有必要将这些数据输入到管道中,以便处理程序可以访问相应的资源,我这样做是这样的:



该应用程序分为多个部分/模块。 AdminPartAccountPart函数允许访问这些具有各种角色的模块,所有用户都可以访问AccountPart,只有管​​理员正在访问AdminPart,接收到数据,请注意chooseP函数,我必须添加更多函数,因为标准函数附在Suave类型上,并且AdminPartAccountPart中的处理程序现在具有不同的签名:


 // AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option> 

在内部,新功能与原始功能完全相同。


现在,处理程序可以立即访问每个角色的资源,只需在其中添加主要内容,即可轻松导航,例如,在AccountPart中 ,可以添加昵称,电子邮件,用户角色,朋友列表(如果它是社交网络),但是存在一个问题:大多数处理程序我都需要一个朋友列表,但是对于其余的我根本不需要它,该怎么办? 要么将这些处理程序分配到不同的模块(最好是),要么使访问变得懒惰(将其包装在unit-> friends list中 ),主要是不要将IQueryable <Friend>放在那里 ,因为这不是服务-它是定义角色的数据集


我在当前的“应用程序”上下文中输入了有关当前管理员批准和禁止的用户的AdminInfo信息,该信息定义了管理员的角色:


  type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string } 

Claim有什么区别? 是否可以在控制器中创建User.Claims并获得相同的结果?


在打字和交谈:模块中,开发人员不必在相同上下文中的处理程序中查找代码示例,他可以创建一个处理程序并将其添加到路由中并全部编译


 let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler 

getUserInfo接收“ 帐户”模块的数据,有权访问上下文以获取个人数据(这正是该用户'a,admin'a)


PermissionHandler检查jwt令牌,对其进行解密,然后检查访问权限,返回原始WebPart以维持与Suave的兼容性


完整的源代码可以在github上找到


感谢您的关注!

Source: https://habr.com/ru/post/zh-CN461593/


All Articles