در بخش پیشین به بررسی جزئیتر ایجاد پایگاه داده و همچنین توسعه Custom Filter Attribute پرداختیم که وظیفه تایید صلاحیت کاربر جاری و بررسی دسترسی وی به API Method مورد نظر را بررسی میکرد. در این مقاله به این بحث میپردازیم که در Filter Attribute توسعه داده شده، قصد داریم یک سرویس Access Control ایجاد نماییم.
همانطور که ملاحظه میکنید، ما سه متد GetUserPermissions، GetUserRoles و HasPermission را توسعه دادهایم. حال اینکه بر حسب نیاز، میتوانید متدهای بیشتری را نیز به این سرویس اضافه نمایید. متد اول، وظیفهی واکشی تمامی permissionهای کاربر را عهده دار میباشد. متد GetUserRoles نیز تمامی نقشهای کاربر را در سیستم، بازمیگرداند و در نهایت متد سوم، همان متدی است که ما در Filterattribute از آن استفاده کردهایم. این متد با دریافت پارامترها و بازگردانی یک مقدار درست یا نادرست، تعیین میکند که کاربر جاری به آن محدوده دسترسی دارد یا خیر.
در متد فوق ما از متد سرویس Access Control که لیست تمامی permissionهای کاربر را باز میگرداند، کمک گرفتیم. متد GetUserPermissions پس از ورود کاربر توسط کلاینت فراخوانی میگردد و لیست تمامی دسترسیها در سمت کلاینت، در rootScope انگیولار ذخیره سازی میگردد. حال نوبت به آن رسیده که به بررسی عملیات سمت کلاینت بپردازیم.
اگر بخواهیم مختصری دربارهی این سرویس صحبت کنیم، متد اول که یک دستور GET ساده است و لیست دسترسیها را از PermissionController دریافت میکند. متد بعدی که در آینده بیشتر با آن آشنا میشویم، عملیات تایید صلاحیت کاربر را به ناحیه مورد نظر، انجام میدهد (همان مثال دسترسی به دکمه ویرایش مطلب در یک صفحه). در این متد برای جستجوی لیست permissions از کتابخانهای با نام Lodash کمک گرفتهایم. این کتابخانه کاری شبیه به دستورات Linq را در کالکشنها و آرایههای جاوااسکریپتی، انجام میدهد. متد some یک مقدار درست یا نادرست را بازمیگرداند. بازگردانی مقدار درست، به این معنی است که کاربر به ناحیهی مورد نظر اجازهی دسترسی را خواهد داشت.
حال تمامی اطلاعات دسترسی، در سمت کلاینت نیز قابل دسترسی میباشد. تنها کاری که نیاز است، broadCast کردن متد isAuthorize است که آن هم باید در rootScope قرار بگیرد. ما برای این انتساب یک راهکار را ارائه کردهایم. معماری سیستم کلاینت به این صورت است که تمامی کنترلرها درون یک parentController قرار گرفتهاند. از این رو میتوان در parentController این انتساب (ایجاد دسترسی عمومی برایisAuthorize) صورت گیرد. برای این کار در parentController تغییرات زیر صورت میگیرد:
در کد فوق ما isAuthorize را درون scope قرار دادهایم. دلیل آن هم این است که هر چه که در scope قرار بگیرد، تمامی کنترلرهای child نیز به آن دسترسی خواهند داشت. البته ممکن است که این بهترین نوع پیاده سازی برای به اشتراک گذاری یک منبع نباشد.
همانطور که مشاهده میکنید، تمامی المانها را میتوان با دستور ساده ng-if، از دید کاربران بدون صلاحیت، پنهان نمود. البته توجه داشته باشید که شما نمیتوانید تنها به پنهان کردن این اطلاعات اکتفا کنید. بلکه باید تمامی متدهای کنترلرهای سمت سرور را هم با همین روش (فیلتر کردن با Filter Attribute) بررسی نمایید. به ازای هر درخواست کاربر باید بررسی شود که او به منبع مورد نظر دسترسی دارد یا خیر.
این سرویس وظیفه تمامی اعمال مربوط به نقشها و دسترسیهای کاربر را بر عهده خواهد داشت. این سرویس به صورت زیر تعریف میگردد:
public class AccessControlService { private DbContext db; public AccessControlService() { db = new DbContext(); } public IEnumerable<Permission> GetUserPermissions(string userId) { var userRoles = this.GetUserRoles(userId); var userPermissions = new List<Permission>(); foreach (var userRole in userRoles) { foreach (var permission in userRole.Permissions) { // prevent duplicates if (!userPermissions.Contains(permission)) userPermissions.Add(permission); } } return userPermissions; } public IEnumerable<Role> GetUserRoles(string userId) { return db.Users.FirstOrDefault(x => x.UserId == userId).Roles.ToList(); } public bool HasPermission(string userId, string area, string control) { var found = false; var userPermissions = this.GetUserPermissions(userId); var permission = userPermissions.FirstOrDefault(x => x.Area == area && x.Control == control); if (permission != null) found = true; return found; } {
تمامی حداقلهایی که برای نگارش سمت سرور نیاز بود، به پایان رسید. حال به بررسی این موضوع خواهیم پرداخت که چگونه این سطوح دسترسی را با سمت کاربر همگام نماییم. به طوری که به عنوان مثال اگر کاربری حق دسترسی به ویرایش مطالب یک سایت را ندارد، دکمه مورد نظر که او را به آن صفحه هدایت میکند نیز نباید به وی نشان داده شود. سناریویی که ما برای این کار در نظر گرفتهایم، به این گونه میباشد که در هنگام ورود کاربر، لیستی از تمامی دسترسیهای او به صورت JSON به سمت کلاینت ارسال میگردد. حال وظیفه مدیریت نمایش یا عدم نمایش المانهای صفحه، بر عهده زبان سمت کلاینت، یعنی AngularJs خواهد بود. بنابراین در ابتداییترین حالت، ما نیاز به یک کنترلر و متد Web API داریم تا لیست دسترسیهای کاربر را بازگرداند. این کنترلر در حال حاضر شامل یک متد است. اما بر حسب نیاز، میتوانید متدهای بیشتری را به کنترلر اضافه نمایید.
[RoutePrefix("َAuth/permissions")] public class PermissionsController : ApiController { private AccessControlService _AccessControlService = null; public PermissionsController() { _AccessControlService = new AccessControlService(); } [Route("GetUserPermissions")] public async Task<IHttpActionResult> GetUserPermissions() { if (!User.Identity.IsAuthenticated) { return Unauthorized(); } return Ok(_AccessControlService.GetPermissions(User.Identity.GetCurrentUserId())); } }
توسعه سرویسها و فرآیندهای سمت وب کلاینت AngularJS
در ابتدا در سمت کلاینت نیاز به سرویسی داریم که دسترسیهای سمت سرور را دریافت نماید. از این رو ما نام این سرویس را permissionService مینامیم.
'use strict'; angular.module('App').factory('permissionService', ['$http', '$q', function ($http, $q) { var _getUserPermissions = function () { return $http.get(serviceBaseUrl + '/api/permissions/GetUserPermissions/'); } var _isAuthorize = function (area, control) { return _.some($scope.permissions, { 'area': area, 'control': control }); } return { getUserPermissions: _getUserPermissions, isAuthorize: _isAuthorize }; }]);
حال باید متدهای این سرویس را در کنترلر لاگین فراخوانی نماییم. در این مرحله ما از rootScope dependency استفاده میکنیم. برای نحوهی عملکرد rootScope میتوانید به مقالهای در این زمینه در وب سایت toddomotto مراجعه کنید. در این مقاله ویژگیها و اختلافهای scope و rootScope به تفصیل بیان شده است. مقالهای دیگر در همین زمینه نوشته شده است که در انتهای مقاله به بررسی چند نکته در مورد کدهای مشترک پرداخته شدهاست. تکه کد زیر، متد login را نمایش میدهد که در loginController قرار گرفته است و ما در آن از نوشتن کل بلاک loginController چشم پوشی کردهایم. متد savePermissions تنها یک کار را انجام میدهد و آن هم این است که در ابتدا، به سرویس permissionService متصل شده و تمامی دسترسیها را واکشی مینماید و پس از آن، آنها را درون rootScope قرار میدهد تا در تمامی کنترلرها قابل دسترسی باشد.
$scope.login = function () { authService.login($scope.loginData).then(function (response) { savePermissions(); $location.path('/userPanel'); }, function (err) { $scope.message = err.error_description; }); }; var savePermissions = function () { permissionService.getUserPermissions().then(function (response) { $rootScope.permissions = response.data; }, function (err) { }); } }
App.controller('parentController', ['$rootScope', '$scope', 'authService', 'permissionService', function ($rootScope, $scope, authService, permissionService) { $scope.authentication = authService.authentication; // isAuthorize Method $scope.isAuthorize = permissionService.isAuthorize(); // rest of codes }]);
در گام بعدی کافیست المانهای صفحه را بر اساس همین دسترسیها فعال یا غیر فعال کنیم. برای این کار از دستور ng-if میتوان استفاده کرد. برای این کار به مثال زیر توجه کنید:
<div ng-controller="childController"> <div ng-if="isAuthorize('articles', 'edit')" > <!-- the block that we want to not see unauthorize person --> </div> </div>
تنها نکتهای که باقی میماند این است که طول عمر scope و rootScope چقدر است؟! برای پاسخ به این سوال باید بگوییم هر بار که صفحه refresh میشود، تمامی مقادیر scope و rootScope خالی میشوند. برای این کار هم یک راهکار خیلی ساده (و شاید کمی ناشیانه) در نظر گرفته شدهاست. میتوان بلاک مربوط به پر کردن rootScope.permissions را که در loginController نوشته شده بود، به درون parentController انتقال داد و آن را با استفاده از emit اجرا کرد و در حالت عادی، در هنگام refresh شدن صفحه نیز چون parentController در اولین لحظه اجرا میشود، میتوان تمامی مقادیر rootScope.permissions را دوباره از سمت سرور دریافت کرد.