API di F #. Modul Aplikasi Berbasis Peran Akses

ASP.NET Core menawarkan standar untuk mengkonfigurasi akses ke api menggunakan atribut, dimungkinkan untuk membatasi akses ke pengguna dengan klaim tertentu, Anda dapat menentukan kebijakan dan mengikat ke controller, membuat controller untuk peran yang berbeda
Sistem ini memiliki minus, yang terbesar di dalamnya, melihat atribut ini:


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

Kami tidak menerima informasi apa pun tentang hak apa yang dimiliki administrator.


Tugas saya adalah untuk menampilkan semua pengguna yang dilarang untuk bulan ini (tidak hanya pergi ke database dan filter, ada aturan penghitungan tertentu yang ada di suatu tempat), saya melakukan CTRL + N pada proyek dan mencari BannedUserHandler atau IHasInfoAbounBannedUser atau GetBannedUsersForAdmin .


Saya menemukan pengendali yang ditandai dengan atribut [Otorisasi (Peran = "Administrator")] , mungkin ada dua skenario:


Kami melakukan semua yang ada di controller


  [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 //... //... } 

Kami mendistribusikan pada penangan


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

Pendekatan pertama tidak buruk karena kita tahu akses ke sumber daya yang dimiliki Admin, dependensi apa yang dapat digunakan, saya akan menggunakan pendekatan ini dalam aplikasi kecil, tanpa area subjek yang kompleks


Yang kedua tidak begitu berbicara, semua dependensi diselesaikan dalam penangan, saya tidak bisa melihat konstruktor dan mengerti apa jenis dependensi yang saya butuhkan, pendekatan ini membenarkan dirinya ketika aplikasi tersebut kompleks dan pengendali membengkak, menjadi tidak mungkin untuk mendukung mereka. Solusi klasik untuk masalah ini adalah mempartisi solusi ke dalam folder / proyek, layanan yang diperlukan diletakkan di masing-masing, mereka mudah ditemukan dan digunakan


Semua ini memiliki kelemahan besar, kode tidak memberi tahu pengembang apa yang harus dilakukan, itu membuat Anda berpikir => buang-buang waktu => kesalahan implementasi


Dan semakin Anda harus berpikir, semakin banyak kesalahan dibuat.


Pengantar Routing Suave


Bagaimana jika routing dibangun seperti ini :


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

''> => '' - apa itu? Benda ini memiliki nama, tetapi pengetahuannya tidak akan membuat pembaca satu gram lebih dekat untuk memahami cara kerjanya, jadi tidak ada gunanya membawanya, lebih baik untuk mempertimbangkan bagaimana semuanya bekerja


Pipa dari Suave ditulis di atas, sama digunakan di Giraffe (dengan fungsi tanda tangan yang berbeda), ada tanda tangan:


 type WebPart = HttpContext -> Async<HttpContext option> 

Async dalam hal ini tidak memainkan peran khusus (untuk memahami cara kerjanya), hilangkan saja


 HttpContext -> HttpContext option 

Sebuah fungsi dengan tanda tangan seperti itu menerima HttpContext , memproses (deserializes tubuh, melihat cookie, header permintaan), menghasilkan respons, dan jika semuanya berjalan dengan baik, membungkusnya dalam Beberapa , jika terjadi kesalahan, mengembalikan Tidak ada , misalnya ( fungsi perpustakaan ):


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

Fungsi ini tidak dapat "membungkus alur eksekusi permintaan", ia selalu melempar respons baru lebih jauh, dengan tubuh dan status 200, tetapi yang ini bisa:


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

Fungsi terakhir yang Anda butuhkan adalah memilih - ia mendapat daftar fungsi yang berbeda dan memilih yang mengembalikan Beberapa terlebih dahulu:


 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 } 

Nah, fungsi pengikatan yang paling penting (Async dihilangkan):


 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 

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

"> =>" menerima dua penangan di sisi kiri dan kanan dan httpContext , ketika permintaan datang, server membentuk objek HttpContext dan meneruskannya ke fungsi, "> =>" menjalankan penangan pertama (kiri) jika mengembalikan beberapa ctx , melewati ctx ke input dari penangan kedua.


Dan mengapa kita bisa menulis seperti ini (menggabungkan beberapa fungsi)?


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

Karena "> =>" menerima dua fungsi WebPart dan mengembalikan satu fungsi yang mengambil HttpContext dan mengembalikan Async <HttpContext option> , dan fungsi mana yang mengambil konteks dan mengembalikan Async <HttpContext option> ?
Webpart .


Ternyata "> =>" mengambil WebPart untuk handler dan mengembalikan WebPart , jadi kita bisa menulis beberapa kombinator berturut-turut, dan bukan hanya dua.
Detail tentang kerja kombinator dapat ditemukan di sini.


Apa hubungan peran dan pembatasan akses dengan hal itu?


Mari kita kembali ke awal artikel, bagaimana kita dapat secara eksplisit menunjukkan kepada programmer sumber daya apa yang dapat diakses untuk peran tertentu? Penting untuk memasukkan data ini ke dalam pipa sehingga penangan memiliki akses ke sumber daya yang sesuai, saya melakukannya seperti ini:



Aplikasi ini dibagi menjadi beberapa bagian / modul. Fungsi AdminPart dan AccountPart memungkinkan akses ke modul ini dari berbagai peran, semua pengguna memiliki akses ke AccountPart, hanya admin yang mengakses AdminPart, data diterima, memperhatikan fungsi selectP, saya harus menambahkan lebih banyak fungsi, karena yang standar melekat pada jenis ramah tamah, dan penangan di dalam AdminPart dan AccountPart sekarang memiliki tanda tangan yang berbeda:


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

Di dalam, fitur-fitur baru benar-benar identik dengan yang asli.


Sekarang pawang segera memiliki akses ke sumber daya untuk setiap peran, hanya hal utama yang perlu ditambahkan di sana sehingga Anda dapat dengan mudah menavigasi, misalnya, di AccountPart Anda dapat menambahkan nama panggilan, email, peran pengguna, daftar teman jika itu adalah jejaring sosial, tetapi ada masalah: untuk satu hal yang luar biasa kebanyakan penangan saya membutuhkan daftar teman, tetapi untuk sisanya saya tidak membutuhkannya sama sekali, apa yang harus saya lakukan? Baik mendistribusikan penangan ini ke modul yang berbeda (lebih disukai), atau membuat akses malas (bungkus dalam unit -> daftar teman ), hal utama adalah tidak menempatkan IQueryable <Friend> di sana , karena ini bukan layanan - itu adalah kumpulan data yang menentukan peran


Saya memasukkan informasi AdminInfo tentang pengguna yang disetujui dan dicekal oleh administrator saat ini, dalam konteks "aplikasi" saya, ini mendefinisikan peran Administrator:


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

Apa perbedaan dari Klaim ? Apakah mungkin untuk membuat User.Claims di controller dan mendapatkan hal yang sama?


Dalam mengetik dan berbicara: modul, pengembang tidak harus mencari contoh kode pada penangan dalam konteks yang sama, ia menciptakan penangan dan menambahkannya ke perutean dan membuatnya semuanya dikompilasi


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

getUserInfo menerima data untuk modul Akun , memiliki akses ke konteks untuk mendapatkan data pribadi (dari user'a ini, admin'a)


permitHandler memeriksa token token, mendekripsi, dan memeriksa akses, mengembalikan WebPart asli untuk mempertahankan kompatibilitas dengan Suave


Kode sumber lengkap dapat ditemukan di github


Terima kasih atas perhatian anda!

Source: https://habr.com/ru/post/id461593/


All Articles