مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هفتم- امن سازی Web API
تا اینجا بجای قرار دادن مستقیم قسمت مدیریت هویت کاربران، داخل یک یا چند برنامه‌ی مختلف، این دغدغه‌ی مشترک (common concern) بین برنامه‌ها را به یک برنامه‌ی کاملا مجزای دیگری به نام Identity provider و یا به اختصار IDP منتقل و همچنین دسترسی به کلاینت MVC برنامه‌ی گالری تصاویر را نیز توسط آن امن سازی کردیم. اما هنوز یک قسمت باقی مانده‌است: برنامه‌ی کلاینت MVC، منابع خودش را از یک برنامه‌ی Web API دیگر دریافت می‌کند و هرچند دسترسی به برنامه‌ی MVC امن شده‌است، اما دسترسی به منابع برنامه‌ی Web API آن کاملا آزاد و بدون محدودیت است. بنابراین امن سازی Web API را توسط IDP، در این قسمت پیگیری می‌کنیم. پیش از مطالعه‌ی این قسمت نیاز است مطلب «آشنایی با JSON Web Token» را مطالعه کرده و با ساختار ابتدایی یک JWT آشنا باشید.


بررسی Hybrid Flow جهت امن سازی Web API

این Flow را پیشتر نیز مرور کرده بودیم. تفاوت آن با قسمت‌های قبل، در استفاده از توکن دومی است به نام access token که به همراه identity token از طرف IDP صادر می‌شود و تا این قسمت از آن بجز در قسمت «دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint» استفاده نکرده بودیم.


در اینجا، ابتدا برنامه‌ی وب، یک درخواست اعتبارسنجی را به سمت IDP ارسال می‌کند که response type آن از نوع code id_token است (یا همان مشخصه‌ی Hybrid Flow) و همچنین تعدادی scope نیز جهت دریافت claims متناظر با آن‌ها در این درخواست ذکر شده‌اند. در سمت IDP، کاربر با ارائه‌ی مشخصات خود، اعتبارسنجی شده و پس از آن IDP صفحه‌ی اجازه‌ی دسترسی به اطلاعات کاربر (صفحه‌ی consent) را ارائه می‌دهد. پس از آن IDP اطلاعات code و id_token را به سمت برنامه‌ی وب ارسال می‌کند. در ادامه کلاینت وب، توکن هویت رسیده را اعتبارسنجی می‌کند. پس از موفقیت آمیز بودن این عملیات، اکنون کلاینت درخواست دریافت یک access token را از IDP ارائه می‌دهد. اینکار در پشت صحنه و بدون دخالت کاربر صورت می‌گیرد که به آن استفاده‌ی از back channel هم گفته می‌شود. یک چنین درخواستی به token endpoint، شامل اطلاعات code و مشخصات دقیق کلاینت جاری است. به عبارتی نوعی اعتبارسنجی هویت برنامه‌ی کلاینت نیز می‌باشد. در پاسخ، دو توکن جدید را دریافت می‌کنیم: identity token و access token. در اینجا access token توسط خاصیت at_hash موجود در id_token به آن لینک می‌شود. سپس هر دو توکن اعتبارسنجی می‌شوند. در این مرحله، میان‌افزار اعتبارسنجی، هویت کاربر را از identity token استخراج می‌کند. به این ترتیب امکان وارد شدن به برنامه‌ی کلاینت میسر می‌شود. در اینجا همچنین access token ای نیز صادر شده‌است.
اکنون علاقمند به کار با Web API برنامه‌ی کلاینت MVC خود هستیم. برای این منظور access token که اکنون در برنامه‌ی MVC Client در دسترس است، به صورت یک Bearer token به هدر ویژه‌ای با کلید Authorization اضافه می‌شود و به همراه هر درخواست، به سمت API ارسال خواهد شد. در سمت Web API این access token رسیده، اعتبارسنجی می‌شود و در صورت موفقیت آمیز بودن عملیات، دسترسی به منابع Web API صادر خواهد شد.


امن سازی دسترسی به Web API

تنظیمات برنامه‌ی IDP
برای امن سازی دسترسی به Web API از کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP شروع می‌کنیم. در اینجا باید یک scope جدید مخصوص دسترسی به منابع Web API را تعریف کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
            };
        }
هدف آن داشتن access token ای است که در قسمت Audience آن، نام این ApiResource، درج شده باشد؛ پیش از اینکه دسترسی به API را پیدا کند. برای تعریف آن، متد جدید GetApiResources را به صورت فوق به کلاس Config اضافه می‌کنیم.
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر می‌کنیم:
AllowedScopes =
{
  IdentityServerConstants.StandardScopes.OpenId,
  IdentityServerConstants.StandardScopes.Profile,
  IdentityServerConstants.StandardScopes.Address,
  "roles",
  "imagegalleryapi"
},
اکنون باید متد جدید GetApiResources را به کلاس src\IDP\DNT.IDP\Startup.cs معرفی کنیم که توسط متد AddInMemoryApiResources به صورت زیر قابل انجام است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddTestUsers(Config.GetUsers())
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
        }

تنظیمات برنامه‌ی MVC Client
اکنون نوبت انجام تنظیمات برنامه‌ی MVC Client در فایل ImageGallery.MvcClient.WebApp\Startup.cs است. در اینجا در متد AddOpenIdConnect، درخواست scope جدید imagegalleryapi را صادر می‌کنیم:
options.Scope.Add("imagegalleryapi");

تنظیمات برنامه‌ی Web API
اکنون می‌خواهیم مطمئن شویم که Web API، به access token ای که قسمت Audience آن درست مقدار دهی شده‌است، دسترسی خواهد داشت.
برای این منظور به پوشه‌ی پروژه‌ی Web API در مسیر src\WebApi\ImageGallery.WebApi.WebApp وارد شده و دستور زیر را صادر کنید تا بسته‌ی نیوگت AccessTokenValidation نصب شود:
dotnet add package IdentityServer4.AccessTokenValidation
اکنون کلاس startup در سطح Web API را در فایل src\WebApi\ImageGallery.WebApi.WebApp\Startup.cs به صورت زیر تکمیل می‌کنیم:
using IdentityServer4.AccessTokenValidation;

namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme)
               .AddIdentityServerAuthentication(options =>
               {
                   options.Authority = Configuration["IDPBaseAddress"];
                   options.ApiName = "imagegalleryapi";
               });
متد AddAuthentication یک defaultScheme را تعریف می‌کند که در بسته‌ی IdentityServer4.AccessTokenValidation قرار دارد و این scheme در اصل دارای مقدار Bearer است.
سپس متد AddIdentityServerAuthentication فراخوانی شده‌است که به آدرس IDP اشاره می‌کند که مقدار آن‌را در فایل appsettings.json قرار داده‌ایم. از این آدرس برای بارگذاری متادیتای IDP استفاده می‌شود. کار دیگر این میان‌افزار، اعتبارسنجی access token رسیده‌ی به آن است. مقدار خاصیت ApiName آن، به نام API resource تعریف شده‌ی در سمت IDP اشاره می‌کند. هدف این است که بررسی شود آیا خاصیت aud موجود در access token رسیده به مقدار imagegalleryapi تنظیم شده‌است یا خیر؟

پس از تنظیم این میان‌افزار، اکنون نوبت به افزودن آن به ASP.NET Core request pipeline است:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
محل فراخوانی UseAuthentication باید پیش از فراخوانی app.UseMvc باشد تا پس از اعتبارسنجی درخواست، به میان‌افزار MVC منتقل شود.

اکنون می‌توانیم اجبار به Authorization را در تمام اکشن متدهای این Web API در فایل ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs فعالسازی کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {


ارسال Access Token به همراه هر درخواست به سمت Web API

تا اینجا اگر مراحل اجرای برنامه‌ها را طی کنید، مشاهده خواهید کرد که برنامه‌ی MVC Client دیگر کار نمی‌کند و نمی‌تواند از فیلتر Authorize فوق رد شود. علت اینجا است که در حال حاضر، تمامی درخواست‌های رسیده‌ی به Web API، فاقد Access token هستند. بنابراین اعتبارسنجی آن‌ها با شکست مواجه می‌شود.
برای رفع این مشکل، سرویس ImageGalleryHttpClient را به نحو زیر اصلاح می‌کنیم تا در صورت وجود Access token، آن‌را به صورت خودکار به هدرهای ارسالی توسط HttpClient اضافه کند:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace ImageGallery.MvcClient.Services
{
    public interface IImageGalleryHttpClient
    {
        Task<HttpClient> GetHttpClientAsync();
    }

    /// <summary>
    /// A typed HttpClient.
    /// </summary>
    public class ImageGalleryHttpClient : IImageGalleryHttpClient
    {
        private readonly HttpClient _httpClient;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public ImageGalleryHttpClient(
            HttpClient httpClient,
            IConfiguration configuration,
            IHttpContextAccessor httpContextAccessor)
        {
            _httpClient = httpClient;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }

        public async Task<HttpClient> GetHttpClientAsync()
        {
            var currentContext = _httpContextAccessor.HttpContext;
            var accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                _httpClient.SetBearerToken(accessToken);
            }

            _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]);
            _httpClient.DefaultRequestHeaders.Accept.Clear();
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            return _httpClient;
        }
    }
}
اسمبلی این سرویس برای اینکه به درستی کامپایل شود، نیاز به این وابستگی‌ها نیز دارد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="5.2.0.0" />
    <PackageReference Include="IdentityModel" Version="3.9.0" />
  </ItemGroup>
</Project>
در اینجا با استفاده از سرویس IHttpContextAccessor، به HttpContext جاری درخواست دسترسی یافته و سپس توسط متد GetTokenAsync، توکن دسترسی آن‌را استخراج می‌کنیم. سپس این توکن را در صورت وجود، توسط متد SetBearerToken به عنوان هدر Authorization از نوع Bearer، به سمت Web API ارسال خواهیم کرد.
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید GetHttpClientAsync بجای خاصیت HttpClient قبلی استفاده کرد.

اکنون اگر برنامه را اجرا کنیم، پس از لاگین، دسترسی به Web API امن شده، برقرار شده و برنامه بدون مشکل کار می‌کند.


بررسی محتوای Access Token

اگر بر روی سطر if (!string.IsNullOrWhiteSpace(accessToken)) در سرویس ImageGalleryHttpClient یک break-point را قرار دهیم و محتویات Access Token را در حافظه ذخیره کنیم، می‌توانیم با مراجعه‌ی به سایت jwt.io، محتویات آن‌را بررسی نمائیم:


