اعمال کنترل دسترسی پویا در پروژه‌های ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

در مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا» به طور مفصل به قضیه کنترل دسترسی پویا در ASP.NET Core Identity پرداخته شده‌است؛ در این مطلب روش دیگری را بررسی خواهیم کرد.

مشخص می‌باشد که بدون وابستگی به روش خاصی، خیلی ساده می‌توان به شکل زیر عمل کرد:

services.AddAuthorization(options =>
{
    options.AddPolicy("View Projects", 
        policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view"));
});
با یک ClaimType مشخص برای دسترسی‌ها، یک سیاست جدید را تعریف کرده و برای استفاده از آن نیز همانند قبل به شکل زیر می‌توان عمل کرد:
[Authorize("View Projects")]
public IActionResult Index(int siteId)
{
    return View();
}
روشی یکپارچه و بدون نیاز به کوچکترین سفارشی سازی؛ ولی در مقیاس بزرگ تعریف سیاست‌ها برای تک تک دسترسی‌های مورد نیاز، قطعا آزار دهنده خواهد بود. خبر خوب اینکه زیرساخت احراز هویت و کنترل دسترسی در ASP.NET Core مکانیزمی برای خودکار کردن فرآیند تعریف options.AddPolicy‌ها در کلاس آغازین برنامه، ارائه داده‌است که دقیقا یکی از موارد استفاده‌ی آن، راه حلی می‌باشد برای همین مشکلی که مطرح شد.
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to add each individual authorization policy with an AuthorizationOptions.AddPolicy call. 


کار با پیاده سازی واسط IAuthorizationPolicyProvider شروع می‌شود؛ یا شاید ارث بری از DefaultAuthorizationPolicyProvider رجیستر شده‌ی در سیستم DI و توسعه آن هم کافی باشد.

public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
        : base(options)
    {
    }

    public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        if (!policyName.StartsWith(PermissionAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase))
        {
            return base.GetPolicyAsync(policyName);
        }

        var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(',');

        var policy = new AuthorizationPolicyBuilder()
            .RequireClaim(CustomClaimTypes.Permission, permissionNames)
            .Build();

        return Task.FromResult(policy);
    }
}

متد GetPolicyAsync موظف به یافتن و بازگشت یک Policy ثبت شده می‌باشد؛ با این حال می‌توان با بازنویسی آن و با استفاده از وهله‌ای از AuthorizationPolicyBuilder، فرآیند تعریف سیاست درخواست شده را که احتمالا در تنظیمات آغازین پروژه تعریف نشده و پیشوند مدنظر را نیز دارد، خوکار کرد. در اینجا امکان ترکیب کردن چندین دسترسی را هم خواهیم داشت که برای این منظور می‌توان دسترسی‌های مختلف را به صورت comma separated به سیستم معرفی کرد. 

نکته‌ی مهم در تکه کد بالا مربوط است به PolicyPrefix که با استفاده از آن مشخص کرده‌ایم که برای هر سیاست درخواستی، این فرآیند را طی نکند و موجب اختلال در سیستم نشود. 


پس از پیاده سازی واسط مطرح شده، لازم است این پیاده سازی جدید را به سیستم DI هم معرفی کنید:

services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();


خوب، تا اینجا فرآیند تعریف سیاست‌ها به صورت خودکار انجام شد. در ادامه نیاز است با تعریف یک فیلتر Authorization، بتوان لیست دسترسی‌های مورد نظر برای اکشنی خاص را نیز مشخص کرد تا در متد GetPolicyAsync فوق، کار ثبت خودکار سیاست دسترسی متناظر با آن‌را توسط فراخوانی متد policyBuilder.RequireClaim، انجام دهد تا دیگر نیازی به تعریف دستی و جداگانه‌ی آن، در کلاس آغازین برنامه نباشد. برای این منظور به شکل زیر عمل خواهیم کرد:

    public class PermissionAuthorizeAttribute : AuthorizeAttribute
    {
        internal const string PolicyPrefix = "PERMISSION:";

        /// <summary>
        /// Creates a new instance of <see cref="AuthorizeAttribute"/> class.
        /// </summary>
        /// <param name="permissions">A list of permissions to authorize</param>
        public PermissionAuthorizeAttribute(params string[] permissions)
        {
            Policy = $"{PolicyPrefix}{string.Join(",", permissions)}";
        }
    }

