تعریف نقشها، سادهترین روش محدود کردن دسترسی به منابع است؛ برای نمونه مشخص میکنیم که کاربران دارای نقش PayingUser، امکان سفارش دادن نگارش قاب شدهی تصاویر را داشته باشند. اما میخواهیم منطق دسترسی به منابع مختلف را پیچیدهتر کنیم. برای مثال میخواهیم بررسی کنیم اگر منبعی واقعا متعلق به کاربر جاری سیستم است، به آن دسترسی داشته باشد. برای مثال هرچند کاربر جاری دارای نقش PayingUser است، اما آیا باید اجازهی ویرایش تصاویر تمام کاربران، برای او صادر شده باشد؟ برای پیاده سازی یک چنین موارد پیچیدهای که فراتر از مفهوم نقشها هستند، ویژگی جدیدی به نام Authorization policies به ASP.NET Core اضافه شدهاست که آنرا در این قسمت بر اساس امکانات IdentityServer 4 بررسی میکنیم.
مقایسه تعریف سطوح دسترسی «مبتنی بر نقشها» با سطوح دسترسی «مبتنی بر سیاستهای امنیتی» - در سطوح دسترسی «مبتنی بر نقشها»
یکسری نقش از پیش تعریف شده وجود دارند؛ مانند PayingUser و یا FreeUser که کاربر توسط هر نقش، به یکسری دسترسیهای خاص نائل میشود. برای مثال PayingUser میتواند نگارش قاب شدهی تصاویر را سفارش دهد و یا تصویری را به سیستم اضافه کند.
- در سطوح دسترسی «مبتنی بر سیاستهای امنیتی»
سطوح دسترسی بر اساس یک سری سیاست که بیانگر ترکیبی از منطقهای دسترسی هستند، اعطاء میشوند. این منطقها نیز از طریق ترکیب User Claims حاصل میشوند و میتوانند منطقهای پیچیدهتری را به همراه داشته باشند. برای مثال اگر کاربری از کشور A است و نوع اشتراک او B است و اگر در بین یک بازهی زمانی خاصی متولد شده باشد، میتواند به منبع خاصی دسترسی پیدا کند. به این ترتیب حتی میتوان نیاز به ترکیب چندین نقش را با تعریف یک سیاست امنیتی جدید جایگزین کرد. به همین جهت نسبت به روش بکارگیری مستقیم کار با نقشها ترجیح داده میشود.
جایگزین کردن بررسی سطوح دسترسی توسط نقشها با روش بکارگیری سیاستهای دسترسی
در ادامه میخواهیم بجای بکارگیری مستقیم نقشها جهت محدود کردن دسترسی به قسمتهای خاصی از برنامهی کلاینت، تنها کاربرانی که از کشور خاصی وارد شدهاند و نیز سطح اشتراک خاصی را دارند، بتوانند دسترسیهای ویژهای داشته باشند؛ چون برای مثال امکان ارسال مستقیم تصاویر قاب شده را به کشور دیگری نداریم.
تنظیم User Claims جدید در برنامهی IDP
برای تنظیم این سیاست امنیتی جدید، ابتدا دو claim جدید subscriptionlevel و country را به خواص کاربران در کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP اضافه میکنیم:
namespace DNT.IDP
{
public static class Config
{
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
Username = "User 1",
// ...
Claims = new List<Claim>
{
// ...
new Claim("subscriptionlevel", "PayingUser"),
new Claim("country", "ir")
}
},
new TestUser
{
Username = "User 2",
// ...
Claims = new List<Claim>
{
// ...
new Claim("subscriptionlevel", "FreeUser"),
new Claim("country", "be")
}
}
};
}
سپس باید تعاریف این claims جدید را به متد GetIdentityResources افزود تا به صورت scopeهای جدید از طرف کلاینتها قابل درخواست باشند و چون این claimها استاندارد نیستند، برای تعریف آنها از IdentityResource استفاده میکنیم:
namespace DNT.IDP
{
public static class Config
{
// identity-related resources (scopes)
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
// ...
new IdentityResource(
name: "country",
displayName: "The country you're living in",
claimTypes: new List<string> { "country" }),
new IdentityResource(
name: "subscriptionlevel",
displayName: "Your subscription level",
claimTypes: new List<string> { "subscriptionlevel" })
};
}
همچنین باید مطمئن شد که کلاینت مدنظر ما قادر است این scopeهای تعریف شده را درخواست کند و IDP مجاز است تا آنها را بازگشت دهد. برای این منظور آنها را به لیست AllowedScopes تعریف کلاینت، اضافه میکنیم:
namespace DNT.IDP
{
public static class Config
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientName = "Image Gallery",
// ...
AllowedScopes =
{
// ...
"country",
"subscriptionlevel"
}
// ...
}
};
}
}
استفادهی از User Claims جدید در برنامهی MVC Client
در ادامه به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی MVC Client مراجعه کرده و دو scope جدیدی را که در سمت IDP تعریف کردیم، در اینجا در تنظیمات متد AddOpenIdConnect، درخواست میدهیم:
options.Scope.Add("subscriptionlevel");
options.Scope.Add("country");
به این ترتیب برنامهی کلاینت میتواند دسترسی به این دو claim جدید را از طریق IDP، پیدا کند.
البته همانطور که در قسمتهای قبل نیز ذکر شد، اگر claim ای در لیست نگاشتهای تنظیمات میانافزار OpenID Connect مایکروسافت نباشد، آنرا در لیست this.User.Claims ظاهر نمیکند. به همین جهت همانند claim role که پیشتر MapUniqueJsonKey را برای آن تعریف کردیم، نیاز است برای این دو claim نیز نگاشتهای لازم را به سیستم افزود:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
options.ClaimActions.MapUniqueJsonKey(claimType: "subscriptionlevel", jsonKey: "subscriptionlevel");
options.ClaimActions.MapUniqueJsonKey(claimType: "country", jsonKey: "country");
ایجاد سیاستهای دسترسی در برنامهی MVC Client
برای تعریف یک سیاست دسترسی جدید در کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی MVC Client، به متد ConfigureServices آن مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace ImageGallery.MvcClient.WebApp
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy(
name: "CanOrderFrame",
configurePolicy: policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.RequireClaim(claimType: "country", requiredValues: "ir");
policyBuilder.RequireClaim(claimType: "subscriptionlevel", requiredValues: "PayingUser");
});
});
در اینجا نحوهی تعریف یک Authorization Policy جدید را مشاهده میکنید. ابتدا یک نام برای آن تعریف میشود که در قسمتهای دیگر برنامه جهت ارجاع به آن مورد استفاده قرار میگیرد. سپس تنظیمات این سیاست دسترسی جدید را مشاهده میکنید که در آن نیاز است کاربر مدنظر حتما اعتبارسنجی شده باشد. از کشور ir بوده و همچنین سطح اشتراک او PayingUser باشد. در اینجا پارامتر requiredValues، یک آرایه را میپذیرد. بنابراین اگر برای مثال کشورهای دیگری نیز مدنظر هستند، میتوان لیست آنها را در اینجا اضافه کرد.
به علاوه policyBuilder شامل متد RequireRole نیز هست. به همین جهت است که این روش تعریف سطوح دسترسی، روش قدیمی مبتنی بر نقشها را جایگزین کرده و در برگیرندهی آن نیز میشود؛ چون در این سیستم، role نیز تنها یک claim است، مانند country و یا subscriptionlevel فوق.
بررسی نحوهی استفادهی از Authorization Policy تعریف شده و جایگزین کردن آن با روش بررسی نقشها
تا کنون از روش بررسی سطوح دسترسیها بر اساس نقشهای کاربران در دو قسمت استفاده کردهایم:
الف) اصلاح Views\Shared\_Layout.cshtml برای استفادهی از Authorization Policy
در فایل Layout با بررسی نقش PayingUser، منوهای مرتبط با این نقش را فعال میکنیم:
@if(User.IsInRole("PayingUser"))
{
<li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
برای جایگزین کردن آن جهت استفادهی از سیاست دسترسی جدید CanOrderFrame، ابتدا نیاز است در این View به سرویس IAuthorizationService دسترسی پیدا کنیم که روش تزریق آنرا در ذیل مشاهده میکنید:
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
پس از آن، روش استفادهی از این سرویس را در ذیل مشاهده میکنید:
@if (User.IsInRole("PayingUser"))
{
<li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
}
@if ((await AuthorizationService.AuthorizeAsync(User, "CanOrderFrame")).Succeeded)
{
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
اکنون لینک منوی درخواست نگارش قاب شدهی یک تصویر، صرفا به کاربران تامین کنندهی سیاست دسترسی CanOrderFrame نمایش داده میشود.
ب) اصلاح کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs برای استفادهی از Authorization Policy
namespace ImageGallery.MvcClient.WebApp.Controllers
{
[Authorize]
public class GalleryController : Controller
{
[Authorize(Policy = "CanOrderFrame")]
public async Task<IActionResult> OrderFrame()
{
در اینجا فیلتر Authorize امکان پذیرش نام یک Policy را نیز به همراه دارد.
اکنون برای آزمایش برنامه یکبار از آن خارج شده و سپس توسط اکانت User 1 که از نوع PayingUser در کشور ir است، به آن وارد شوید.
ابتدا به قسمت IdentityInformation آن وارد شوید. در اینجا لیست claims جدید را میتوانید مشاهده کنید. همچنین لینک سفارش تصویر قاب شده نیز نمایان است و میتوان به آدرس آن نیز وارد شد.
استفاده از سیاستهای دسترسی در سطح برنامهی Web API
در سمت برنامهی Web API، در حال حاضر کاربران میتوانند به متدهای Get ،Put و Delete ای که رکوردهای آنها الزاما متعلق به آنها نیست دسترسی داشته باشند. بنابراین نیاز است از ورود کاربران به متدهای تغییرات رکوردهایی که OwnerID آنها با هویت کاربری آنها تطابقی ندارد، جلوگیری کرد. در این حالت Authorization Policy تعریف شده نیاز دارد تا با سرویس کاربران و بانک اطلاعاتی کار کند. همچنین نیاز به دسترسی به اطلاعات مسیریابی جاری را برای دریافت ImageId دارد. پیاده سازی یک چنین سیاست دسترسی پیچیدهای توسط متدهای RequireClaim و RequireRole میسر نیست. خوشبختانه امکان بسط سیستم Authorization Policy با پیاده سازی یک IAuthorizationRequirement سفارشی وجود دارد. RequireClaim و RequireRole، جزو Authorization Requirementهای پیشفرض و توکار هستند. اما میتوان نمونههای سفارشی آنها را نیز پیاده سازی کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace ImageGallery.WebApi.Services
{
public class MustOwnImageRequirement : IAuthorizationRequirement
{
}
public class MustOwnImageHandler : AuthorizationHandler<MustOwnImageRequirement>
{
private readonly IImagesService _imagesService;
private readonly ILogger<MustOwnImageHandler> _logger;
public MustOwnImageHandler(
IImagesService imagesService,
ILogger<MustOwnImageHandler> logger)
{
_imagesService = imagesService;
_logger = logger;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context, MustOwnImageRequirement requirement)
{
var filterContext = context.Resource as AuthorizationFilterContext;
if (filterContext == null)
{
context.Fail();
return;
}
var imageId = filterContext.RouteData.Values["id"].ToString();
if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
{
_logger.LogError($"`{imageId}` is not a Guid.");
context.Fail();
return;
}
var subClaim = context.User.Claims.FirstOrDefault(c => c.Type == "sub");
if (subClaim == null)
{
_logger.LogError($"User.Claims don't have the `sub` claim.");
context.Fail();
return;
}
var ownerId = subClaim.Value;
if (!await _imagesService.IsImageOwnerAsync(imageIdAsGuid, ownerId))
{
_logger.LogError($"`{ownerId}` is not the owner of `{imageIdAsGuid}` image.");
context.Fail();
return;
}
// all checks out
context.Succeed(requirement);
}
}
}
در پروژهی ImageGallery.WebApi.Services ابتدا یک Authorization Requirement و سپس پیاده سازی کنندهی آن که Authorization Handler نام دارد را تعریف کردهایم. این پروژه نیاز به وابستگیهای ذیل را دارد تا کامپایل شود.
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1.0" />
</ItemGroup>
</Project>
پیاده سازی سیاستهای پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class MustOwnImageRequirement : IAuthorizationRequirement
{
}
ابتدا نیاز است یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی، خالی است و صرفا به عنوان نشانهای جهت یافت AuthorizationHandler استفاده کنندهی از آن استفاده میشود. در اینجا در صورت نیاز میتوان یک سری خاصیت اضافه را تعریف کرد تا آنها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.
2- پیاده سازی یک AuthorizationHandler استفاده کنندهی از نیازمندی دسترسی تعریف شده
که کدهای کامل آنرا در کلاس MustOwnImageHandler مشاهده میکنید. کار آن با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن توسط ASP.NET Core استفاده میشود. بنابراین در اینجا MustOwnImageRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آنرا بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری میتواند به منبع درخواستی بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت میکند.
در این پیاده سازی از filterContext.RouteData برای یافتن Id تصویر مورد نظر استفاده شدهاست. همچنین Id شخص جاری نیز از sub claim موجود استخراج گردیدهاست. اکنون این اطلاعات را به سرویس تصاویر ارسال میکنیم تا توسط متد IsImageOwnerAsync آن مشخص شود که آیا کاربر جاری سیستم، همان کاربری است که تصویر را در بانک اطلاعاتی ثبت کردهاست؟ اگر بله، با فراخوانی context.Succeed به سیستم Authorization اعلام خواهیم کرد که این سیاست دسترسی و نیازمندی مرتبط با آن با موفقیت پشت سر گذاشته شدهاست.
3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحلهی زیر است:
مراجعهی به کلاس ImageGallery.WebApi.WebApp\Startup.cs و افزودن نیازمندی آن:
namespace ImageGallery.WebApi.WebApp
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
name: "MustOwnImage",
configurePolicy: policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new MustOwnImageRequirement());
});
});
services.AddScoped<IAuthorizationHandler, MustOwnImageHandler>();
ابتدا باید MustOwnImageHandler تهیه شده را به سیستم تزریق وابستگیها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه MustOwnImage تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه میکنیم. همانطور که ملاحظه میکنید یک وهلهی جدید از MustOwnImageRequirement در اینجا ثبت شدهاست. همین وهله به متد HandleRequirementAsync نیز ارسال میشود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، میتوان خواص مرتبطی را به کلاس MustOwnImageRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را میتوان ثبت کرد و نه Handler آنرا. این Handler از سیستم تزریق وابستگیها بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا میشود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آنرا به سیستم تزریق وابستگیها معرفی کردهاید).
پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy ="MustOwnImage")]
به صورت خودکار توسط MustOwnImageHandler مدیریت میشود.
اعمال سیاست دسترسی پویای تعریف شده به Web API
پس از تعریف سیاست دسترسی MustOwnImage که پویا عمل میکند، اکنون نوبت به استفادهی از آن در کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs است:
namespace ImageGallery.WebApi.WebApp.Controllers
{
[Route("api/images")]
[Authorize]
public class ImagesController : Controller
{
[HttpGet("{id}", Name = "GetImage")]
[Authorize("MustOwnImage")]
public async Task<IActionResult> GetImage(Guid id)
{
}
[HttpDelete("{id}")]
[Authorize("MustOwnImage")]
public async Task<IActionResult> DeleteImage(Guid id)
{
}
[HttpPut("{id}")]
[Authorize("MustOwnImage")]
public async Task<IActionResult> UpdateImage(Guid id, [FromBody] ImageForUpdateModel imageForUpdate)
{
}
}
}
در اینجا در سه قسمت GetImage ،DeleteImage و UpdateImage با اعمال سیاست دسترسی پویای MustOwnImage، اگر کاربر جاری همان Owner تصویر درخواستی نباشد، دسترسی او به اجرای کدهای داخل این اکشن متدها به صورت خودکار بسته خواهد شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.