که در حقیقت این محتوا را به همراه دارد:
{
  "nbf": 1536394771,
  "exp": 1536398371,
  "iss": "https://localhost:6001",
  "aud": [
    "https://localhost:6001/resources",
    "imagegalleryapi"
  ],
  "client_id": "imagegalleryclient",
  "sub": "d860efca-22d9-47fd-8249-791ba61b07c7",
  "auth_time": 1536394763,
  "idp": "local",  
  "role": "PayingUser",
  "scope": [
    "openid",
    "profile",
    "address",
    "roles",
    "imagegalleryapi"
  ],
  "amr": [
    "pwd"
  ]
}
در اینجا در لیست scope، مقدار imagegalleryapi وجود دارد. همچنین در قسمت audience و یا aud نیز ذکر شده‌است. بنابراین یک چنین توکنی قابلیت دسترسی به Web API تنظیم شده‌ی ما را دارد.
همچنین اگر دقت کنید، Id کاربر جاری در خاصیت sub آن قرار دارد.


مدیریت صفحه‌ی عدم دسترسی به Web API

با اضافه شدن scope جدید دسترسی به API در سمت IDP، این مورد در صفحه‌ی دریافت رضایت کاربر نیز ظاهر می‌شود:


در این حالت اگر کاربر این گزینه را انتخاب نکند، پس از هدایت به برنامه‌ی کلاینت، در سطر response.EnsureSuccessStatusCode استثنای زیر ظاهر خواهد شد:
An unhandled exception occurred while processing the request.
HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
 System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
برای اینکه این صفحه‌ی نمایش استثناء را با صفحه‌ی عدم دسترسی جایگزین کنیم، می‌توان پس از دریافت response از سمت Web API، به StatusCode مساوی Unauthorized = 401 به صورت زیر عکس‌العمل نشان داد:
        public async Task<IActionResult> Index()
        {
            var httpClient = await _imageGalleryHttpClient.GetHttpClientAsync();
            var response = await httpClient.GetAsync("api/images");

            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
                response.StatusCode == System.Net.HttpStatusCode.Forbidden)
            {
                return RedirectToAction("AccessDenied", "Authorization");
            }
            response.EnsureSuccessStatusCode();


فیلتر کردن تصاویر نمایش داده شده بر اساس هویت کاربر وارد شده‌ی به سیستم

