يقدم ASP.NET Core وفقًا للمعايير تكوين الوصول إلى api باستخدام السمات ، فمن الممكن تقييد الوصول إلى المستخدمين الذين لديهم مطالبة معينة ، ويمكنك تحديد السياسات والربط بوحدات التحكم ، وإنشاء وحدات تحكم لأدوار مختلفة
يحتوي هذا النظام على السلبيات ، وهو الأكبر في ذلك ، بالنظر إلى هذه السمة:
[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { }
لا نتلقى أي معلومات حول الحقوق التي يتمتع بها المسؤول.
مهمتي هي عرض جميع المستخدمين المحظورين لهذا الشهر (وليس فقط الذهاب إلى قاعدة البيانات والتصفية ، وهناك بعض قواعد العد التي تقع في مكان ما) ، وأنا أفعل CTRL + N على المشروع والبحث عن BannedUserHandler أو IHasInfoAbounBannedUsersForAdmin .
أجد وحدات التحكم المميزة بالسمة [تخويل (الأدوار = "المسؤول")] ، قد يكون هناك سيناريوهان:
نحن نفعل كل شيء في وحدة تحكم
[Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService;
نحن نوزع على معالجات
[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); }
الطريقة الأولى ليست سيئة لأننا نعرف الوصول إلى الموارد التي يملكها المسؤول ، وما هي التبعيات التي يمكن أن يستخدمها ، وأود أن استخدم هذا النهج في التطبيقات الصغيرة ، دون وجود موضوع معقد
والثاني لا يتحدث عن ذلك ، يتم حل جميع التبعيات في معالجات ، ولا يمكنني النظر إلى المنشئ وفهم نوع التبعية الذي أحتاجه ، وهذا النهج يبرر نفسه عندما يكون التطبيق معقدًا وتضخم وحدات التحكم ، ويصبح من المستحيل دعمها. الحل الكلاسيكي لهذه المشكلة هو تقسيم الحل إلى مجلدات / مشاريع ، ويتم تقديم الخدمات اللازمة في كل منها ، ويسهل العثور عليها واستخدامها
كل هذا له عيب كبير ، الكود لا يخبر المطور بما يجب فعله ، فهو يجعلك تفكر => مضيعة للوقت => أخطاء التنفيذ
وكلما زاد تفكيرك ، كلما ارتكبت أخطاء أكثر.
مقدمة لرقيق التوجيه
ماذا لو تم بناء التوجيه مثل هذا :
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 مكتوب أعلاه ، ويستخدم نفس الشيء في Giraffe (مع توقيع مختلف للوظائف) ، وهناك توقيع:
type WebPart = HttpContext -> Async<HttpContext option>
لا يلعب المزامنة في هذه الحالة دورًا خاصًا (لفهم كيفية عمله) ، أو حذفه
HttpContext -> HttpContext option
تقبل الوظيفة ذات هذا التوقيع HttpContext ، والعمليات (إلغاء تسلسل الجسم ، والنظر إلى ملفات تعريف الارتباط ، ورؤوس الطلبات) ، وتشكيل استجابة ، وإذا سارت الأمور على ما يرام ، فقم بلفها في البعض ، وإذا حدث خطأ ما ، فتُرجع بلا ، على سبيل المثال ( وظيفة المكتبة ):
// 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
الوظيفة الأخيرة التي تحتاج إليها هي الاختيار - فهي تحصل على قائمة بالوظائف المختلفة وتختار الوظيفة التي تُرجع بعضها أولاً:
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 }
حسنًا ، أهم وظيفة ربط (تم حذف Async):
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 ويمرره إلى الوظيفة ، "> =>" ينفذ المعالج الأول (يسار) إذا قام بإرجاع بعض ctx ، يمرر ctx إلى مدخلات المعالج الثاني.
ولماذا يمكننا كتابة مثل هذا (الجمع بين عدة وظائف)؟
GET >=> path "/api" >=> OK
لأن "> =>" تقبل دالتين WebPart وإرجاع دالة واحدة تأخذ HttpContext وتعيد Async <خيار HttpContext> ، وأي وظيفة تأخذ السياق وتعيد Async <خيار HttpContext> ؟
Webpart .
اتضح أن "> =>" يأخذ WebPart للمعالج ويعيد WebPart ، حتى نتمكن من كتابة عدة أدوات دمج في صف واحد ، وليس اثنين فقط.
يمكن الاطلاع هنا على تفاصيل حول أدوات الدمج .
ما علاقة الدور وتقييد الوصول به؟
دعنا نعود إلى بداية المقالة ، كيف يمكننا أن نوضح للمبرمج بوضوح الموارد التي يمكن الوصول إليها لدور معين؟ من الضروري إدخال هذه البيانات في خط الأنابيب حتى يتسنى للمعالجات الوصول إلى الموارد المقابلة ، وقد فعلت ذلك بالشكل التالي:

ينقسم التطبيق إلى أجزاء / وحدات. تسمح وظيفتا AdminPart و AccountPart بالوصول إلى هذه الوحدات ذات الأدوار المختلفة ، وجميع المستخدمين لديهم حق الوصول إلى AccountPart ، والمسؤول هو الوحيد الذي يصل إلى AdminPart ، ويتم استلام البيانات ، والاهتمام بوظيفة selectP ، وعلي إضافة المزيد من الوظائف ، لأن الوظائف القياسية مرفقة بأنواع Suave ، و لدى المعالجات داخل AdminPart و AccountPart الآن توقيعات مختلفة:
// AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option>
في الداخل ، تتشابه الميزات الجديدة تمامًا مع الميزات الأصلية.
الآن لدى المعالج على الفور إمكانية الوصول إلى الموارد لكل دور ، يجب إضافة الشيء الرئيسي فقط بحيث يمكنك التنقل بسهولة ، على سبيل المثال ، في AccountPart يمكنك إضافة اسم مستعار ، بريد إلكتروني ، دور مستخدم ، قائمة أصدقاء إذا كانت شبكة اجتماعية ، ولكن هناك مشكلة: بالنسبة لشخص واحد ساحق أحتاج لمعظم الأصدقاء إلى قائمة الأصدقاء ، لكن بالنسبة للبقية لا أحتاج إليها على الإطلاق ، فماذا أفعل؟ إما أن توزع هذه المعالجات في وحدات نمطية مختلفة (يفضل) أو تجعل الوصول كسولًا (التفاف في الوحدة -> قائمة الأصدقاء ) ، الشيء الرئيسي هو عدم وضع IQueryable <Friend> هناك ، لأن هذه ليست خدمة - إنها مجموعة بيانات تحدد الدور
قمت بوضع معلومات AdminInfo حول المستخدمين المعتمدين والممنوعين من قِبل المسؤول الحالي ، في سياق "تطبيقي" هذا يحدد دور المسؤول:
type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string }
ما هو الفرق من المطالبة ؟ هل من الممكن جعل User.Claims في وحدة التحكم والحصول على نفس الشيء؟
في الكتابة والتحدث: الوحدات النمطية ، لا يتعين على المطور البحث عن أمثلة التعليمات البرمجية على معالجات في نفس السياق ، فهو ينشئ معالجًا ويضيفه إلى التوجيه ويجعله كله مترجمًا
let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler
يتلقى getUserInfo بيانات وحدة الحساب ، لديه حق الوصول إلى السياق للحصول على بيانات شخصية (هذا هو بالضبط user'a ، admin'a)
تقوم الإذن Handler بالتحقق من وجود الرمز المميز jwt وفك تشفيره والتحقق من الوصول وإرجاع WebPart الأصلي للحفاظ على التوافق مع Suave
يمكن العثور على شفرة المصدر الكاملة على جيثب
شكرا لاهتمامكم!