همانطور که مشخص می‌باشد، رشته PERMISSION به عنوان پیشوند رشته تولیدی از لیست اسامی دسترسی‌ها، استفاده شده‌است و در پراپرتی Policy قرار داده شده‌است. این بار برای کنترل دسترسی می‌توان به شکل زیر عمل کرد:

[PermissionAuthorize(PermissionNames.Projects_View)]
public IActionResult Get(FilteredQueryModel query)
{
   //...
}
[PermissionAuthorize(PermissionNames.Projects_Create)]
public IActionResult Post(ProjectModel model)
{
   //...
}

برای مثال در اولین فراخوانی فیلتر PermissionAuthorize فوق، مقدار ثابت PermissionNames.Projects_View به عنوان یک Policy جدید به متد GetPolicyAsync کلاس AuthorizationPolicyProvider سفارشی ما ارسال می‌شود. چون دارای پیشوند «:PERMISSION» است، مورد پردازش قرار گرفته و توسط متد policyBuilder.RequireClaim به صورت خودکار به سیستم معرفی و ثبت خواهد شد.


همچنین راه حل مطرح شده برای مدیریت دسترسی‌های پویا، در gist به اشتراک گذاشته شده «موجودیت‌های مرتبط با مدیریت دسترسی‌های پویا» را نیز مد نظر قرار دهید.

  • #
    ‫۵ سال و ۱۰ ماه قبل، یکشنبه ۲۰ آبان ۱۳۹۷، ساعت ۰۵:۵۶
    با توجه به اینکه برای هر درخواست رسیده به اکشن متد تزئین شده با PermissionAuthorizeAttribute لازم است تا وهله سازی جدید از AuthorizationPolicy برای policyName دریافتی صورت پذیرد، شاید بهتر باشد برای بهبود کارایی سیستم، تغییرات زیر را اعمال کرد:
     public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
        {
            private readonly LazyConcurrentDictionary<string, AuthorizationPolicy> _policies =
                new LazyConcurrentDictionary<string, AuthorizationPolicy>();
    
            public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
                : base(options)
            {
            }
    
            public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
            {
               //...
            }
        }
    با توجه به مطلب «الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary»، مخزنی امن برای دسترسی همزمان به لیست سیاست‌های درخواست شده قبلی تحت عنوان ‎ _policies ایجاد شده است. سپس در بدنه متد GetPolicyAsync تغییرات زیر را اعمال کنید:
            public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
            {
              //...
    
                var policy = _policies.GetOrAdd(policyName, name =>
                {
                    var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(',');
    
                    return new AuthorizationPolicyBuilder()
                        .RequireClaim(CustomClaimTypes.Permission, permissionNames)
                        .Build();
                });
    
                return policy;
            }

  • #
    ‫۵ سال و ۱ ماه قبل، یکشنبه ۱۳ مرداد ۱۳۹۸، ساعت ۲۲:۴۶
    جهت اطلاع
    به عنوان راهکاری جهت کاهش حجم کوکی/توکن احراز هویت، امکان PackedPermission اضافه شد.
    سوال: بدون نگهداری لیست دسترسی‌های پویای کاربر در کوکی/توکن، بازهم امکان استفاده از این زیرساخت را خواهیم داشت؟
    بله؛ اگر قصد استفاده از مکانیزم PackedPermission را ندارید، با بازطراحی انجام شده و به دلیل عدم فراخوانی context.Fail()‎ در هندلر پیش فرض، امکان پیاده سازی AuthorizationHandler متناظر با PermissionAuthorizationRequriment را خواهید داشت که می‎تواند مستقیم از بانک اطلاعاتی نیز بررسی دسترسی به منابع مورد نظر را انجام دهد.

    claims.Add(new Claim(UserClaimTypes.PackedPermission,
        new[] {"48", "65", "6C", "6C", "6F", "20", "57", "6F", "72", "6C", "64", "21"}
            .PackPermissionsToString()));