تا اینجا هرچند دسترسی به API امن شده‌است، اما هنوز کاربر وارد شده‌ی به سیستم می‌تواند تصاویر سایر کاربران را نیز مشاهده کند. بنابراین قدم بعدی امن سازی API، عکس العمل نشان دادن به هویت کاربر جاری سیستم است.
برای این منظور به کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs سمت API مراجعه کرده و Id کاربر جاری را از لیست Claims او استخراج می‌کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet()]
        public async Task<IActionResult> GetImages()
        {
            var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
اگر به قسمت «بررسی محتوای Access Token» مطلب جاری دقت کنید، مقدار Id کاربر در خاصیت sub این Access token قرار گرفته‌است که روش دسترسی به آن‌را در ابتدای اکشن متد GetImages فوق ملاحظه می‌کنید.
مرحله‌ی بعد، مراجعه به ImageGallery.WebApi.Services\ImagesService.cs و تغییر متد GetImagesAsync است تا صرفا بر اساس ownerId دریافت شده کار کند:
namespace ImageGallery.WebApi.Services
{
    public class ImagesService : IImagesService
    {
        public Task<List<Image>> GetImagesAsync(string ownerId)
        {
            return _images.Where(image => image.OwnerId == ownerId).OrderBy(image => image.Title).ToListAsync();
        }
پس از این تغییرات، اکشن متد GetImages سمت API چنین پیاده سازی را پیدا می‌کند که در آن بر اساس Id شخص وارد شده‌ی به سیستم، صرفا لیست تصاویر مرتبط با او بازگشت داده خواهد شد و نه لیست تصاویر تمام کاربران سیستم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet()]
        public async Task<IActionResult> GetImages()
        {
            var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
            var imagesFromRepo = await _imagesService.GetImagesAsync(ownerId);
            var imagesToReturn = _mapper.Map<IEnumerable<ImageModel>>(imagesFromRepo);
            return Ok(imagesToReturn);
        }
اکنون اگر از برنامه‌ی کلاینت خارج شده و مجددا به آن وارد شویم، تنها لیست تصاویر مرتبط با کاربر وارد شده، نمایش داده می‌شوند.

هنوز یک مشکل دیگر باقی است: سایر اکشن متدهای این کنترلر Web API همچنان محدود به کاربر جاری نشده‌اند. یک روش آن تغییر دستی تمام کدهای آن است. در این حالت متد IsImageOwnerAsync زیر، جهت بررسی اینکه آیا رکورد درخواستی متعلق به کاربر جاری است یا خیر، به سرویس تصاویر اضافه می‌شود:
namespace ImageGallery.WebApi.Services
{
    public class ImagesService : IImagesService
    {
        public Task<bool> IsImageOwnerAsync(Guid id, string ownerId)
        {
            return _images.AnyAsync(i => i.Id == id && i.OwnerId == ownerId);
        }
و سپس در تمام اکشن متدهای دیگر، در ابتدای آن‌ها باید این بررسی را انجام دهیم و در صورت شکست آن return Unauthorized را بازگشت دهیم.
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی می‌کنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس Authorization policy است. در این حالت اگر کاربری مجوز انجام عملیاتی را نداشت، اصلا وارد کدهای یک اکشن متد نخواهد شد.


ارسال سایر User Claims مانند نقش‌ها به همراه یک Access Token

برای تکمیل قسمت ارسال تصاویر می‌خواهیم تنها کاربران نقش خاصی قادر به انجام اینکار باشند. اما اگر به محتوای access token ارسالی به سمت Web API دقت کرده باشید، حاوی Identity claims نیست. البته می‌توان مستقیما در برنامه‌ی Web API با UserInfo Endpoint، برای دریافت اطلاعات بیشتر، کار کرد که نمونه‌ای از آن‌را در قسمت قبل مشاهده کردید، اما مشکل آن زیاد شدن تعداد رفت و برگشت‌های به سمت IDP است. همچنین باید درنظر داشت که فراخوانی مستقیم UserInfo Endpoint جهت برنامه‌ی MVC client که درخواست دریافت access token را از IDP می‌دهد، متداول است و نه برنامه‌ی Web API.
برای رفع این مشکل باید در حین تعریف ApiResource، لیست claim مورد نیاز را هم ذکر کرد:
namespace DNT.IDP
{
    public static class Config
    {
        // api-related resources (scopes)
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(
                    name: "imagegalleryapi",
                    displayName: "Image Gallery API",
                    claimTypes: new List<string> {"role" })
            };
        }
در اینجا ذکر claimTypes است که سبب خواهد شد نقش کاربر جاری به توکن دسترسی اضافه شود.

سپس کار با اکشن متد CreateImage در سمت API را به نقش PayingUser محدود می‌کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpPost]
        [Authorize(Roles = "PayingUser")]
        public async Task<IActionResult> CreateImage([FromBody] ImageForCreationModel imageForCreation)
        {
همچنین در این اکشن متد، پیش از فراخوانی متد AddImageAsync نیاز است مشخص کنیم OwnerId این تصویر کیست تا رکورد بانک اطلاعاتی تصویر آپلود شده، دقیقا به اکانت متناظری در سمت IDP مرتبط شود:
var ownerId = User.Claims.FirstOrDefault(c => c.Type == "sub").Value;
imageEntity.OwnerId = ownerId;
// add and save.
await _imagesService.AddImageAsync(imageEntity);

نکته‌ی مهم: در اینجا نباید این OwnerId را از سمت برنامه‌ی کلاینت MVC به سمت برنامه‌ی Web API ارسال کرد. برنامه‌ی Web API باید این اطلاعات را از access token اعتبارسنجی شده‌ی رسیده استخراج و استفاده کند؛ از این جهت که دستکاری اطلاعات اعتبارسنجی نشده‌ی ارسالی به سمت Web API ساده‌است؛ اما access tokenها دارای امضای دیجیتال هستند.

در سمت کلاینت نیز در فایل ImageGallery.MvcClient.WebApp\Views\Shared\_Layout.cshtml نمایش لینک افزودن تصویر را نیز محدود به 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>
}
علاوه بر آن، در کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs نیاز است فیلتر Authorize زیر نیز به اکشن متد نمایش صفحه‌ی AddImage اضافه شود تا فراخوانی مستقیم آدرس آن در مرورگر، توسط سایر کاربران میسر نباشد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Roles = "PayingUser")]
        public IActionResult AddImage()
        {
            return View();
        }
این مورد را باید به متد AddImage در حالت دریافت اطلاعات از کاربر نیز افزود تا اگر شخصی مستقیما با این قسمت کار کرد، حتما سطح دسترسی او بررسی شود:
[HttpPost]
[Authorize(Roles = "PayingUser")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddImage(AddImageViewModel addImageViewModel)

برای آزمایش این قسمت یکبار از برنامه خارج شده و سپس با اکانت User 1 که PayingUser است به سیستم وارد شوید. در ادامه از منوی بالای سایت، گزینه‌ی Add an image را انتخاب کرده و تصویری را آپلود کنید. پس از آن، این تصویر آپلود شده را در لیست تصاویر صفحه‌ی اول سایت، مشاهده خواهید کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
نظرات مطالب
ASP.NET MVC #18
این مورد بستگی به طراحی و آنالیز برنامه دارد. در جایی مانند صفحه تنظیمات SMTP Server برنامه، سطح دسترسی، فقط مدیریتی است و پویایی آن معنایی ندارد. در جای دیگری قرار است مطالبی ارسال شوند، اینجا دسترسی به دو نقش ادیتور و ادمین که شامل عده‌ای خواهند بود مفهوم دارد.
اما اگر سایتی پویا طراحی شده و از روز اول طرح دقیقی ندارد، مثلا در آن صفحات مختلفی به صورت پویا اضافه می‌شوند و قرار است به هر صفحه‌ای نقش(های) خاصی انتساب داده شوند که از روز اول پیش بینی و طراحی نشده، فقط کافی است همین فیلتر AuthorizeAttribute را با ارث بری از آن سفارشی سازی کرد تا بر اساس آدرس صفحه جاری (filterContext.HttpContext.Request.Url)، نقش‌های انتساب داده شده را از بانک اطلاعاتی بخواند و تصمیم گیری کند که آیا کاربر لاگین شده به سیستم (filterContext.HttpContext.User) می‌تواند به این صفحه دسترسی داشته باشد یا خیر و هدایتش کند (filterContext.HttpContext.Response.Redirect) به صفحه لاگین برنامه (FormsAuthentication.LoginUrl). یا حتی در اینجا بر اساس نام کنترلر و اکشن متد جاری هم می‌توان تصمیم گیری کرد:
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName
filterContext.ActionDescriptor.ActionName
مطالب
استاندارد وب WIA-ARIA
چند روز پیش مطلبی به عنوان اشتراک در سایت جاری معرفی شده که به ما یادآوری می‌کرد، ما تنها استفاده کنندگان سیستم‌های کامپیوتری، به خصوص اینترنت نیستیم و معلولین هم نیازمند استفاده از این فناوری‌ها هستند.
WAI-ARIA  که برگرفته از Web Accessibility Initiative - Accessible Rich internet Application است به معنی برنامه‌ی اینترنتی تعامل گرا با خاصیت دسترسی پذیری بالا می‌باشد و یک راهنماست که توسط کنسرسیوم وب (+ ) معرفی گشته است تا وب سایت‌ها با رعایت این قوانین، دسترسی سایت خود را بالاتر ببرند. این قوانین به خصوص برای سایت‌هایی با محتوای پویا هستند که از فناوری‌هایی چون Ajax,Javascrip, HTML و دیگر فناوری‌های مرتبط استفاده می‌کنند.
امروزه طراحان وب بیش از هر وقتی از فناوری‌های سمت کلاینت چون جاوااسکریپت برای ساخت رابط‌های کاربری استفاده می‌کنند که html به تنها قادر به ایجاد آن‌ها نیست. یکی از تکنیک‌های جاوااسکریپت، دریافت محتوای جدید و به روزآوری قسمتی از صفحات وب است بدون اینکه مجددا کل صفحه از وب سرور درخواست گردد که به این تکنیک  Rich Internet Application هم می‌گویند. تا به اینجای کار هیچ مشکلی نیست و خوب هم هست؛ ولی مشکلی که در این بین وجود دارد این است که این نوع تکنیک‌ها باعث از بین رفتن خاصیت دسترسی پذیری معلولین می‌گردند. معلولینی که از صفحه خوان ها استفاده می‌کنند یا به دلیل معلولیت‌های خود قادر به حرکت دادن ماوس نیستند.
ARIA با استفاده از خصوصیت‌ها Properties، نقش‌ها Roles و وضعیت‌ها States به طراحان برنامه‌های وب و سازندگان فناوری‌های یاری رسان، اجازه می‌دهد که با ابزارهای کمکی معلولان ارتباط برقرار کنیم و یک صفحه‌ی وب ساده را به یک صفحه‌ی پویا تبدیل کنیم. ARIA تنها یک استاندارد برای وب نیست، بلکه یک فناوری چند پلتفرمه است که برای بازی‌های رایانه‌ای، موبایل‌ها، دستگاه‌های سرگرمی و سلامتی و دیگر انواع برنامه‌ها نیز تعریف شده است.



فریمورک ARIA
خود HTML به تنهایی نمی‌تواند نقش‌های هر المان و ارتباط بین آن‌ها را به درستی بیان کند و به این منظور ARIA به کمک می‌آید. با استفاده از نقش‌ها میتوان هدف هر المان را مشخص کرد و با استفاده از خصوصیات ARIA نحوه‌ی عملکرد آن‌ها را تعریف کرد.

نقش ها
نقش‌ها طبق مستندات کنسرسیوم وب بر 4 نوع هستند:
  • Abstract Roles
  • Widget Roles
  • Document Roles
  • Landmark Roles
فریمورک ARIA با استفاده از Landmark Roles یا نقش‌های راهنما، به معرفی بخش‌های ویژوالی می‌پردازد تا فناوری‌های کمکی به آن دسترسی سریعی داشته باشند. هشت نقش راهنما وجود دارد که در زیر آن‌ها را بررسی می‌کنیم.

 Banner این قسمت که عموما برای اجزای مهمی مثل هدر سایت قرار می‌گیرد و شامل معرفی وب سایت هست و در همه‌ی صفحات وجود دارد که شامل لوگو، اطلاعات عمومی سایت و اسپانسرها و ... می‌گردد و بسیار مهم است که تنها یکبار در صفحه‌ی وب به کار برود و تکرار آن پرهیز شود.
 Main  این نقش به محتوای اصلی وب سایت اشاره می‌کند و نباید بیشتر از یکبار در هر صفحه‌ی وب به کار برود و عموما بهتر است این خصوصیت در تگ div قرار گیرد:
<div Role="main"></div>
یا در HTML5 به طور مفهومی ساخته می‌شود:
<main role="main">.....

Navigation اشاره به یک ناحیه پر از المان‌های لینک برای ارتباط با صفحات دیگر
 Complementary  مشخص سازی ناحیه‌ای که اطلاعات اضافی درباره‌ی محتوای اصلی سایت دارد؛ مانند بخش مقالات مرتبط، آخرین کامنت‌ها و ...
 ContentInfo  این نقش که بیشتر برای فوتر مناسب است برای محتوایی به کار می‌رود که در آن به قوانین کپی رایت و ... اشاره می‌شود.
 form  برای اشاره به فرم‌ها که دارای قسمت‌های ورودی کاربر هستند.
 search
 در صورتیکه فرمی دارید و از آن برای گزینه‌ی جست و جو استفاده می‌کنید، از این نقش استفاده کنید.
 application  برای اینکه وب سایت خود را به صورت یک وب اپلیکیشن معرفی کنید؛ تا یک صفحه وب معمولی، استفاده می‌شود و برای وب سایت‌های قدیمی یا با حالت سنتی توصیه نمی‌شود و به برنامه‌های کمکیار معلولین می‌گوند که از حالت عادی به حالت application سوئیچ کنند؛ پس با دقت بیشتری باید از این گزینه استفاده کرد.
 


خصوصیت‌ها و وضعیت ها

در حالیکه از نقش‌ها برای معرفی هر المان یا تگ استفاده می‌کنید؛ خصویت‌ها و وضعیت‌ها به کاربر اطلاعات اضافی می‌دهند که چگونه ز آن استفاده کنند. برای معرفی خصوصیت‌ها و وضعیت‌ها از یک خصوصیت که با -aria شروع می‌شود استفاده کنید. از معروفترین آن‌ها خصوصیت aria-required و وضعیت aria-checked می‌باشند، تا به ترتیب به کاربر اعلام کنید این تگ نیاز به پر شدن دارد، یا المانی نیاز به تغییر وضعیت انتخابی دارد.
نحوه‌ی استفاده از آن‌ها به شکل زیر است:
<div id="some-id" class="some-class" aria-live="assertive"><div>
aria-live سه مقدار می‌پذیرد که عبارتند از Off,Polite,Assertive  و مشخص می‌کنند که المان مورد نظر آپدیت پذیر هست یا خیر و اگر آری، نحوه‌ی به روز شوندگی آن به چه نحوی است.

ساخت ارتباطات میان المان‌ها با خصوصیت‌های ارتباطی

مثال شماره یک
<div role="main" aria-labelledby="some-id">
 
    <h1 id="some-id">This Is A Heading</h1>
 
    Main content...
 
</div>
خصوصیت aria-labelledby به تعریف المان‌هایی با نام جاری می‌پردازد. این خصوصیت معرفی کننده‌ی برچسب هاست. به عنوان مثال بین المان‌ها ورودی و برچسب آنان ارتباط ایجاد می‌کند تا ابزارهای معلولین، مانند صفحه خوان‌ها، در خواندن به مشکل برنخورند.

مثال شماره دو
در این مثال هر گروهی از المان‌ها یک برچسب و بعضی المان‌ها یک برچسب اختصاصی دارند که توسط خصوصیت معرفی شده aria-labelldby کامل شده‌اند:
<div id="billing">Billing Address</div>

<div>
    <div id="name">Name</div>
    <input type="text" aria-labelledby="name billing"/>
</div>
<div>
    <div id="address">Address</div>
    <input type="text" aria-labelledby="address billing"/>
</div>

مثال شماره سه

در این مثال گروهی از radio button‌ها با برچسبشان ارتباط برقرار می‌کنند.
<div id="radio_label">My radio label</div>
<ul role="radiogroup" aria-labelledby="radio_label">
    <li role="radio">Item #1</li>
    <li role="radio">Item #2</li>
    <li role="radio">Item #3</li>
</ul>

خصوصیت‌ها و وضعیت‌های aria را با چرخه‌ی فعالیت‌های صفحه به روز کنید

در صورتیکه چرخه‌ی فعالیت صفحه‌ی شما تغییر می‌کند و تگ‌ها نیاز به مقادیر جدیدی از aria دارند، حتما این مقادیر را هم به نسبت تغییراتی که در صفحه زخ می‌دهد، تغییر دهید تا وضعیت بحرانی برای کاربر به خصوص در حین کار با فرم‌ها و ... پیش نیاید.


هر aria را دوباره استفاده نکنید

امروزه به خصوص با آمدن html5 و ویژگیهایی چون تگ‌های مفهومی، کار بسیار راحت‌تر شده‌است و مرورگر به طور خودکار می‌تواند aria را بر روی بعضی از المان‌ها پیاده کند. به عنوان نمونه:
<form></form>
<form role="form"></form>
همچنین به عنوان مثال با استفاده از خصوصیات HTML مثل hidden کردن یک شیء نیازی به استفاده از وضعیت aria-hidden نمی‌باشد. مرورگر به طور پیش فرض آن را لحاظ می‌کند.

امروزه شاهد پیشرفت فناوری در همه‌ی عرصه‌ها هستیم و همیشه این پیشرفت‌ها ما را ذوق زده کرده‌اند، ولی یکی از بی نظیرترین استفاده‌های فناوری روز، استفاده در صنایع سلامتی است که نه تنها ما را ذوق زده می‌کند، بلکه از لحاظ احساسی هم ما را به وجد می‌آورند و جزء زیباترین نتایج فناوری می‌باشند. بسیاری از شرکت‌ها چون گوگل در این راستا فعالیت‌های زیادی کرده‌اند تا بتوانند سلامت جامعه را کنترل کنند، از ساخت لنز چشمی برای کنترل دیابت گرفته تا ساخت قاشق عذای خوری برای بیماران پارکینسون، ولی استفاده‌های ساده از مسائلی مانند بالا به افراد معلول این مژده را می‌دهد که ما آن‌ها را فراموش نکرده‌ایم.
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت چهارم - User Claims
از نگارش‌های پیشین ASP.NET، هنوز هم اطلاعات شیء User مانند User.Identity.Name در ASP.NET Core نیز در دسترس هستند. به این ترتیب زمانیکه کاربری به سیستم وارد شد، دیگر نیازی نیست تا جهت یافتن Name او، از بانک اطلاعاتی کوئری گرفت. خاصیت Name یاد شده به صورت خودکار از کوکی رمزنگاری شده‌ی او دریافت شده و در اختیار برنامه قرار می‌گیرد. این Name در ASP.NET Core Identity، اصطلاحا یک User Claim پیش‌فرض نام دارد و به صورت خودکار ایجاد و مقدار دهی می‌شود. اکنون این سؤال مطرح می‌شود که آیا می‌توان خاصیت دیگری را به شیء User.Identity اضافه کرد؟


جدول AppUserClaims


جدول AppUserClaims، جزو جداول اصلی ASP.NET Core Identity است و هدف آن ذخیره‌ی اطلاعات ویژه‌ی کاربران و بازیابی ساده‌تر آن‌ها از طریق کوکی‌های آن‌ها است (همانند User.Identity.Name). زمانیکه کاربری به سیستم وارد می‌شود، بر اساس UserId او، تمام رکوردهای User Claims متعلق به او از این جدول واکشی شده و به صورت خودکار به کوکی او اضافه می‌شوند.

در پروژه‌ی DNT Identity از این جدول استفاده نمی‌شود. چون اطلاعات User Claims مورد نیاز آن، هم اکنون در جدول AppUsers موجود هستند. به همین جهت افزودن این نوع User Claimها به جدول AppUserClaims، به ازای هر کاربر، کاری بیهوده است. سناریویی که استفاده‌ی از این جدول را با مفهوم می‌کند، ذخیره سازی تنظیمات ویژه‌ی هرکاربر است (خارج از فیلدهای جدول کاربران). برای مثال اگر سایتی را چندزبانه طراحی کردید، می‌توانید یک User Claim سفارشی جدید را برای این منظور ایجاد و زبان انتخابی کاربر را به عنوان یک رکورد جدید مخصوص آن در این جدول ویژه ثبت کنید. مزیت آن این است که واکشی و افزوده شدن اطلاعات آن به کوکی شخص، به صورت خودکار توسط فریم ورک صورت گرفته و در حین مرور صفحات توسط کاربر، دیگر نیازی نیست تا اطلاعات زبان انتخابی او را از بانک اطلاعاتی واکشی کرد.
بنابراین برای ذخیره سازی تنظیمات با کارآیی بالای ویژه‌ی هرکاربر، جدول جدیدی را ایجاد نکنید. جدول User Claim برای همین منظور درنظر گرفته شده‌است و پردازش اطلاعات آن توسط فریم ورک صورت می‌گیرد.


ASP.NET Core Identity چگونه اطلاعات جدول AppUserClaims را پردازش می‌کند؟

ASP.NET Identity Core در حین لاگین کاربر به سیستم، از سرویس SignInManager خودش استفاده می‌کند که با نحوه‌ی سفارشی سازی آن پیشتر در قسمت دوم این سری آشنا شدیم. سرویس SignInManager پس از لاگین شخص، از یک سرویس توکار دیگر این فریم ورک به نام UserClaimsPrincipalFactory جهت واکشی اطلاعات User Claims و همچنین Role Claims و افزودن آن‌ها به کوکی رمزنگاری شده‌ی شخص، استفاده می‌کند.
بنابراین اگر قصد افزودن User Claim سفارشی دیگری را داشته باشیم، می‌توان همین سرویس توکار UserClaimsPrincipalFactory را سفارشی سازی کرد (بجای اینکه الزاما رکوردی را به جدول AppUserClaims اضافه کنیم).

اطلاعات جالبی را هم می‌توان از پیاده سازی متد CreateAsync آن استخراج کرد:
  public virtual async Task<ClaimsPrincipal> CreateAsync(TUser user)
1) userId شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserIdClaimType به کوکی او اضافه می‌شود.
2) userName شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserNameClaimType به کوکی او اضافه می‌شود.
3) security stamp او (آخرین بار تغییر اطلاعات اکانت کاربر) نیز یک Claim پیش‌فرض است.
4) اگر نقش‌هایی به کاربر انتساب داده شده باشند، تمام این نقش‌ها واکشی شده و به عنوان یک Claim جدید به کوکی او اضافه می‌شوند.
5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه می‌شوند. در ASP.NET Identity Core نقش‌ها نیز می‌توانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).

