AspNetCoreRateLimit is an ASP.NET Core rate limiting solution designed to control the rate of requests that clients can make to a Web API or MVC app based on IP address or client ID. The AspNetCoreRateLimit package contains an IpRateLimitMiddleware and a ClientRateLimitMiddleware, with each middleware you can set multiple limits for different scenarios like allowing an IP or Client to make a maximum number of calls in a time interval like per second, 15 minutes, etc. You can define these limits to address all requests made to an API or you can scope the limits to each API URL or HTTP verb and path.
در این مطلب، روشی را برای برقراری دسترسی نقش Admin، به تمام قسمتهای محافظت شدهی برنامه، با معرفی نقش آن به یک ویژگی Authorize سفارشی شده، مشاهده کردید. هرچند این روش کار میکند، اما روش جدیدتر برقراری یک چنین دسترسیهای ترکیبی در برنامههای ASP.NET Core و سایر فناوریهای مشتق شدهی از آن، کار با Policyها است که برای نمونه در مثال فوق، به صورت زیر قابل پیاده سازی است:
الف) تعریف Policyهای مشترک بین برنامههای Web API و WASM
Policyهای تعریف شده، باید قابلیت اعمال به اکشن متدهای کنترلرها و همچنین کامپوننتهای WASM را داشته باشند. به همین جهت آنها را در پروژهی اشتراکی BlazorServer.Common که در هر دو پروژه استفاده میشود، قرار میدهیم:
using System.Security.Claims; using Microsoft.AspNetCore.Authorization; // dotnet add package Microsoft.AspNetCore.Authorization namespace BlazorServer.Common { public static class PolicyTypes { public const string RequireAdmin = nameof(RequireAdmin); public const string RequireCustomer = nameof(RequireCustomer); public const string RequireEmployee = nameof(RequireEmployee); public const string RequireEmployeeOrCustomer = nameof(RequireEmployeeOrCustomer); public static AuthorizationOptions AddAppPolicies(this AuthorizationOptions options) { options.AddPolicy(RequireAdmin, policy => policy.RequireRole(ConstantRoles.Admin)); options.AddPolicy(RequireCustomer, policy => policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Customer)) )); options.AddPolicy(RequireEmployee, policy => policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Employee)) )); options.AddPolicy(RequireEmployeeOrCustomer, policy => policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Employee || claim.Value == ConstantRoles.Customer)) )); return options; } } }
ب) افزودن Policyهای تعریف شده به پروژههای Web API و WASM
پس از تعریف Policyهای مورد نیاز، اکنون نوبت به افزودن آنها به برنامههای Web API:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthorization(options => options.AddAppPolicies()); // ...
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { // ... builder.Services.AddAuthorizationCore(options => options.AddAppPolicies()); // ... } } }
ج) استفاده از Policyهای تعریف شده در برنامهی WASM
اکنون که برنامه قابلیت کار با Policyها را پیدا کرده، میتوان فیلتر Roles سفارشی را حذف و با فیلتر Authorize پالیسی دار جایگزین کرد:
@page "/hotel-room-details/{Id:int}" // ... @* @attribute [Roles(ConstantRoles.Customer, ConstantRoles.Employee)] *@ @attribute [Authorize(Policy = PolicyTypes.RequireEmployeeOrCustomer)]
حتی میتوان از پالیسیها در حین تعریف AuthorizeViewها نیز استفاده کرد:
<AuthorizeView Policy="@PolicyTypes.RequireEmployeeOrCustomer"> <p>You can only see this if you're an admin or an employee or a customer.</p> </AuthorizeView>
ASP.NET MVC #24
ASP.NET MVC 4 نسبت به نگارش 3 آن، آنچنان تغییرات اساسی را به همراه ندارد. یک سری افزونه جدید دارد جهت فشرده سازی اسکریپتها و cssها، اندکی بهبود Razor و یک سری مسایل امنیتی که من لابلای همین سری به آنها اشاره داشتم و ... از این نوع مباحث + مبحث Web API رو اضافه کردن که به نوعی ترکیب WCF با ASP.NET MVC است به نحوی شکیل!
البته مجوز نگارش 4 بتای آن که در دسترس هست، Go live است یعنی میتونید همین الان در محیط کار ازش استفاده کنید. هر زمان که نگارش نهایی آن آمد من یک سری ارتقاء به MVC4 رو اضافه خواهم کرد.
مابقی مسایل هم بیشتر تکنیک است تا مباحث پایه. مثلا تکنیکهای استفاده از jQuery UI در ASP.NET MVC یا استفاده از Kendo UI در ASP.NET MVC و موارد مشابه.
- برای SASS فعلا Web Workbench هست. افزونه معروف Web essentials هم به زودی ...
NET 6. #C Blazor ASP.NET Web API ASP.NET MVC MongoDB Redis MediatR | Microservices DDD CQRS Event Sourcing Notification Repository Onion Architecture | Acceptance Testing Integration Testing Unit Testing UI Testing E2E Testing |
سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده، قرار است به چندین مستاجر (مشتری یا tenant) خدماتی را ارائه کند. در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمتها دسترسی دارد.
مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری، در دامین
مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات
خود را از آنجا دریافت میکنند.
در معماری Multi-Tenancy، چندین کاربر میتوانند از یک نمونه (Single
Instance) از اپلیکیشن نرمافزاری استفاده کنند. یعنی این نمونه روی سرور
اجرا میشود و به چندین کاربر سرویس میدهد. هر کاربر را یک Tenant
مینامیم. میتوان به Tenantها امکان تغییر و شخصیسازی بخشی از اپلیکیشن
را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسبوکار، اما آنها نمیتوانند
کدهای اپلیکیشن را شخصیسازی کنند.
خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، سادهتر کردن هر چه بیشتر ساخت برنامههای SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزارهایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحتتر خواهد بود.
شروع کار
یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy (موجود در Nuget) بدهید.PM> Install-Package SaasKit.Multitenancy
شناسایی مستاجر (tenant)
اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری میباشد که میتواند از hostname ، کاربر جاری یا یک HTTP header باشد.ابتدا به تعریف کلاس مستاجر میپردازیم:
public class AppTenant { public string Name { get; set; } public string[] Hostnames { get; set; } }
public class AppTenantResolver : ITenantResolver<AppTenant> { IEnumerable<AppTenant> tenants = new List<AppTenant>(new[] { new AppTenant { Name = "Tenant 1", Hostnames = new[] { "localhost:6000", "localhost:6001" } }, new AppTenant { Name = "Tenant 2", Hostnames = new[] { "localhost:6002" } } }); public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return tenantContext; } }
سیم کشی کردن
بعد از پیاده سازی این اینترفیس نوبت به سیم کشیهای SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامههای ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگیهای SaasKit را ثبت کنیم. فایل startups.cs را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:public void ConfigureServices(IServiceCollection services) { services.AddMultitenancy<AppTenant, AppTenantResolver>(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // after .UseStaticFiles() app.UseMultitenancy<AppTenant>(); // before .UseMvc() }
دریافت مستاجر جاری
حالا هر جا که نیاز به وهلهای از شیء مستاجر جاری داشتید، میتوانید به روش زیر عمل کنید:public class HomeController : Controller { private AppTenant tenant; public HomeController(AppTenant tenant) { this.tenant = tenant; } }
@inject AppTenant Tenant;
<a asp-controller="Home" asp-action="Index">@Tenant.Name</a>
اجرای نمونه مثال
فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم)"commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002", },
dotnet run
و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده میکنیم:
قابل پیکربندی کردن مستاجر ها
از آنجائیکه نوشتن مشخصات مستاجرها در کد زیاد جالب نیست، برای همین
تصمیم داریم که این مشخصات را با استفاده از قابلیتهای ASP.NET Core از
فایل appsettings.json دریافت کنیم. تنظیمات مستاجرها را مطابق اطلاعات زیر به این فایل اضافه کنید:
"Multitenancy": { "Tenants": [ { "Name": "Tenant 1", "Hostnames": [ "localhost:6000", "localhost:6001" ] }, { "Name": "Tenant 2", "Hostnames": [ "localhost:6002" ] } ] }
public class MultitenancyOptions { public Collection<AppTenant> Tenants { get; set; } }
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
public class AppTenantResolver : ITenantResolver<AppTenant> { private readonly IEnumerable<AppTenant> tenants; public AppTenantResolver(IOptions<MultitenancyOptions> options) { this.tenants = options.Value.Tenants; } public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return Task.FromResult(tenantContext); } }
در آخر
اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این
موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما میتوانید عملیاتهای بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن دادهای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمتهای مختلف
برنامه بر اساس هر مستاجر، انجام دهید.
_ سورس مثال بالا در گیت هاب قابل دریافت میباشد.
_ منبع: اینجا
در این مقاله قصد دارم نحوه ساخت یک بات تلگرامی را با استفاده از webhook که پیشنهاد خود تلگرام میباشد و همچنین کار با سایر apiهای مانند گرفتن عکس پروفایل و ... به اشتراک بگذارم. ما آموزش را بنا بر یک مثال کاربردی، در قالب یک بات تلگرامی قرار میدهیم که بعد از start شدن، پیغام خوش آمد گویی را نمایش میدهد و سپس جملات فارسی را از کاربر دریافت و معادل انگلیسی آنها را با استفاده از از google translate به کاربر نشان میدهد.
شروع به ساخت بات
به طور کلی دو روش برای ساخت یک بات تلگرامی وجود دارد:
1- استفاده از کتابخانههای آماده
2 - استفاده webhook
در روش 1، از یک سری از کتابخانههای آماده و تعریف شده، استفاده میکنیم.
مزایا:
بدون زحمت زیادی و فقط با فراخوانی توابع آماده، قادر خواهیم بود یک بات خیلی ساده را شبیه سازی کنیم.
هزینه آن نسبت به webhook کمتر است و شما میتوانید با یک vps، بات را اجرا کنید.
معایب:
1- این روش ازpolling استفاد میکند. یعنی دستور دریافت شده در یک حلقهی بی نهایت قرا میگیرد و هر بار چک میشود که آیا درخواستی رسیده است یا خیر؟ که سربار بالایی را بر روی سرور ما خواهد داشت.
2- بعد از مدتی down میشود.
3- اگر شمار درخواستها بالا رود، Down میشود.
روش دیگر استفاده از webhook است که اصولیترین روش و روشی است که خود سایت تلگرام آن را پیشنهاد دادهاست. اگر بخواهم توضیح کوتاهی درباره webhook بدهم، با استفاده از آن میتوانید تعیین کنید وقتی یک event، رخداد، api ایی فرخوانی شود؛ یا مثلا شما یک سایت را با api نوشتهاید (ASP.NET Web API) و آن را پابلیش کردهاید و الان میخواهید یک api جدید را بنویسید. در این حالت با استفاده از webhook، دیگر نیازی نیست تا کل پروژه را پابلیش کنید. یک پروژه api را مینویسید و آن را آپلود میکنید و درقسمت تنظیم وب هوک، آدرس دامین خودتون را میدهید. حتی میتوانید آن را با php یا هر زبانی که میتوانید بنویسید.
معایب:
1- هزینه آن. شما علاوه بر تهیهی هاست و دامین، باید ssl را هم فعال کنید که در ادامه بیشتر توضیح خواهیم داد. البته نگرانی برای پیاده سازی ssl نیست. چون سایتهایی هستند که این سرویسها را به صورت رایگان در اختیار شما میگذارند (مانند Lets encrypt).
2- تنظیم آن به مراتب سختتر از روش قبل است.
مزایا:
1- سرعت آن بیشتر است.
2- درخواستهای با تعداد بالا را میتوان به راحتی پاسخ داد.
3- وابستگی ثالثی ندارد.
اولین مرحله ساخت بات
تا اینجای کار به مباحث تئوری باتها پرداختیم. حال وارد اولین مرحلهی ساخت باتها میشویم. قبل از شروع، شما باید در بات BotFather@ عضو شوید و سپس یک بات جدید را بسازید. برای آموزش ساخت بات در BotFather، میتوانید از این مبحث استفاده کنید. بعد از ساخت بات در BotFather، شما داری یک token خواهید شد که یک رشتهی کد شدهاست.
ایجاد پروژهی جدید بات
- در ادامه سراغ ویژوال استودیو رفته و یک پروژهی Web api Empty را ایجاد کنید.
- سپس وارد سایت تلگرام شوید و کتابخانهی مربوطه را دریافت کنیدو یا میتوانید با استفاده از دستور زیر، این کتابخانه را نصب کنید:
Install-Package Telegram.Bot
[HttpPost] public async Task<IHttpActionResult> UpdateMsg(Update update) { //...... }
تنظیم کردن WebHook
- حال به قسمت تنظیم کردن webhook میرسیم. وارد فایل Global.asax.cs برنامه شوید و با دستور زیر، وب هوک را تنظیم کنید:
var bot = new Telegram.Bot.TelegramBotClient("Token"); bot.SetWebhookAsync("https://Domian/api/webhook").Wait();
- در این حالت بعد از اجرای ngrok، آدرس https آن را کپی کرده و در قسمت بالا، بجای Domain ذکر شده قرار دهید.
- برای آزمایش انجام کار، یک break-point را در قسمت action متد یاد شده قرار دهید و سپس برنامه را اجرا کنید.
- اکنون از طریق تلگرام وارد بات شوید و یک درخواست را ارسال کنید.
- اگر کار را به درستی انجام داده باشید، در صفحه ngrok پیغام 200 مبتنی بر ارسال صحیح درخواست را دریافت خواهید کرد و همچنین در قسمت breakpoints برنامه بر روی آرگومان update، میتوانید پراپرتیهای یک درخواست را به صورت کامل دریافت کنید.
- ارسال اولین درخواست ما از طریق بات start/ میباشد. در این حالت میتوان دریافت که کاربر برای بار اول است که از بات استفاده میکند.
- در اکشن متد از طریق خاصیت update.Message.Text میتوان به متن فرستاده شده دسترسی داشت.
- همچین اطلاعات کاربر در update.Message.From، همراه با درخواست، فرستاده میشود.
کار با ابزار ترجمهی گوگل و تکمیل پروژهی Web API
اکنون طبق مثال بالا میخواهیم وقتی کاربر برای اولین بار وارد شد، پیغام خوش آمد گویی به او نمایش داده شود. بعد از آن هر متنی را که فرستاد، معنای آن را از گوگل ترنسلیت گرفته و مجددا به کاربر ارسال میکنیم. برای اینکار کلاس WebhookController را به شکل زیر تکمیل خواهیم کرد:
namespace Telegrambot.Controllers { public class WebhookController : ApiController { Telegram.Bot.TelegramBotClient _bot = new Telegram.Bot.TelegramBotClient("number"); Translator _translator = new Translator(); [HttpPost] public async Task<IHttpActionResult> UpdateMsg(Update update) { if (update.Message.Text == "/start") { await _bot.SendTextMessageAsync(update.Message.From.Id, "Welcome To My Bot"); } else { var translatedRequest = _translator.Translate(update.Message.Text, "Persian", "English"); await _bot.SendTextMessageAsync(update.Message.From.Id, translatedRequest); } return Ok(update); } } }
- با استفاده از update.Message.From.Id میتوان پیغام را به شخصی که درخواست دادهاست فرستاد.
- دقت کنید هنگام ارسال درخواست، در ngrok آیا درخواستی فرستاده میشود یا خطایی وجود دارد.
نکته! برای استفاده از بات باید حتما از ssl استفاده کنید. اگر نیاز به خرید این سرویس را ندارید، از این لینک نیز میتوانید سرویس مورد نظر را بعد از 24 ساعت بر روی دامین خود تنظیم کنید.
توضیحات بیشتر در این مورد را مثلا دکمههای پویا و گرفتن عکس پروفایل و ....، در مقالهی بعدی قرار خواهم داد.
شما میتوانید از این لینک پروژه بالا را دریافت و اجرا کنید .
بررسی 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" }) }; }
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address, "roles", "imagegalleryapi" },
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
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"; });
سپس متد 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();
اکنون میتوانیم اجبار به 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>
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید 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" ] }
همچنین اگر دقت کنید، 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()
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;
مرحلهی بعد، مراجعه به 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(); }
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); }
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی میکنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس 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" }) }; }
سپس کار با اکشن متد 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) {
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> }
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public IActionResult AddImage() { return View(); }
[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 وارد کنید.
No Authentication
اگر گزینه No Authentication را انتخاب کنید، پروژه ایجاد شده صفحاتی را برای ورود به سایت نخواهد ساخت. همچنین رابط کاربری ای برای نمایش کاربر فعلی، کلاسهای موجودیتها برای یک دیتابیس عضویت و رشتههای اتصالی نیز وجود نخواهند داشت.
- سیستم عضویت جدید بجای استفاده از ماژول ASP.NET Forms Authentication بر پایه OWIN نوشته شده است. این بدین معنا است که از یک مکانیزم احراز هویت واحد میتوانید در اپلیکیشنهای ASP.NET Web Forms, MVC, Web API و SignalR استفاده کنید.
- سیستم عضویت جدید توسط Entity Framework Code First مدیریت میشود و شامل تمامی کلاسهایی است که نماینده جداول و موجودیتها هستند. این بدین معنا است که روی الگوی دیتابیس کنترل کامل دارید. سفارشی سازی و تغییر اطلاعات کاربران و پروفایل هایشان بسیار سادهتر است، تنها لازم است برای اعمال تغییرات از Code First Migrations استفاده کنید.
- asp.net/identity
- Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on
- Web API - External Authentication Services
- Adding External Logins to your ASP.NET application in Visual Studio 2013
اگر گزینه Organizational Accounts را انتخاب کنید پروژه ایجاد شده برای استفاده از (Windows Identity Foundation (WIF پیکربندی خواهد شد. این فریم ورک برای احراز هویت کاربران از (Windows Azure Active Directory (WAAD استفاده میکند که شامل Office 365 نیز میشود.
از این گزینه برای احراز هویت کاربرانی استفاده کنید که در قالب یک OWIN Tenant تعریف میشوند. برای مثال سایتی با نام Company.com داریم که برای کارمندان این سازمان از طریق company.onmicrosoft.com قابل دسترسی خواهد بود. نمیتوانید WAAD را طوری پیکربندی کنید که کاربران tenantهای دیگر نیز به اپلیکیشن شما دسترسی داشته باشند.
Domain
نام دامنهای در WAAD که میخواهید اپلیکیشن را برای آن پیکربندی کنید، مثلا company.onmicrosoft.com. اگر از custom domain ها استفاده میکنید مانند company.com بجای company.onmicrosoft.com میتوانید این اطلاعات را اینجا وارد کنید.
سطح دسترسی
اگر اپلیکیشن نیاز به کوئری گرفتن یا بروز رسانی اطلاعات پوشهها (directory information) را توسط Graph API دارد، از گزینههای Single Sign-On, Read Directory Data و یا Single Sign-On, Read and Write Directory Data استفاده کنید. در غیر اینصورت گزینه Single Sign-On را رها کنید. برای اطلاعات بیشتر به Application Access Levels و Using the Graph API to Query Windows Azure AD مراجعه کنید.
Application ID URI
بصورت پیش فرض، قالب پروژه یک شناسه application ID URI برای شما تولید میکند، که این کار با الحاق نام پروژه شما به نام دامنه WAAD صورت میگیرد. برای مثال، اگر نام پروژه Example باشد و نام دامنه contoso.onmicrosoft.com، شناسه خروجی https://contoso.onmicrosoft.com/Example میشود. اگر میخواهید بصورت دستی این فیلد را مقدار دهی کنید، گزینه More Options را انتخاب کنید. این شناسه باید با //:https شروع شود.
بصورت پیش فرض، اگر اپلیکیشنی که در WAAD تهیه و تدارک دیده شده است، شناسهای یکسان با شناسه موجود در پروژه Visual Studio داشته باشد، پروژه شما به اپلیکیشن موجود در WAAD متصل خواهد شد. اگر میخواهید تدارکات جدیدی ببینید تیک گزینه Overwrite the application entry if one with the same ID already exists را حذف کنید.
اگر تیک این گزینه حذف شده باشد، و ویژوال استودیو اپلیکیشنی با شناسهای یکسان را پیدا کند، عددی به آخر URI اضافه خواهد شد. مثلا فرض کنید نام پروژه Example است و اپلیکیشنی نیز با شناسه https://contoso.onmicrosoft.com/Example در WAAD وجود دارد. در این صورت اپلیکیشن جدیدی با شناسه ای مانند https://contoso.onmicrosoft.com/Example_ 20130619330903 ایجاد میشود.
تهیه و تدارک اپلیکیشن در WAAD
برای آنکه یک اپلیکیشن WAAD ایجاد کنید و یا پروژه را به یک اپلیکیشن موجود متصل کنید، ویژوال استودیو به اطلاعات ورود یک مدیر کل برای دامنه مورد نظر، نیاز دارد. هنگامی که در دیالوگ Configure Authentication روی OK کلیک میکنید، اطلاعات ورود یک مدیر کل از شما درخواست میشود و نهایتا هنگامیکه روی Create Project کلیک میکنید، ویژوال استودیو اپلیکیشن شما را در WAAD پیکربندی میکند.
برای اطلاعات بیشتر درباره نحوه استفاده از مدل احراز هویت Cloud - Single Organization به لینکهای زیر مراجعه فرمایید:
- Windows Azure Authentication
- Adding Sign-On to Your Web Application Using Windows Azure AD
- Developing ASP.NET Apps with Windows Azure Active Directory
از این گزینه برای احراز هویت کاربرانی استفاده کنید که در WAAD tenantهای متعددی تعریف شدهاند. برای مثال، نام سایت contoso.com است و برای کارمندان دو سازمان از طریق آدرسهای contoso.onmicrosoft.com و fabrikam.onmicrosoft.com قابل دسترسی خواهد بود. نحوه پیکربندی این مدل نیز مانند قسمت قبلی است.
برای اطلاعات بیشتر درباره احراز هویت Cloud - Multi Organization به لینکهای زیر مراجعه کنید:
- Easy Web App Integration with Windows Azure Active Directory, ASP.NET & Visual Studio
- Developing Multi-Tenant Web Applications with Windows Azure AD
این گزینه را هنگامی انتخاب کنید که کاربران در (Windows Server Active Directory (AD تعریف شده اند و نمیخواهید از WAAD استفاده کنید. از این مدل برای ایجاد وب سایتهای اینترنت و اینترانت میتوانید استفاده کنید. برای یک وب سایت اینترنتی از (Active Directory Federation Services (ADFS استفاده کنید.
برای یک وب سایت اینترانتی، میتوانید کلا این گزینه را رها کنید و از Windows Authentication استفاده کنید. در صورت استفاده از گزینه Windows Authentication لازم نیست تا آدرس سند متادیتا (metadata document URL) را فراهم کنید، همچنین توجه داشته باشید که Windows Authentication امکان کوئری گرفتن از پوشهها و کنترل سطوح دسترسی در Active Directory را ندارد.
On-Premises Authority
آدرس سند متادیتا. این سند اطلاعاتی درباره مختصات Authority دارد که اپلیکیشن از آنها برای به پیش بردن روند احراز هویت و ورود به سایت استفاده میکند.
Application ID URI
یک شناسه منحصر به فرد که AD از آن برای شناسایی اپلیکیشن استفاده میکند. میتوانید این فیلد را خالی رها کنید تا ویژوال استودیو بصورت خودکار اپلیکیشنی بدین منظور بسازد.
قدمهای بعدی
در این مقاله با مدلهای مختلف احراز هویت در اپلیکیشنهای Visual Studio 2013 آشنا شدید و برخی تغییرات و امکانات جدید نیز بررسی شدند. برای اطلاعات تکمیلی به ASP.NET and Web Tools for Visual Studio 2013 Release Notes مراجعه کنید.
I just finished teaching the first week of a 9-week, super-intensive, hands-on boot camp that focuses on TypeScript/ASP.NET Web API/AngularJS. I decided to write a series of blog posts that describes each week of the camp. Who makes it. The challenges the students encounter. And, the material that we cover