بنابراین حداقل مدیریت Claims این 5 مورد خودکار است و اگر برای مثال نیاز به Id کاربر لاگین شده را داشتید، نیازی نیست تا آن‌را از بانک اطلاعاتی واکشی کنید. چون این اطلاعات هم اکنون در کوکی او موجود هستند.


سفارشی سازی کلاس UserClaimsPrincipalFactory جهت افزودن User Claims سفارشی

تا اینجا دریافتیم که کلاس UserClaimsPrincipalFactory کار مدیریت Claims پیش‌فرض این فریم ورک را برعهده دارد. در ادامه از این کلاس ارث بری کرده و متد CreateAsync آن‌را جهت افزودن Claims سفارشی خود بازنویسی می‌کنیم. این پیاده سازی سفارشی را در کلاس ApplicationClaimsPrincipalFactory می‌توانید مشاهده کنید:
        public override async Task<ClaimsPrincipal> CreateAsync(User user)
        {
            var principal = await base.CreateAsync(user).ConfigureAwait(false); 
            addCustomClaims(user, principal);
            return principal;
        }

        private static void addCustomClaims(User user, IPrincipal principal)
        {
            ((ClaimsIdentity) principal.Identity).AddClaims(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer),
                new Claim(ClaimTypes.GivenName, user.FirstName ?? string.Empty),
                new Claim(ClaimTypes.Surname, user.LastName ?? string.Empty),
                new Claim(PhotoFileName, user.PhotoFileName ?? string.Empty, ClaimValueTypes.String),
            });
        }
در حین بازنویسی متد CreateAsync، ابتدا base.CreateAsync را فراخوانی کرده‌ایم، تا اخلالی در عملکرد این فریم ورک رخ ندهد و هنوز هم همان مواردی که در قسمت قبل توضیح داده شد، به صورت پیش فرض به کوکی شخص اضافه شوند. سپس در متد addCustomClaims، تعدادی Claim سفارشی خاص خودمان را اضافه کرده‌ایم.
برای مثال نام، نام خانوادگی و نام تصویر شخص به صورت Claimهایی جدید به کوکی او اضافه می‌شوند. در این حالت دیگر نیازی نیست تا به ازای هر کاربر، جدول AppUserClaims را ویرایش کرد و اطلاعات جدیدی را افزود و یا تغییر داد. همینقدر که کاربر به سیستم لاگین کند، شیء User او به متد Create کلاس UserClaimsPrincipalFactory ارسال می‌شود. به این ترتیب می‌توان به تمام خواص این کاربر دسترسی یافت و در صورت نیاز آن‌ها را به صورت Claimهایی به کوکی او افزود.

پس از تدارک کلاس ApplicationClaimsPrincipalFactory‌، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IUserClaimsPrincipalFactory<User>, ApplicationClaimsPrincipalFactory>();
services.AddScoped<UserClaimsPrincipalFactory<User, Role>, ApplicationClaimsPrincipalFactory>();
یکبار ApplicationClaimsPrincipalFactory را به عنوان پیاده سازی کننده‌ی IUserClaimsPrincipalFactory معرفی کرده‌ایم. همچنین یکبار هم سرویس توکار UserClaimsPrincipalFactory را به سرویس سفارشی خودمان هدایت کرده‌ایم. به این ترتیب مطمئن خواهیم شد که همواره از ApplicationClaimsPrincipalFactory ما استفاده خواهد شد (حتی اگر UserClaimsPrincipalFactory اصلی از سیستم تزریق وابستگی‌ها درخواست شود).
 

چگونه به اطلاعات User Claims در سرویس‌های برنامه دسترسی پیدا کنیم؟

برای دسترسی به اطلاعات User Claims نیاز به دسترسی به HttpContext جاری را داریم. در این مورد و تزریق سرویس IHttpContextAccessor جهت تامین آن، در مطلب «بررسی روش دسترسی به HttpContext در ASP.NET Core» پیشتر بحث شده‌است.
به علاوه در کلاس IdentityServicesRegistry، تزریق وابستگی‌های سفارشی‌تری نیز صورت گرفته‌است:
services.AddScoped<IPrincipal>(provider =>
    provider.GetService<IHttpContextAccessor>()?.HttpContext?.User ?? ClaimsPrincipal.Current);
در اینجا اگر نیاز به اطلاعات Claims شیء User را داشتید، می‌توانید اینترفیس IPrincipal را هم بجای IHttpContextAccessor، به سازنده‌ی کلاس مدنظر خود تزریق کنید.


چگونه اطلاعات User Claims سفارشی را دریافت کنیم؟

برای کار ساده‌تر با Claims یک کلاس کمکی به نام IdentityExtensions به پروژه اضافه شده‌است و متدهایی مانند دو متد ذیل را می‌توانید در آن مشاهده کنید:
        public static string FindFirstValue(this ClaimsIdentity identity, string claimType)
        {
            return identity?.FindFirst(claimType)?.Value;
        }

        public static string GetUserClaimValue(this IIdentity identity, string claimType)
        {
            var identity1 = identity as ClaimsIdentity;
            return identity1?.FindFirstValue(claimType);
        }
در اینجا نحوه‌ی استخراج اطلاعات را از شیء User و یا User.Identity مشاهده می‌کنید. تنها کافی است claimType ایی را درخواست کرده و سپس مقدار آن‌را از کوکی شخص به نحو فوق واکشی کنیم.
برای نمونه متد GetUserDisplayName این کلاس کمکی، از همان Claims سفارشی که در کلاس ApplicationClaimsPrincipalFactory تعریف کردیم، اطلاعات خود را استخراج می‌کند و اگر در View ایی خواستید این اطلاعات را نمایش دهید، می‌توانید بنویسید:
 @User.Identity.GetUserDisplayName()


چگونه پس از ویرایش اطلاعات کاربر، اطلاعات کوکی او را نیز به روز کنیم؟

در پروژه قسمتی وجود دارد جهت ویرایش اطلاعات کاربران (UserProfileController). اگر کاربری برای مثال نام و نام خانوادگی خود را ویرایش کرد، می‌خواهیم بلافاصله متد GetUserDisplayName اطلاعات صحیح و به روزی را از کوکی او دریافت کند. برای اینکار یا می‌توان او را وادار به لاگین مجدد کرد (تا پروسه‌ی رسیدن به متد CreateAsync کلاس ApplicationClaimsPrincipalFactory طی شود) و یا روش بهتری نیز وجود دارد:
 // reflect the changes, in the current user's Identity cookie
await _signInManager.RefreshSignInAsync(user).ConfigureAwait(false);
در اینجا تنها کافی است متد RefreshSignInAsync را مجددا بر اساس اطلاعات ویرایش شده‌ی کاربر، فراخوانی کنیم تا کوکی او را بلافاصله به روز کند و این روش نیازی به اجبار به لاگین مجدد کاربر را ندارد.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
- هدف از بحث جاری صرفا «اعمال تزریق وابستگی‌ها» هست.
- سری مباحث ASP.NET Identity 2,x را از اینجا شروع به مطالعه کنید. برای نمونه: «افزودن تصدیق ایمیل به ASP.NET Identity در MVC 5» و «Identity 2.0 : تایید حساب‌های کاربری و احراز هویت دو مرحله ای » و «پیاده سازی یک سیستم دسترسی Role Based در Web API و AngularJs - بخش اول»


جهت اطلاع عموم
ASP.NET Identity 2.x یک پروژه‌ی خاتمه یافته از دیدگاه مایکروسافت محسوب شده و دیگر پشتیبانی خاصی ندارد . نگارش بعدی آن ASP.NET Core Identity نام دارد.
مطالب
ساخت یک اپلیکیشن ساده ToDo با ASP.NET Identity
یک سناریوی فرضی را در نظر بگیرید. اگر بخواهیم IdentityDbContext و دیگر DbContext‌های اپلیکیشن را ادغام کنیم چه باید کرد؟ مثلا یک سیستم وبلاگ که برخی کاربران می‌توانند پست جدید ثبت کنند، برخی تنها می‌توانند کامنت بگذارند و تمامی کاربران هم اختیارات مشخص دیگری دارند. در چنین سیستمی شناسه کاربران (User ID) در بسیاری از مدل‌ها (موجودیت‌ها و مدل‌های اپلیکیشن) وجود خواهد داشت تا مشخص شود هر رکورد به کدام کاربر متعلق است. در این مقاله چنین سناریو هایی را بررسی می‌کنیم و best practice‌های مربوطه را مرور می‌کنیم.
در این پست یک اپلیکیشن ساده ToDo خواهیم ساخت که امکان تخصیص to-do‌ها به کاربران را نیز فراهم می‌کند. در این مثال خواهیم دید که چگونه می‌توان مدل‌های مختص به سیستم عضویت (IdentityDbContext) را با مدل‌های دیگر اپلیکیشن مخلوط و استفاده کنیم.


تعریف نیازمندی‌های اپلیکیشن

  • تنها کاربران احراز هویت شده قادر خواهند بود تا لیست ToDo‌های خود را ببینند، آیتم‌های جدید ثبت کنند یا داده‌های قبلی را ویرایش و حذف کنند.
  • کاربران نباید آیتم‌های ایجاد شده توسط دیگر کاربران را ببینند.
  • تنها کاربرانی که به نقش Admin تعلق دارند باید بتوانند تمام ToDo‌های ایجاد شده را ببینند.
پس بگذارید ببینیم چگونه می‌شود اپلیکیشنی با ASP.NET Identity ساخت که پاسخگوی این نیازمندی‌ها باشد.
ابتدا یک پروژه ASP.NET MVC جدید با مدل احراز هویت Individual User Accounts بسازید. در این اپلیکیشن کاربران قادر خواهند بود تا بصورت محلی در وب سایت ثبت نام کنند و یا با تامین کنندگان دیگری مانند گوگل و فیسبوک وارد سایت شوند. برای ساده نگاه داشتن این پست ما از حساب‌های کاربری محلی استفاده می‌کنیم.
در مرحله بعد ASP.NET Identity را راه اندازی کنید تا بتوانیم نقش مدیر و یک کاربر جدید بسازیم. می‌توانید با اجرای اپلیکیشن راه اندازی اولیه را انجام دهید. از آنجا که سیستم ASP.NET Identity توسط Entity Framework مدیریت می‌شود می‌توانید از تنظیمات پیکربندی Code First برای راه اندازی دیتابیس خود استفاده کنید.
در قدم بعدی راه انداز دیتابیس را در Global.asax تعریف کنید.
Database.SetInitializer<MyDbContext>(new MyDbInitializer());


ایجاد نقش مدیر و کاربر جدیدی که به این نقش تعلق دارد

اگر به قطعه کد زیر دقت کنید، می‌بینید که در خط شماره 5 متغیری از نوع UserManager ساخته ایم که امکان اجرای عملیات گوناگونی روی کاربران را فراهم می‌کند. مانند ایجاد، ویرایش، حذف و اعتبارسنجی کاربران. این کلاس که متعلق به سیستم ASP.NET Identity است همتای SQLMembershipProvider در ASP.NET 2.0 است.
در خط 6 یک RoleManager می‌سازیم که امکان کار با نقش‌ها را فراهم می‌کند. این کلاس همتای SQLRoleMembershipProvider در ASP.NET 2.0 است.
در این مثال نام کلاس کاربران (موجودیت کاربر در IdentityDbContext) برابر با "MyUser" است، اما نام پیش فرض در قالب‌های پروژه VS 2013 برابر با "ApplicationUser" می‌باشد.
public class MyDbInitializer : DropCreateDatabaseAlways<MyDbContext>
     {
          protected override void Seed(MyDbContext context)
          {
              var UserManager = new UserManager<MyUser>(new 

                                                UserStore<MyUser>(context)); 

              var RoleManager = new RoleManager<IdentityRole>(new 
                                          RoleStore<IdentityRole>(context));
   
              string name = "Admin";
              string password = "123456";
 
   
              //Create Role Admin if it does not exist
              if (!RoleManager.RoleExists(name))
              {
                  var roleresult = RoleManager.Create(new IdentityRole(name));
              }
   
              //Create User=Admin with password=123456
              var user = new MyUser();
              user.UserName = name;
              var adminresult = UserManager.Create(user, password);
   
              //Add User Admin to Role Admin
              if (adminresult.Succeeded)
              {
                  var result = UserManager.AddToRole(user.Id, name);
              }
              base.Seed(context);
          }
      }


حال فایلی با نام Models/AppModels.cs بسازید و مدل EF Code First اپلیکیشن را تعریف کنید. از آنجا که از EF استفاده می‌کنیم، روابط کلید‌ها بین کاربران و ToDo‌ها بصورت خودکار برقرار می‌شود.
public class MyUser : IdentityUser
      {
          public string HomeTown { get; set; }
          public virtual ICollection<ToDo>
                               ToDoes { get; set; }
      }
   
      public class ToDo
      {
          public int Id { get; set; }
          public string Description { get; set; }
          public bool IsDone { get; set; }
          public virtual MyUser User { get; set; }
      }

در قدم بعدی با استفاده از مکانیزم Scaffolding کنترلر جدیدی بهمراه تمام View‌ها و متدهای لازم برای عملیات CRUD بسازید. برای اطلاعات بیشتر درباره  نحوه استفاده از مکانیزم Scaffolding به این لینک مراجعه کنید.
لطفا دقت کنید که از DbContext فعلی استفاده کنید. این کار مدیریت داده‌های Identity و اپلیکیشن شما را یکپارچه‌تر می‌کند. DbContext شما باید چیزی شبیه به کد زیر باشد.
     public class MyDbContext : IdentityDbContext<MyUser>
      {
          public MyDbContext()
              : base("DefaultConnection")
          {
           }
    
           protected override void OnModelCreating(DbModelBuilder modelBuilder)
           {
          public System.Data.Entity.DbSet<AspnetIdentitySample.Models.ToDo> 
                     ToDoes { get; set; }
      }

تنها کاربران احراز هویت شده باید قادر به اجرای عملیات CRUD باشند

برای این مورد از خاصیت Authorize استفاده خواهیم کرد که در MVC 4 هم وجود داشت. برای اطلاعات بیشتر لطفا به این لینک مراجعه کنید.
[Authorize]
public class ToDoController : Controller

کنترلر ایجاد شده را ویرایش کنید تا کاربران را به ToDo‌ها اختصاص دهد. در این مثال تنها اکشن متدهای Create و List را بررسی خواهیم کرد. با دنبال کردن همین روش می‌توانید متدهای Edit و Delete را هم بسادگی تکمیل کنید.
یک متد constructor جدید بنویسید که آبجکتی از نوع UserManager می‌پذیرد. با استفاده از این کلاس می‌توانید کاربران را در ASP.NET Identity مدیریت کنید.
 private MyDbContext db;
          private UserManager<MyUser> manager;
          public ToDoController()
          {
              db = new MyDbContext();
              manager = new UserManager<MyUser>(new UserStore<MyUser>(db));
          }

اکشن متد Create را بروز رسانی کنید

هنگامی که یک ToDo جدید ایجاد می‌کنید، کاربر جاری را در ASP.NET Identity پیدا می‌کنیم و او را به ToDo‌ها اختصاص می‌دهیم.
    public async Task<ActionResult> Create
          ([Bind(Include="Id,Description,IsDone")] ToDo todo)
          {
              var currentUser = await manager.FindByIdAsync
                                                 (User.Identity.GetUserId()); 
              if (ModelState.IsValid)
              {
                  todo.User = currentUser;
                  db.ToDoes.Add(todo);
                  await db.SaveChangesAsync();
                  return RedirectToAction("Index");
              }
   
              return View(todo);
          }

اکشن متد List را بروز رسانی کنید

در این متد تنها ToDo‌های کاربر جاری را باید بگیریم.
          public ActionResult Index()
          {
              var currentUser = manager.FindById(User.Identity.GetUserId());

               return View(db.ToDoes.ToList().Where(
                                   todo => todo.User.Id == currentUser.Id));
          }

تنها مدیران سایت باید بتوانند تمام ToDo‌ها را ببینند

بدین منظور ما یک اکشن متد جدید به کنترل مربوطه اضافه می‌کنیم که تمام ToDo‌ها را لیست می‌کند. اما دسترسی به این متد را تنها برای کاربرانی که در نقش مدیر وجود دارند میسر می‌کنیم.
     [Authorize(Roles="Admin")]
          public async Task<ActionResult> All()
          {
              return View(await db.ToDoes.ToListAsync());
          }

نمایش جزئیات کاربران از جدول ToDo ها

از آنجا که ما کاربران را به ToDo هایشان مرتبط می‌کنیم، دسترسی به داده‌های کاربر ساده است. مثلا در متدی که مدیر سایت تمام آیتم‌ها را لیست می‌کند می‌توانیم به اطلاعات پروفایل تک تک کاربران دسترسی داشته باشیم و آنها را در نمای خود بگنجانیم. در این مثال تنها یک فیلد بنام HomeTown اضافه شده است، که آن را در کنار اطلاعات ToDo نمایش می‌دهیم.
 @model IEnumerable<AspnetIdentitySample.Models.ToDo>
   
  @{
    ViewBag.Title = "Index";
  }
   
  <h2>List of ToDoes for all Users</h2>
  <p>
      Notice that we can see the User info (UserName) and profile info such as HomeTown for the user as well.
      This was possible because we associated the User object with a ToDo object and hence
      we can get this rich behavior.
  12:  </p>
   
  <table class="table">
      <tr>
          <th>
              @Html.DisplayNameFor(model => model.Description)
          </th>
          <th>
              @Html.DisplayNameFor(model => model.IsDone)
          </th>
          <th>@Html.DisplayNameFor(model => model.User.UserName)</th>
          <th>@Html.DisplayNameFor(model => model.User.HomeTown)</th>
      </tr>
  25:   
  26:      @foreach (var item in Model)
  27:      {
  28:          <tr>
  29:              <td>
  30:                  @Html.DisplayFor(modelItem => item.Description)
  31:              </td>
  32:              <td>
                  @Html.DisplayFor(modelItem => item.IsDone)
              </td>
              <td>
                  @Html.DisplayFor(modelItem => item.User.UserName)
              </td>
              <td>
                  @Html.DisplayFor(modelItem => item.User.HomeTown)
              </td>
          </tr>
      }
   
  </table>

صفحه Layout را بروز رسانی کنید تا به ToDo‌ها لینک شود

<li>@Html.ActionLink("ToDo", "Index", "ToDo")</li>
 <li>@Html.ActionLink("ToDo for User In Role Admin", "All", "ToDo")</li>

حال اپلیکیشن را اجرا کنید. همانطور که مشاهده می‌کنید دو لینک جدید به منوی سایت اضافه شده اند.


ساخت یک ToDo بعنوان کاربر عادی

روی لینک ToDo کلیک کنید، باید به صفحه ورود هدایت شوید چرا که دسترسی تنها برای کاربران احراز هویت شده تعریف وجود دارد. می‌توانید یک حساب کاربری محلی ساخته، با آن وارد سایت شوید و یک ToDo بسازید.

پس از ساختن یک ToDo می‌توانید لیست رکوردهای خود را مشاهده کنید. دقت داشته باشید که رکوردهایی که کاربران دیگر ثبت کرده اند برای شما نمایش داده نخواهند شد.


مشاهده تمام ToDo‌ها بعنوان مدیر سایت

روی لینک ToDoes for User in Role Admin کلیک کنید. در این مرحله باید مجددا به صفحه ورود هدایت شوید چرا که شما در نقش مدیر نیستید و دسترسی کافی برای مشاهده صفحه مورد نظر را ندارید. از سایت خارج شوید و توسط حساب کاربری مدیری که هنگام راه اندازی اولیه دیتابیس ساخته اید وارد سایت شوید.
User = Admin
Password = 123456
پس از ورود به سایت بعنوان یک مدیر، می‌توانید ToDo‌های ثبت شده توسط تمام کاربران را مشاهده کنید.

مطالب
ایجاد سرویس Account Manager با تکنولوژی های Identity 2.1 و Web API 2.2
ASP.NET Identity 2.1 جدیدترین فریم ورک عضویت و مدیریت کاربر است که چندی پیش توسط شرکت مایکروسافت منتشر شد. این سیستم عضویت می‌تواند به تمامی فریم‌ورک‌های دات نتی مانند Web API، MVC و ... متصل گردد.
در این دوره چند قسمتی به همراه یک پروژه‌ی نمونه، نحوه‌ی ارتباط Identity و Web API را نمایش خواهیم داد. در قسمت front-end این پروژه‌ی SPA، ما از AngularJs استفاده خواهیم نمود. قسمت front-end که توسط AngularJs توسعه داده می‌شود از bearet token based authentication استفاده می‌کند. این متد از JSON Web Token برای فرایند Authorization بهره می‌گیرد که به اختصار آن را JWT نیز می‌نامند. این روش سیستم اعتبار سنجی role based بوده و  تمامی آنچه را که در یک سیستم membership داریم، پوشش می‌دهد. توجه داشته باشید که برای فهم بهتر تمامی مراحل، ما پروژه را بدون هیچ قالب از پیش تعریف شده‌ای در VS2013 آغاز می‌کنیم.
به دلیل اینکه پروژه در طی چند قسمت انجام می‌پذیرد آن را به بخش‌های زیر تقسیم می‌کنیم.
  1.  تنظیمات اولیه ASP.NET Identity 2.1 با Web API
  2.  ایجاد Account Confirmation به وسیله Identity به همراه تنظیمات policy برای user name و password
  3.  توسعه OAuth Json Web Token Authentication به کمک Web API و Identity
  4.  ایجاد یک سیستم Role Based و تایید صلاحیت‌های مربوط به آن
  5.  توسعه Web API Claims Authorization در Identity 2.1
  6.  توسعه بخش front-end با AngularJs

تنظیمات اولیه ASP.NET Identity 2.1 با Web API 

1. تنظیمات ASP.Net Identity 2.1

1-1. ساخت یک پروژه Web API

در ابتدا ما یک empty solution را با نام "AspNetIdentity" همانند شکل مقابل می‌سازیم.

یک ASP.NET Web Application با نام "AspNetIdentity.WebApi" را به این solution اضافه می‌نماییم. در بخش select template ما empty template را انتخاب می‌کنیم. همچنین در قسمت add folders and core references for: نیز هیچیک از گزینه‌ها را انتخاب نمی‌کنیم.

1-2. نصب package‌های مورد نیاز از Nuget

در زیر package‌های مورد نیاز برای ASP.NET Web API و Owin را مشاهده میکنید. همچنین package‌های مربوط به ASP.Net Identity 2.1 نیز در زیر قرار داده شده‌اند.

Install-Package Microsoft.AspNet.Identity.Owin -Version 2.1.0
Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.1.0
Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.0.0
Install-Package Microsoft.AspNet.WebApi.Owin -Version 5.2.2
Install-Package Microsoft.Owin.Security.OAuth -Version 3.0.0
Install-Package Microsoft.Owin.Cors -Version 3.0.0

1-3. اضافه کردن user class و database context

حال که تمامی پکیج‌های مورد نیاز را به پروژه خود اضافه نمودیم، قصد داریم تا اولین کلاس EF را با نام "ApplicationUser" به پروژه اضافه کنیم. این کلاس، کاربری را که قصد ثبت نام در membership system، دارد را نمایش می‌دهد. برای این کار ما یک کلاس جدید را به نام "ApplicationUser" می‌سازیم و کلاس "Microsoft.AspNet.Identity.EntityFramework.IdentityUser" را در آن به ارث می‌بریم.

برای این کار ما یک پوشه‌ی جدید را در برنامه با نام "Infrastructure" می‌سازیم و درون آن کلاس ذکر شده را اضافه می‌کنیم:

 public class ApplicationUser : IdentityUser
    {
        [Required]
        [MaxLength(100)]
        public string FirstName { get; set; }
 
        [Required]
        [MaxLength(100)]
        public string LastName { get; set; }
 
        [Required]
        public byte Level { get; set; }
 
        [Required]
        public DateTime JoinDate { get; set; }
 
    }

پس از افزودن کلاس User، نوبت به اضافه نمودن Db Context است. این کلاس وظیفه‌ی ارتباط با پایگاه داده را بر عهده دارد. ما یک کلاس جدید را با نام ApplicationDbContext، به پوشه‌ی Infrastructure اضافه می‌نماییم. کد مربوط به این کلاس به صورت زیر است:

 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
            Configuration.ProxyCreationEnabled = false;
            Configuration.LazyLoadingEnabled = false;
        }
 
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
 
    }

همانطور که ملاحظه می‌کنید این کلاس از IdentityDbContext ارث بری نموده است. این کلاس یک نسخه‌ی جدیدتر از DbContext است که تمامی نگاشت‌های Entity Framework Code First را انجام می‌دهد. در ادامه ما یک Connection String را با نام DefaultConnection در فایل web.config اضافه می‌نماییم.

همچنین متد static ایی را با نام Create که در تکه کد فوق از سوی Owin Startup class فراخوانی می‌گردد که در ادامه به شرح آن نیز خواهیم پرداخت.

ConnectionString قرار داده شده در فایل web.config در قسمت زیر قرار داده شده است:

<connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=.\sqlexpress;Initial Catalog=AspNetIdentity;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
  </connectionStrings>

قدم 4: ساخت پایگاه داده و فعال سازی DB Migration

حال ما باید EF CodeFirst Migration را برای آپدیت کردن دیتابیس، بجای دوباره ساختن آن به ازای هر تغییری، فعال نماییم. برای این کار بایستی در قسمت NuGet Package Manager Console عبارت زیر را وارد نماییم:

enable-migrations
add-migration InitialCreate

اگر تا به اینجای کار تمامی مراحل به درستی صورت گرفته باشند، پوشه‌ی Migration با موفقیت ساخته می‌شود. همچنین پایگاه داده متشکل از جداول مورد نیاز برای سیستم Identity نیز ایجاد می‌شود. برای اطلاعات بیشتر می‌توانید مقالات مرتبط با Code First را مطالعه نمایید. ساختار جداول ما باید به صورت زیر باشد:

در بخش بعدی، کلاس‌های مربوط به UserManager را ایجاد خواهیم کرد و پس از آن فایل Startup Owin را برای مدیریت کاربران، تشریح می‌کنیم.

مطالب
ASP.NET MVC #18

اعتبار سنجی کاربران در ASP.NET MVC

دو مکانیزم اعتبارسنجی کاربران به صورت توکار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication.
در حالت Forms authentication، برنامه موظف به نمایش فرم لاگین به کاربر‌ها و سپس بررسی اطلاعات وارده توسط آن‌ها است. برخلاف آن، Windows authentication حالت یکپارچه با اعتبار سنجی ویندوز است. برای مثال زمانیکه کاربری به یک دومین ویندوزی وارد می‌شود، از همان اطلاعات ورود او به شبکه داخلی، به صورت خودکار و یکپارچه جهت استفاده از برنامه کمک گرفته خواهد شد و بیشترین کاربرد آن در برنامه‌های نوشته شده برای اینترانت‌های داخلی شرکت‌ها است. به این ترتیب کاربران یک بار به دومین وارد شده و سپس برای استفاده از برنامه‌های مختلف ASP.NET، نیازی به ارائه نام کاربری و کلمه عبور نخواهند داشت. Forms authentication بیشتر برای برنامه‌هایی که از طریق اینترنت به صورت عمومی و از طریق انواع و اقسام سیستم عامل‌ها قابل دسترسی هستند، توصیه می‌شود (و البته منعی هم برای استفاده در حالت اینترانت ندارد).
ضمنا باید به معنای این دو کلمه هم دقت داشت: هدف از Authentication این است که مشخص گردد هم اکنون چه کاربری به سایت وارد شده است. Authorization، سطح دسترسی کاربر وارد شده به سیستم و اعمالی را که مجاز است انجام دهد، مشخص می‌کند.


فیلتر Authorize در ASP.NET MVC

یکی دیگر از فیلترهای امنیتی ASP.NET MVC به نام Authorize، کار محدود ساختن دسترسی به متدهای کنترلرها را انجام می‌دهد. زمانیکه اکشن متدی به این فیلتر یا ویژگی مزین می‌شود، به این معنا است که کاربران اعتبارسنجی نشده، امکان دسترسی به آن‌را نخواهند داشت. فیلتر Authorize همواره قبل از تمامی فیلترهای تعریف شده دیگر اجرا می‌شود.
فیلتر Authorize با پیاده سازی اینترفیس System.Web.Mvc.IAuthorizationFilter توسط کلاس System.Web.Mvc.AuthorizeAttribute در دسترس می‌باشد. این کلاس علاوه بر پیاده سازی اینترفیس یاد شده، دارای دو خاصیت مهم زیر نیز می‌باشد:

public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames

زمانیکه فیلتر Authorize به تنهایی بکارگرفته می‌شود، هر کاربر اعتبار سنجی شده‌ای در سیستم قادر خواهد بود به اکشن متد مورد نظر دسترسی پیدا کند. اما اگر همانند مثال زیر، از خواص Roles و یا Users نیز استفاده گردد، تنها کاربران اعتبار سنجی شده مشخصی قادر به دسترسی به یک کنترلر یا متدی در آن خواهند شد:

[Authorize(Roles="Admins")]
public class AdminController : Controller
{
  [Authorize(Users="Vahid")]
  public ActionResult DoSomethingSecure()
   {
  }
}

در این مثال، تنها کاربرانی با نقش Admins قادر به دسترسی به کنترلر جاری Admin خواهند بود. همچنین در بین این کاربران ویژه، تنها کاربری به نام Vahid قادر است متد DoSomethingSecure را فراخوانی و اجرا کند.

اکنون سؤال اینجا است که فیلتر Authorize چگونه از دو مکانیزم اعتبار سنجی یاد شده استفاده می‌کند؟ برای پاسخ به این سؤال، فایل web.config برنامه را باز نموده و به قسمت authentication آن دقت کنید:

<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

به صورت پیش فرض، برنامه‌های ایجاد شده توسط VS.NET جهت استفاده از حالت Forms یا همان Forms authentication تنظیم شده‌اند. در اینجا کلیه کاربران اعتبار سنجی نشده، به کنترلری به نام Account و متد LogOn در آن هدایت می‌شوند.
برای تغییر آن به حالت اعتبار سنجی یکپارچه با ویندوز، فقط کافی است مقدار mode را به Windows تغییر داد و تنظیمات forms آن‌را نیز حذف کرد.


یک نکته: اعمال تنظیمات اعتبار سنجی اجباری به تمام صفحات سایت
تنظیم زیر نیز در فایل وب کانفیگ برنامه، همان کار افزودن ویژگی Authorize را انجام می‌دهد با این تفاوت که تمام صفحات سایت را به صورت خودکار تحت پوشش قرار خواهد داد (البته منهای loginUrl ایی که در تنظیمات فوق مشاهده نمودید):

<authorization>
<deny users="?" />
</authorization>

در این حالت دسترسی به تمام آدرس‌های سایت تحت تاثیر قرار می‌گیرند، منجمله دسترسی به تصاویر و فایل‌های CSS و غیره. برای اینکه این موارد را برای مثال در حین نمایش صفحه لاگین نیز نمایش دهیم، باید تنظیم زیر را پیش از تگ system.web به فایل وب کانفیگ برنامه اضافه کرد:

<!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>

در اینجا پوشه Content از سیستم اعتبارسنجی اجباری خارج می‌شود و تمام کاربران به آن دسترسی خواهند داشت.
به علاوه امکان امن ساختن تنها قسمتی از سایت نیز میسر است؛ برای مثال:

<location path="secure">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

در اینجا مسیری به نام secure، نیاز به اعتبارسنجی اجباری دارد. به علاوه تنها کاربرانی در نقش Administrators به آن دسترسی خواهند داشت.


نکته: به تنظیمات انجام شده در فایل Web.Config دقت داشته باشید
همانطور که می‌شود دسترسی به یک مسیر را توسط تگ location بازگذاشت، امکان بستن آن هم فراهم است (بجای allow از deny استفاده شود). همچنین در ASP.NET MVC به سادگی می‌توان تنظیمات مسیریابی را در فایل global.asax.cs تغییر داد. برای مثال اینبار مسیر دسترسی به صفحات امن سایت، Admin خواهد بود نه Secure. در این حالت چون از فیلتر Authorize استفاده نشده و همچنین فایل web.config نیز تغییر نکرده، این صفحات بدون محافظت رها خواهند شد.
بنابراین اگر از تگ location برای امن سازی قسمتی از سایت استفاده می‌کنید، حتما باید پس از تغییرات مسیریابی، فایل web.config را هم به روز کرد تا به مسیر جدید اشاره کند.
به همین جهت در ASP.NET MVC بهتر است که صریحا از فیلتر Authorize بر روی کنترلرها (جهت اعمال به تمام متدهای آن) یا بر روی متدهای خاصی از کنترلرها استفاده کرد.
امکان تعریف AuthorizeAttribute در فایل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسری نیز وجود دارد. اما در این حالت حتی صفحه لاگین سایت هم دیگر در دسترس نخواهد بود. برای رفع این مشکل در ASP.NET MVC 4 فیلتر دیگری به نام AllowAnonymousAttribute معرفی شده است تا بتوان قسمت‌هایی از سایت را مانند صفحه لاگین، از سیستم اعتبارسنجی اجباری خارج کرد تا حداقل کاربر بتواند نام کاربری و کلمه عبور خودش را وارد نماید:

[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
return View();
}

بنابراین در ASP.NET MVC 4.0، فیلتر AuthorizeAttribute را سراسری تعریف کنید. سپس در کنترلر لاگین برنامه از فیلتر AllowAnonymous استفاده نمائید.
البته نوشتن فیلتر سفارشی AllowAnonymousAttribute در ASP.NET MVC 3.0 نیز میسر است. برای مثال:

public class LogonAuthorize : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!(filterContext.Controller is AccountController))
base.OnAuthorization(filterContext);
}
}

در این فیلتر سفارشی، اگر کنترلر جاری از نوع AccountController باشد، از سیستم اعتبار سنجی اجباری خارج خواهد شد. مابقی کنترلرها همانند سابق پردازش می‌شوند. به این معنا که اکنون می‌توان LogonAuthorize را به صورت یک فیلتر سراسری در فایل global.asax.cs معرفی کرد تا به تمام کنترلرها، منهای کنترلر Account اعمال شود.



مثالی جهت بررسی حالت Windows Authentication

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس یک کنترلر جدید را به نام Home نیز به آن اضافه کنید. در ادامه متد Index آن‌را با ویژگی Authorize، مزین نمائید. همچنین بر روی نام این متد کلیک راست کرده و یک View خالی را برای آن ایجاد کنید:

using System.Web.Mvc;

namespace MvcApplication15.Controllers
{
public class HomeController : Controller
{
[Authorize]
public ActionResult Index()
{
return View();
}
}
}

محتوای View متناظر با متد Index را هم به شکل زیر تغییر دهید تا نام کاربر وارد شده به سیستم را نمایش دهد:

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>
Current user: @User.Identity.Name

به علاوه در فایل Web.config برنامه، حالت اعتبار سنجی را به ویندوز تغییر دهید:

<authentication mode="Windows" />

اکنون اگر برنامه را اجرا کنید و وب سرور آزمایشی انتخابی هم IIS Express باشد، پیغام HTTP Error 401.0 - Unauthorized نمایش داده می‌شود. علت هم اینجا است که Windows Authentication به صورت پیش فرض در این وب سرور غیرفعال است. برای فعال سازی آن به مسیر My Documents\IISExpress\config مراجعه کرده و فایل applicationhost.config را باز نمائید. تگ windowsAuthentication را یافته و ویژگی enabled آن‌را که false است به true تنظیم نمائید. اکنون اگر برنامه را مجددا اجرا کنیم، در محل نمایش User.Identity.Name، نام کاربر وارد شده به سیستم نمایش داده خواهد شد.
همانطور که مشاهده می‌کنید در اینجا همه چیز یکپارچه است و حتی نیازی نیست صفحه لاگین خاصی را به کاربر نمایش داد. همینقدر که کاربر توانسته به سیستم ویندوزی وارد شود، بر این اساس هم می‌تواند از برنامه‌های وب موجود در شبکه استفاده کند.



بررسی حالت Forms Authentication

برای کار با Forms Authentication نیاز به محلی برای ذخیره سازی اطلاعات کاربران است. اکثر مقالات را که مطالعه کنید شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع می‌دهند. این روش در ASP.NET MVC هم کار می‌کند؛ اما الزامی به استفاده از آن نیست.

برای بررسی حالت اعتبار سنجی مبتنی بر فرم‌ها، یک برنامه خالی ASP.NET MVC جدید را آغاز کنید. یک کنترلر Home ساده را نیز به آن اضافه نمائید.
سپس نیاز است نکته «تنظیمات اعتبار سنجی اجباری تمام صفحات سایت» را به فایل وب کانفیگ برنامه اعمال نمائید تا نیازی نباشد فیلتر Authorize را در همه جا معرفی کرد. سپس نحوه معرفی پیش فرض Forms authentication تعریف شده در فایل web.config نیز نیاز به اندکی اصلاح دارد:

<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403MyApp"
cookieless="UseCookies"
loginUrl="~/Account/LogOn"
defaultUrl="~/Home"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200"/>
</authentication>

در اینجا استفاده از کوکی‌ها اجباری شده است. loginUrl به کنترلر و متد لاگین برنامه اشاره می‌کند. defaultUrl مسیری است که کاربر پس از لاگین به صورت خودکار به آن هدایت خواهد شد. همچنین نکته‌ی مهم دیگری را که باید رعایت کرد، name ایی است که در این فایل config عنوان می‌‌کنید. اگر بر روی یک وب سرور، چندین برنامه وب ASP.Net را در حال اجرا دارید، باید برای هر کدام از این‌ها نامی جداگانه و منحصربفرد انتخاب کنید، در غیراینصورت تداخل رخ داده و گزینه مرا به خاطر بسپار شما کار نخواهد کرد.
کار slidingExpiration که در اینجا تنظیم شده است نیز به صورت زیر می‌باشد:
اگر لاگین موفقیت آمیزی ساعت 5 عصر صورت گیرد و timeout شما به عدد 10 تنظیم شده باشد، این لاگین به صورت خودکار در 5:10‌ منقضی خواهد شد. اما اگر در این حین در ساعت 5:05 ، کاربر، یکی از صفحات سایت شما را مرور کند، زمان منقضی شدن کوکی ذکر شده به 5:15 تنظیم خواهد شد(مفهوم تنظیم slidingExpiration). لازم به ذکر است که اگر کاربر پیش از نصف زمان منقضی شدن کوکی (مثلا در 5:04)، یکی از صفحات را مرور کند، تغییری در این زمان نهایی منقضی شدن رخ نخواهد داد.
اگر timeout ذکر نشود، زمان منقضی شدن کوکی ماندگار (persistent) مساوی زمان جاری + زمان منقضی شدن سشن کاربر که پیش فرض آن 30 دقیقه است، خواهد بود.

سپس یک مدل را به نام Account به پوشه مدل‌های برنامه با محتوای زیر اضافه نمائید:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication15.Models
{
public class Account
{
[Required(ErrorMessage = "Username is required to login.")]
[StringLength(20)]
public string Username { get; set; }

[Required(ErrorMessage = "Password is required to login.")]
[DataType(DataType.Password)]
public string Password { get; set; }

public bool RememberMe { get; set; }
}
}

همچنین مطابق تنظیمات اعتبار سنجی مبتنی بر فرم‌های فایل وب کانفیگ، نیاز به یک AccountController نیز هست:

using System.Web.Mvc;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn()
{
return View();
}

[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
return View();
}
}
}

در اینجا در حالت HttpGet فرم لاگین نمایش داده خواهد شد. بنابراین بر روی این متد کلیک راست کرده و گزینه Add view را انتخاب کنید. سپس در صفحه باز شده گزینه Create a strongly typed view را انتخاب کرده و مدل را هم بر روی کلاس Account قرار دهید. قالب scaffolding را هم Create انتخاب کنید. به این ترتیب فرم لاگین برنامه ساخته خواهد شد.
اگر به متد HttpPost فوق دقت کرده باشید، علاوه بر دریافت وهله‌ای از شیء Account، یک رشته را به نام returnUrl نیز تعریف کرده است. علت هم اینجا است که سیستم Forms authentication، صفحه بازگشت را به صورت خودکار به شکل یک کوئری استرینگ به انتهای Url جاری اضافه می‌کند. مثلا:

http://localhost/Account/LogOn?ReturnUrl=something

بنابراین اگر یکی از پارامترهای متد تعریف شده به نام returnUrl باشد، به صورت خودکار مقدار دهی خواهد شد.

تا اینجا زمانیکه برنامه را اجرا کنیم، ابتدا بر اساس تعاریف مسیریابی پیش فرض برنامه، آدرس کنترلر Home و متد Index آن فراخوانی می‌گردد. اما چون در وب کانفیگ برنامه authorization را فعال کرده‌ایم، برنامه به صورت خودکار به آدرس مشخص شده در loginUrl قسمت تعاریف اعتبارسنجی مبتنی بر فرم‌ها هدایت خواهد شد. یعنی آدرس کنترلر Account و متد LogOn آن درخواست می‌گردد. در این حالت صفحه لاگین نمایان خواهد شد.

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

using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn(string returnUrl)
{
if (User.Identity.IsAuthenticated) //remember me
{
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect(FormsAuthentication.DefaultUrl);
}

return View(); // show the login page
}

[HttpGet]
public void LogOut()
{
FormsAuthentication.SignOut();
}

private bool shouldRedirect(string returnUrl)
{
// it's a security check
return !string.IsNullOrWhiteSpace(returnUrl) &&
Url.IsLocalUrl(returnUrl) &&
returnUrl.Length > 1 &&
returnUrl.StartsWith("/") &&
!returnUrl.StartsWith("//") &&
!returnUrl.StartsWith("/\\");
}

[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
if (this.ModelState.IsValid)
{
if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
{
FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
}
}
this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
return View(loginInfo);
}
}
}

در اینجا با توجه به گزینه «مرا به خاطر بسپار»، اگر کاربری پیشتر لاگین کرده و کوکی خودکار حاصل از اعتبار سنجی مبتنی بر فرم‌های او نیز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوی true خواهد بود. بنابراین نیاز است در متد LogOn از نوع HttpGet به این مساله دقت داشت و کاربر اعتبار سنجی شده را به صفحه پیش‌فرض تعیین شده در فایل web.config برنامه یا returnUrl هدایت کرد.
در متد LogOn از نوع HttpPost، کار اعتبارسنجی اطلاعات ارسالی به سرور انجام می‌شود. در اینجا فرصت خواهد بود تا اطلاعات دریافتی، با بانک اطلاعاتی مقایسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا کوکی خودکار FormsAuthentication تنظیم شده و سپس به کمک متد RedirectFromLoginPage کاربر را به صفحه پیش فرض سیستم هدایت می‌کنیم. یا اگر returnUrl ایی وجود داشت، آن‌را پردازش خواهیم کرد.
برای پیاده سازی خروج از سیستم هم تنها کافی است متد FormsAuthentication.SignOut فراخوانی شود تا تمام اطلاعات سشن و کوکی‌های مرتبط، به صورت خودکار حذف گردند.

تا اینجا فیلتر Authorize بدون پارامتر و همچنین در حالت مشخص سازی صریح کاربران به نحو زیر را پوشش دادیم:

[Authorize(Users="Vahid")]

اما هنوز حالت استفاده از Roles در فیلتر Authorize باقی مانده است. برای فعال سازی خودکار بررسی نقش‌های کاربران نیاز است یک Role provider سفارشی را با پیاده سازی کلاس RoleProvider، طراحی کنیم. برای مثال:

using System;
using System.Web.Security;

namespace MvcApplication15.Helper
{
public class CustomRoleProvider : RoleProvider
{
public override bool IsUserInRole(string username, string roleName)
{
if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "User")
return true;
// blabla ...
return false;
}

public override string[] GetRolesForUser(string username)
{
if (username.ToLowerInvariant() == "ali")
{
return new[] { "User", "Helpdesk" };
}

if(username.ToLowerInvariant()=="vahid")
{
return new [] { "Admin" };
}

return new string[] { };
}

public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}

public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}

public override void CreateRole(string roleName)
{
throw new NotImplementedException();
}

public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
throw new NotImplementedException();
}

public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
throw new NotImplementedException();
}

public override string[] GetAllRoles()
{
throw new NotImplementedException();
}

public override string[] GetUsersInRole(string roleName)
{
throw new NotImplementedException();
}

public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}

public override bool RoleExists(string roleName)
{
throw new NotImplementedException();
}
}
}

در اینجا حداقل دو متد IsUserInRole و GetRolesForUser باید پیاده سازی شوند و مابقی اختیاری هستند.
بدیهی است در یک برنامه واقعی این اطلاعات باید از یک بانک اطلاعاتی خوانده شوند؛ برای نمونه به ازای هر کاربر تعدادی نقش وجود دارد. به ازای هر نقش نیز تعدادی کاربر تعریف شده است (یک رابطه many-to-many باید تعریف شود).
در مرحله بعد باید این Role provider سفارشی را در فایل وب کانفیگ برنامه در قسمت system.web آن تعریف و ثبت کنیم:

<roleManager>
<providers>
<clear />
<add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
</providers>
</roleManager>


همین مقدار برای راه اندازی بررسی نقش‌ها در ASP.NET MVC کفایت می‌کند. اکنون امکان تعریف نقش‌ها، حین بکارگیری فیلتر Authorize میسر است:

[Authorize(Roles = "Admin")]
public class HomeController : Controller



نظرات مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
سلام.  سناریویی رو در نظر بگیرید که کاربر(client) برای دسترسی به اکشن‌ها در کنترلر‌های api هر بار با درخواست خود access token که در مرحله قبلی ایجاد و ارسال شده را به سمت سرور ارسال کند. در این مورد میشه فقط با access token کار کرد و refresh token را از سیستم فوق حذف کرد. چون در مطلب فوق فرمودید : "refreshToken فقط یک Guid است. کار آن ساده سازی و به روز رسانی عملیات Login بدون ارائه‌ی نام کاربری و کلمه‌ی عبور است. ".  فلسفه استفاده از refreshToken در سیستم مذکور  با خود accessToken قابل پیاده سازی نیست؟ هر بار که کاربر درخواستی به سمت سرور ارسال کرد AccessToken را ارسال و پس از بررسی و احراز هویت کاربر دسترسی‌ها رو مشخص کند.
پاسخ به بازخورد‌های پروژه‌ها
توضیحاتی در مورد سیستم Identity پروژه
ممنون بابت پاسختون.

در این مورد که فرمودید

"به صورت پیش فرض بهتره یک نقش کلی و سیستمی تحت عنوان مدیران ارشد درج شود و بعد از توزیع پروژه، این امکان وجود خواهد داشت که این افراد گروه‌های کاربری جدید ایجاد کنند و دسترسی به بخش‌ها رو همانطور که متوجه شدید برای گروه درج شده اعمال کنند.  "
 
کاملا درسته و به عبارتی با توجه به ساختار پروژه شما این گروه سیستمی می‌باشد.

و در این مورد :

موردی رو در نظر بگیرید که اصلا هیچ اشتراکی بین دو گروه کاربری از نظر دسترسی‌ها نباشد ، اون موقع چه کار خواهید کرد؟ اینکه مدیر ارشد ما به چه شکل با سیستم کار میکند خود مختار است. 
چون در این سیستم شما نقش هارو پویا کردید مدیر ارشد به راحتی میتونه گروه کاربری درست بکنه که فرضا هیچ اشتراکی نداشته باشن ولی هدف من از طرح این "درخواست راهنمایی" نقش هایی هستند که وجه مشترک دارن . لطفا مثال زیر رو ببینید :


کل مجوز‌ها : A و B و C و D و E و ...

گروه 1 با مجوزهای A و B
گروه 2 با مجوز‌های B و C
( مجوز B بین هر دو مشترکه )
حالا مدیر ارشد میخواد گروهی تشکیل بده که به مجوز A و C نیاز داره. برای این کار بهتره که یک گروه 3 ایجاد کنه که مجوز‌های A و C روداشته باشه. ( دقیقا چیزی که فرمودید یعنی ساخت گروه هایی که نقطه مشترکی ندارن )
حالا شرایطی رخ میده که مدیر ارشد میخواد گروهی تشکیل بده که به مجوز‌های A و B و C نیاز داشته باشه. به نظرتون باید گروه 1 و گروه 2 رو به کاربران مورد نظر اختصاص بده یا اینکه فرضا یک گروه تشکل بده که این 3 مجوز رو داشته باشن و این گروهرو به کاربران اختصاص بده ؟
با توجه به سیستم طراحی شده شما ، مدیر ارشد میتونه هر دو روش رو طوری که دوست داره پیاده سازی بکنه. که یا یک گروه جدید بسازه یا اینکه هر دو گروه رو به کاربر بده. که این پویا بودن کار رو نشون میده ولی یک سوال برای من پیش اومده اونم اینه که فرض کنید مدیر ارشد گروه 1 و گروه 2 رو به یک کاربر اختصاص بده حالا بعد از مدتی میخواد مجوز B رو از این کاربر بگیرن. با این شرایط باز هم مجبورن از هر دو گروه صرفه نظر کنن چون هر دو گروه مجوز B رو دارن.
در کل هدف من اینه اگر فقط اجازه انتخاب یک نقش برای کاربر رو بدیم آیا این روش مناسب هست یا خیر ( منظورم اینه برنامه نویس اجازه بده مدیران ارشد فقط یک نقش به کاربر بدن و نه چند نقش) ؟ چون اگر این کارو بکنیم باز هم مدیر ارشد میتونه هرکاری که دوست داره بکنه و گروه‌های زیادی برای خودش اونطوری که دوست داره بسازه با مجوز‌های مختلف و به کاربران خاصی نقش‌های خاصی بده و در کل جلوی چند نقش دادن به یک کاربر رو بگیریم.