اشتراک‌ها
اعتبارسنجی فیلدها به روش Async Validation در ASP.NET
فرض کنید کاربر الان دارد فیلد Username را پر می‌کند و ازآن خارج می‌شود و بعد از آن نوبت به پر کردن فیلد رمز عبور می‌شود. در همین لحظه ای که ما از Textbox مربوط به Username خارج می‌شویم.
 Username به صورت غیر همزمان به سرور پاس داده می‌شود و سرور چک می‌کند که این نام کاربری مورد تایید سایت قرار گرفته یا نه. دقت کنید همه این‌ها در چند لحظه کوتاه اتفاق می‌افتد و در صورتی که نام Username مورد تایید سایت قرار نگیرد در همان لحظه به کاربر پیامی مبنی بر عدم تایید Username می‌دهیم و کاربر قبل از اینکه دکمه تایید را بفشارد Username را تصحیح می‌کند. 
اعتبارسنجی فیلدها به روش Async Validation در ASP.NET
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت یازدهم- استفاده از تامین کننده‌های هویت خارجی
همیشه نمی‌توان کاربران را وادار به استفاده‌ی از صفحه‌ی لاگین برنامه‌ی IDP کرد. ممکن است کاربران بخواهند توسط سطوح دسترسی خود در یک شبکه‌ی ویندوزی به سیستم وارد شوند و یا از Social identity providers مانند تلگرام، گوگل، فیس‌بوک، توئیتر و امثال آن‌ها برای ورود به سیستم استفاده کنند. برای مثال شاید کاربری بخواهد توسط اکانت گوگل خود به سیستم وارد شود. همچنین مباحث two-factor authentication را نیز باید مدنظر داشت؛ برای مثال ارسال یک کد موقت از طریق ایمیل و یا SMS و ترکیب آن با روش فعلی ورود به سیستم جهت بالا بردن میزان امنیت برنامه.
در این مطلب نحوه‌ی یکپارچه سازی Windows Authentication دومین‌های ویندوزی را با IdentityServer بررسی می‌کنیم.


کار با تامین کننده‌های هویت خارجی

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


نگاهی به شیوه‌ی پشتیبانی از تامین کننده‌های هویت خارجی توسط Quick Start UI

Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کننده‌های هویت خارجی نیز می‌باشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl)

[HttpGet]
public async Task<IActionResult> Callback()
زمانیکه کاربر بر روی یکی از تامین کننده‌های لاگین خارجی در صفحه‌ی لاگین کلیک می‌کند، اکشن Challenge، نام provider مدنظر را دریافت کرده و پس از آن returnUrl را به اکشن متد Callback به صورت query string ارسال می‌کند. اینجا است که کاربر به تامین کننده‌ی هویت خارجی مانند گوگل منتقل می‌شود. البته مدیریت حالت Windows Authentication و استفاده از اکانت ویندوزی در اینجا متفاوت است؛ از این جهت که از returnUrl پشتیبانی نمی‌کند. در اینجا اطلاعات کاربر از اکانت ویندوزی او به صورت خودکار استخراج شده و به لیست Claims او اضافه می‌شود. سپس یک کوکی رمزنگاری شده از این اطلاعات تولید می‌شود تا در ادامه از محتویات آن استفاده شود.
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شده‌ی متد Challenge استخراج می‌شود و بر اساس آن هویت کاربر در سطح IDP شکل می‌گیرد.


فعالسازی Windows Authentication برای ورود به IDP

در ادامه می‌خواهیم برنامه را جهت استفاده‌ی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامه‌های ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامه‌ی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمه‌ی جدید Windows را در قسمت External Login مشاهده خواهید کرد:


یک نکته: برچسب این دکمه را در حالت استفاده‌ی از مشتقات IIS، به صورت زیر می‌توان تغییر داد:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows Account";
                iis.AutomaticAuthentication = false;
            });

اتصال کاربر وارد شده‌ی از یک تامین کننده‌ی هویت خارجی به کاربران بانک اطلاعاتی برنامه

سازنده‌ی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آن‌را در قسمت قبل تغییر دادیم، از TestUserStore استفاده می‌کند:
        public ExternalController(
            IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IEventService events,
            TestUserStore users = null)
        {
            _users = users ?? new TestUserStore(TestUsers.Users);

            _interaction = interaction;
            _clientStore = clientStore;
            _events = events;
        }
بنابراین در ابتدا آن‌را با IUsersService تعویض خواهیم کرد:
        private readonly IUsersService _usersService;
        public ExternalController(
    // ...
            IUsersService usersService)
        {
    // ...
            _usersService = usersService;
        }
و سپس تمام ارجاعات قبلی به users_ را نیز توسط امکانات این سرویس اصلاح می‌کنیم:
الف) در متد FindUserFromExternalProvider
سطر قدیمی
 var user = _users.FindByExternalProvider(provider, providerUserId);
به این صورت تغییر می‌کند:
 var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
در این حالت امضای این متد نیز باید اصلاح شود تا async شده و همچنین User را بجای TestUser بازگشت دهد:
 private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
ب) متد AutoProvisionUser قبلی
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims)
{
   var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
   return user;
}
نیز باید حذف شود؛ زیرا در ادامه آن‌را با صفحه‌ی ثبت نام کاربر، جایگزین می‌کنیم.
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحه‌ی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کرده‌ایم و از آن می‌شود در اینجا استفاده‌ی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کننده‌ی هویت خارجی را به نحوی اصلاح می‌کنیم که کاربر جدید، ابتدا به صفحه‌ی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامه‌ی مراحل را طی نماید:
در اکشن متد نمایش صفحه‌ی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحه‌ی قبلی را دریافت می‌کند:
[HttpGet]
public IActionResult RegisterUser(string returnUrl)
اکنون نیاز است اطلاعات Provider و ProviderUserId را نیز در اینجا دریافت کرد. به همین جهت ViewModel زیر را به برنامه اضافه می‌کنیم:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class RegistrationInputModel
    {
        public string ReturnUrl { get; set; }
        public string Provider { get; set; }
        public string ProviderUserId { get; set; }

        public bool IsProvisioningFromExternal => !string.IsNullOrWhiteSpace(Provider);
    }
}
سپس با داشتن اطلاعات FindUserFromExternalProvider که آن‌را در قسمت الف اصلاح کردیم، اگر خروجی آن null باشد، یعنی کاربری که از سمت تامین کننده‌ی هویت خارجی به برنامه‌ی ما وارد شده‌است، دارای اکانتی در سمت IDP نیست. به همین جهت او را به صفحه‌ی ثبت نام کاربر هدایت می‌کنیم. همچنین پس از پایان کار ثبت نام نیاز است مجددا به همینجا، یعنی متد Callback که فراخوان FindUserFromExternalProvider است، بازگشت:
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class ExternalController : Controller
    {
        public async Task<IActionResult> Callback()
        {
            var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
            var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user = AutoProvisionUser(provider, providerUserId, claims);
                
                var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl });
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" ,
                    new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId });
                return Redirect(continueWithUrl);
            }
در اینجا نحوه‌ی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحه‌ی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده می‌کنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره می‌کند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.

در ادامه نیاز است امضای متد نمایش صفحه‌ی ثبت نام را نیز بر این اساس اصلاح کنیم:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class UserRegistrationController : Controller
    {
        [HttpGet]
        public IActionResult RegisterUser(RegistrationInputModel registrationInputModel)
        {
            var vm = new RegisterUserViewModel
            {
                ReturnUrl = registrationInputModel.ReturnUrl,
                Provider = registrationInputModel.Provider,
                ProviderUserId = registrationInputModel.ProviderUserId
            };

            return View(vm);
        }
به این ترتیب اطلاعات provider نیز علاوه بر ReturnUrl در اختیار View آن قرار خواهد گرفت. البته RegisterUserViewModel هنوز شامل این خواص اضافی نیست. به همین جهت با ارث بری از RegistrationInputModel، این خواص در اختیار RegisterUserViewModel نیز قرار می‌گیرند:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class RegisterUserViewModel : RegistrationInputModel
    {

اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه می‌کنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آن‌ها نیاز داریم:
<inputtype="hidden"asp-for="ReturnUrl"/>
<inputtype="hidden"asp-for="Provider"/>
<inputtype="hidden"asp-for="ProviderUserId"/>
- با توجه به اینکه کاربر از طریق یک تامین کننده‌ی هویت خارجی وارد شده‌است، دیگر نیازی به ورود کلمه‌ی عبور ندارد. به همین جهت خاصیت آن‌را در ViewModel مربوطه به صورت Required تعریف نکرده‌ایم:
@if (!Model.IsProvisioningFromExternal)
{
    <div>
        <label asp-for="Password"></label>
        <input type="password" placeholder="Password"
               asp-for="Password" autocomplete="off">
    </div>
}
مابقی این فرم ثبت نام مانند قبل خواهد بود.

پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class UserRegistrationController : Controller
    {
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> RegisterUser(RegisterUserViewModel model)
        {
    // ...
            
            if (model.IsProvisioningFromExternal)
            {
                userToCreate.UserLogins.Add(new UserLogin
                {
                    LoginProvider = model.Provider,
                    ProviderKey = model.ProviderUserId
                });
            }

            // add it through the repository
            await _usersService.AddUserAsync(userToCreate);

// ...
        }
    }
که اینکار را با مقدار دهی UserLogins کاربر در حال ثبت، انجام داده‌ایم.
همچنین در ادامه‌ی این اکشن متد، کار لاگین خودکار کاربر نیز انجام می‌شود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کننده‌ی خارجی، غیرفعال می‌کنیم:
if (!model.IsProvisioningFromExternal)
{
    // log the user in
    // issue authentication cookie with subject ID and username
    var props = new AuthenticationProperties
    {
        IsPersistent = false,
        ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
    };
    await HttpContext.SignInAsync(userToCreate.SubjectId, userToCreate.Username, props);
}

بررسی ورود به سیستم توسط دکمه‌ی External Login -> Windows

پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینه‌ی External Login -> Windows را انتخاب می‌کنیم. بلافاصله به صفحه‌ی ثبت‌نام کاربر هدایت خواهیم شد:


همانطور که مشاهده می‌کنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کرده‌است. همچنین در اینجا خبری از ورود کلمه‌ی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامه‌ی MVC Client انجام می‌شود.
در ادامه از برنامه‌ی کلاینت logout کنید. اکنون در صفحه‌ی login مجددا بر روی دکمه‌ی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامه‌ی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شده‌است.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.

یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامه‌ی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمی‌کنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکته‌ی تکمیلی: UseHttpSys و استفاده‌ی از HTTPS»  فعال کنید.
مطالب
مهاجرت از SQL Membership به ASP.NET Identity
در این مقاله مهاجرت یک اپلیکیشن وب که توسط SQL Membership ساخته شده است را به سیستم جدید ASP.NET Identity بررسی می‌کنیم. برای این مقاله از یک قالب اپلیکیشن وب (Web Forms) که توسط Visual Studio 2010 ساخته شده است برای ساختن کاربران و نقش‌ها استفاده می‌کنیم. سپس با استفاده از یک SQL Script دیتابیس موجود را به دیتابیسی که ASP.NET Identity نیاز دارد تبدیل می‌کنیم. در قدم بعدی پکیج‌های مورد نیاز را به پروژه اضافه می‌کنیم و صفحات جدیدی برای مدیریت حساب‌های کاربری خواهیم ساخت. بعنوان یک تست، کاربران قدیمی که توسط SQL Membership ساخته شده بودند باید قادر باشند به سایت وارد شوند. همچنین کاربران جدید باید بتوانند بدون هیچ مشکلی در سیستم ثبت نام کنند. سورس کد کامل این مقاله را می‌توانید از این لینک دریافت کنید.


یک اپلیکیشن با SQL Membership بسازید

برای شروع به اپلیکیشنی نیاز داریم که از SQL Membership استفاده می‌کند و دارای داده هایی از کاربران و نقش‌ها است. برای این مقاله، بگذارید پروژه جدیدی توسط VS 2010 بسازیم.

حال با استفاده از ابزار ASP.NET Configuration دو کاربر جدید بسازید: oldAdminUser و oldUser.

نقش جدیدی با نام Admin بسازید و کاربر oldAdminUser را به آن اضافه کنید.

بخش جدیدی با نام Admin در سایت خود بسازید و فرمی بنام Default.aspx به آن اضافه کنید. همچنین فایل web.config این قسمت را طوری پیکربندی کنید تا تنها کاربرانی که در نقش Admin هستند به آن دسترسی داشته باشند. برای اطلاعات بیشتر به این لینک مراجعه کنید.

پنجره Server Explorer را باز کنید و جداول ساخته شده توسط SQL Membership را بررسی کنید. اطلاعات اصلی کاربران که برای ورود به سایت استفاده می‌شوند، در جداول aspnet_Users و aspnet_Membership ذخیره می‌شوند. داده‌های مربوط به نقش‌ها نیز در جدول aspnet_Roles ذخیره خواهند شد. رابطه بین کاربران و نقش‌ها نیز در جدول aspnet_UsersInRoles ذخیره می‌شود، یعنی اینکه هر کاربری به چه نقش هایی تعلق دارد.

برای مدیریت اساسی سیستم عضویت، مهاجرت جداول ذکر شده به سیستم جدید ASP.NET Identity کفایت می‌کند.

مهاجرت به Visual Studio 2013

  • برای شروع ابتدا Visual Studio Express 2013 for Web یا Visual Studio 2013 را نصب کنید.
  • حال پروژه ایجاد شده را در نسخه جدید ویژوال استودیو باز کنید. اگر نسخه ای از SQL Server Express را روی سیستم خود نصب نکرده باشید، هنگام باز کردن پروژه پیغامی به شما نشان داده می‌شود. دلیل آن وجود رشته اتصالی است که از SQL Server Express استفاده می‌کند. برای رفع این مساله می‌توانید SQL Express را نصب کنید، و یا رشته اتصال را طوری تغییر دهید که از LocalDB استفاده کند.
  • فایل web.config را باز کرده و رشته اتصال را مانند تصویر زیر ویرایش کنید.

  • پنجره Server Explorer را باز کنید و مطمئن شوید که الگوی جداول و داده‌ها قابل رویت هستند.
  • سیستم ASP.NET Identity با نسخه 4.5 دات نت فریم ورک و بالا‌تر سازگار است. پس نسخه فریم ورک پروژه را به آخرین نسخه (4.5.1) تغییر دهید.

پروژه را Build کنید تا مطمئن شوید هیچ خطایی وجود ندارد.

نصب پکیج‌های NuGet

در پنجره Solution Explorer روی نام پروژه خود کلیک راست کرده، و گزینه Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده، عبارت "Microsoft.AspNet.Identity.EntityFramework" را وارد کنید. این پکیج را در لیست نتایج انتخاب کرده و آن را نصب کنید. نصب این بسته، نیازمندهای موجود را بصورت خودکار دانلود و نصب می‌کند: EntityFramework و ASP.NET Identity Core. حال پکیج‌های زیر را هم نصب کنید (اگر نمی‌خواهید OAuth را فعال کنید، 4 پکیج آخر را نادیده بگیرید).
  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Facebook
  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.MicrosoftAccount
  • Microsoft.Owin.Security.Twitter

مهاجرت دیتابیس فعلی به سیستم ASP.NET Identity

قدم بعدی مهاجرت دیتابیس فعلی به الگویی است، که سیستم ASP.NET Identity به آن نیاز دارد. بدین منظور ما یک اسکریپت SQL را اجرا می‌کنیم تا جداول جدیدی بسازد و اطلاعات کاربران را به آنها انتقال دهد. فایل این اسکریپت را می‌توانید از لینک https://github.com/suhasj/SQLMembership-Identity-OWIN دریافت کنید.
این اسکریپت مختص این مقاله است. اگر الگوی استفاده شده برای جداول سیستم عضویت شما ویرایش/سفارشی-سازی شده باید این اسکریپت را هم بر اساس این تغییرات بروز رسانی کنید.
پنجره Server Explorer را باز کنید. گره اتصال ApplicationServices را باز کنید تا جداول را مشاهده کنید. روی گره Tables کلیک راست کرده و گزینه New Query را انتخاب کنید.

در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.

ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف می‌کنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، می‌توانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.

لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان می‌دهد.

  • aspnet_Roles --> AspNetRoles
  • aspnet_Users, aspnet_Membership --> AspNetUsers
  • aspnet_UsersInRoles --> AspNetUserRoles

جداول AspNetUserClaims و AspNetUserLogins خالی هستند. فیلد تفکیک کننده (Discriminator) در جدول AspNetUsers باید مطابق نام کلاس مدل باشد، که در مرحله بعدی تعریف خواهد شد. همچنین ستون PasswordHash به فرم 'encrypted password|password salt|password format' می‌باشد. این شما را قادر می‌سازد تا از رمزنگاری برای ذخیره و بازیابی کلمه‌های عبور استفاده کنید. این مورد نیز در ادامه مقاله بررسی شده است.



ساختن مدل‌ها و صفحات عضویت

بصورت پیش فرض سیستم ASP.NET Identity برای دریافت و ذخیره اطلاعات در دیتابیس عضویت از Entity Framework استفاده می‌کند. برای آنکه بتوانیم با جداول موجود کار کنیم، می‌بایست ابتدا مدل هایی که الگوی دیتابیس را نمایندگی می‌کنند ایجاد کنیم. برای این کار مدل‌های ما یا باید اینترفیس‌های موجود در Identity.Core را پیاده سازی کنند، یا می‌توانند پیاده سازی‌های پیش فرض را توسعه دهند. پیاده سازی‌های پیش فرض در Microsoft.AspNet.Identity.EntityFramework وجود دارند.
در نمونه ما، جداول AspNetRoles, AspNetUserClaims, AspNetLogins و AspNetUserRole ستون هایی دارند که شباهت زیادی به پیاده سازی‌های پیش فرض سیستم Identity دارند. در نتیجه می‌توانیم از کلاس‌های موجود، برای Map کردن الگوی جدید استفاده کنیم. جدول AspNetUsers ستون‌های جدیدی نیز دارد. می‌توانیم کلاس جدیدی بسازیم که از IdentityUser ارث بری کند و آن را گسترش دهیم تا این فیلدهای جدید را پوشش دهد.
پوشه ای با نام Models بسازید (در صورتی که وجود ندارد) و کلاسی با نام User به آن اضافه کنید.

کلاس User باید کلاس IdentityUser را که در اسمبلی Microsoft.AspNet.Identity.EntityFramework وجود دارد گسترش دهد. خاصیت هایی را تعریف کنید که نماینده الگوی جدول AspNetUser هستند. خواص ID, Username, PasswordHash و SecurityStamp در کلاس IdentityUser تعریف شده اند، بنابراین این خواص را در لیست زیر نمی‌بینید.

  public class User : IdentityUser
    {
        public User()
        {
            CreateDate = DateTime.Now;
            IsApproved = false;
            LastLoginDate = DateTime.Now;
            LastActivityDate = DateTime.Now;
            LastPasswordChangedDate = DateTime.Now;
            LastLockoutDate = DateTime.Parse("1/1/1754");
            FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754");
            FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754");
        }

        public System.Guid ApplicationId { get; set; }
        public string MobileAlias { get; set; }
        public bool IsAnonymous { get; set; }
        public System.DateTime LastActivityDate { get; set; }
        public string MobilePIN { get; set; }
        public string Email { get; set; }
        public string LoweredEmail { get; set; }
        public string LoweredUserName { get; set; }
        public string PasswordQuestion { get; set; }
        public string PasswordAnswer { get; set; }
        public bool IsApproved { get; set; }
        public bool IsLockedOut { get; set; }
        public System.DateTime CreateDate { get; set; }
        public System.DateTime LastLoginDate { get; set; }
        public System.DateTime LastPasswordChangedDate { get; set; }
        public System.DateTime LastLockoutDate { get; set; }
        public int FailedPasswordAttemptCount { get; set; }
        public System.DateTime FailedPasswordAttemptWindowStart { get; set; }
        public int FailedPasswordAnswerAttemptCount { get; set; }
        public System.DateTime FailedPasswordAnswerAttemptWindowStart { get; set; }
        public string Comment { get; set; }
    }

حال برای دسترسی به دیتابیس مورد نظر، نیاز به یک DbContext داریم. اسمبلی Microsoft.AspNet.Identity.EntityFramework کلاسی با نام IdentityDbContext دارد که پیاده سازی پیش فرض برای دسترسی به دیتابیس ASP.NET Identity است. نکته قابل توجه این است که IdentityDbContext آبجکتی از نوع TUser را می‌پذیرد. TUser می‌تواند هر کلاسی باشد که از IdentityUser ارث بری کرده و آن را گسترش می‌دهد.

در پوشه Models کلاس جدیدی با نام ApplicationDbContext بسازید که از IdentityDbContext ارث بری کرده و از کلاس User استفاده می‌کند.

public class ApplicationDbContext : IdentityDbContext<User>
{
        
}

مدیریت کاربران در ASP.NET Identity توسط کلاسی با نام UserManager انجام می‌شود که در اسمبلی Microsoft.AspNet.Identity.EntityFramework قرار دارد. چیزی که ما در این مرحله نیاز داریم، کلاسی است که از UserManager ارث بری می‌کند و آن را طوری توسعه می‌دهد که از کلاس User استفاده کند.

در پوشه Models کلاس جدیدی با نام UserManager بسازید.

public class UserManager : UserManager<User>
{
        
}

کلمه عبور کاربران بصورت رمز نگاری شده در دیتابیس ذخیره می‌شوند. الگوریتم رمز نگاری SQL Membership با سیستم ASP.NET Identity تفاوت دارد. هنگامی که کاربران قدیمی به سایت وارد می‌شوند، کلمه عبورشان را توسط الگوریتم‌های قدیمی SQL Membership رمزگشایی می‌کنیم، اما کاربران جدید از الگوریتم‌های ASP.NET Identity استفاده خواهند کرد.

کلاس UserManager خاصیتی با نام PasswordHasher دارد. این خاصیت نمونه ای از یک کلاس را ذخیره می‌کند، که اینترفیس IPasswordHasher را پیاده سازی کرده است. این کلاس هنگام تراکنش‌های احراز هویت کاربران استفاده می‌شود تا کلمه‌های عبور را رمزنگاری/رمزگشایی شوند. در کلاس UserManager کلاس جدیدی بنام SQLPasswordHasher بسازید. کد کامل را در لیست زیر مشاهده می‌کنید.

public class SQLPasswordHasher : PasswordHasher
{
        public override string HashPassword(string password)
        {
            return base.HashPassword(password);
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            string[] passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                string passwordHash = passwordProperties[0];
                int passwordformat = 1;
                string salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }

        //This is copied from the existing SQL providers and is provided only for back-compat.
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = HashAlgorithm.Create("SHA1");
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length; )
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
    }
}



دقت کنید تا فضاهای نام System.Text و System.Security.Cryptography را وارد کرده باشید.

متد EncodePassword کلمه عبور را بر اساس پیاده سازی پیش فرض SQL Membership رمزنگاری می‌کند. این الگوریتم از System.Web گرفته می‌شود. اگر اپلیکیشن قدیمی شما از الگوریتم خاصی استفاده می‌کرده است، همینجا باید آن را منعکس کنید. دو متد دیگر نیز بنام‌های HashPassword و VerifyHashedPassword نیاز داریم. این متدها از EncodePassword برای رمزنگاری کلمه‌های عبور و تایید آنها در دیتابیس استفاده می‌کنند.

سیستم SQL Membership برای رمزنگاری (Hash) کلمه‌های عبور هنگام ثبت نام و تغییر آنها توسط کاربران، از PasswordHash, PasswordSalt و PasswordFormat استفاده می‌کرد. در روند مهاجرت، این سه فیلد در ستون PasswordHash جدول AspNetUsers ذخیره شده و با کاراکتر '|' جدا شده اند. هنگام ورود کاربری به سایت، اگر کله عبور شامل این فیلدها باشد از الگوریتم SQL Membership برای بررسی آن استفاده می‌کنیم. در غیر اینصورت از پیاده سازی پیش فرض ASP.NET Identity استفاده خواهد شد. با این روش، کاربران قدیمی لازم نیست کلمه‌های عبور خود را صرفا بدلیل مهاجرت اپلیکیشن ما تغییر دهند.

کلاس UserManager را مانند قطعه کد زیر بروز رسانی کنید.

public UserManager()
            : base(new UserStore<User>(new ApplicationDbContext()))
        {
            this.PasswordHasher = new SQLPasswordHasher();
 }

ایجاد صفحات جدید مدیریت کاربران

قدم بعدی ایجاد صفحاتی است که به کاربران اجازه ثبت نام و ورود را می‌دهند. صفحات قدیمی SQL Membership از کنترل هایی استفاده می‌کنند که با ASP.NET Identity سازگار نیستند. برای ساختن این صفحات جدید به این مقاله مراجعه کنید. از آنجا که در این مقاله پروژه جدید را ساخته ایم و پکیج‌های لازم را هم نصب کرده ایم، می‌توانید مستقیما به قسمت Adding Web Forms for registering users to your application بروید.
چند تغییر که باید اعمال شوند:
  • فایل‌های Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده می‌کنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
  • همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
  • لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار می‌توانید پیش از ساختن حساب‌های کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
مثال: در فایل Register.aspx.cs متد جدیدی تعریف کنید که جدول aspnet_Applications را بررسی میکند و شناسه اپلیکیشن را بر اساس نام اپلیکیشن بدست می‌آورد.
private Guid GetApplicationID()
{
    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString))
            {
                string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database

                SqlCommand command = new SqlCommand(queryString, connection);
                command.Connection.Open();

                var reader = command.ExecuteReader();
                while (reader.Read())
                {
                    return reader.GetGuid(0);
                }

            return Guid.NewGuid();
        }
}


  حال می‌توانید این مقدار را برای آبجکت کاربر تنظیم کنید.
var currentApplicationId = GetApplicationID();

User user = new User() { UserName = Username.Text,
ApplicationId=currentApplicationId, …};
در این مرحله می‌توانید با استفاده از اطلاعات پیشین وارد سایت شوید، یا کاربران جدیدی ثبت کنید. همچنین اطمینان حاصل کنید که کاربران پیشین در نقش‌های مورد نظر وجود دارند.
مهاجرت به ASP.NET Identity مزایا و قابلیت‌های جدیدی را به شما ارائه می‌کند. مثلا کاربران می‌توانند با استفاده از تامین کنندگان ثالثی مثل Facebook, Google, Microsoft, Twitter و غیره به سایت وارد شوند. اگر به سورس کد این مقاله مراجعه کنید خواهید دید که امکانات OAuth نیز فعال شده اند.
در این مقاله انتقال داده‌های پروفایل کاربران بررسی نشد. اما با استفاده از نکات ذکر شده می‌توانید پروفایل کاربران را هم بسادگی منتقل کنید. کافی است مدل‌های لازم را در پروژه خود تعریف کرده و با استفاده از اسکریپت‌های SQL داده‌ها را انتقال دهید.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهارم - نصب و راه اندازی IdentityServer
معرفی IdentityServer 4


اگر استاندارد OpenID Connect را بررسی کنیم، از مجموعه‌ای از دستورات و رهنمودها تشکیل شده‌است. بنابراین نیاز به کامپوننتی داریم که این استاندارد را پیاده سازی کرده باشد تا بتوان بر اساس آن یک Identity Provider را تشکیل داد و پیاده سازی مباحثی که در قسمت قبل بررسی شدند مانند توکن‌ها، Flow، انواع کلاینت‌ها، انواع Endpoints و غیره چیزی نیستند که به سادگی قابل انجام باشند. اینجا است که IdentityServer 4، به عنوان یک فریم ورک پیاده سازی کننده‌ی استانداردهای OAuth 2 و OpenID Connect مخصوص ASP.NET Core ارائه شده‌است. این فریم ورک توسط OpenID Foundation تائید شده و داری مجوز رسمی از آن است. همچنین جزئی از NET Foundation. نیز می‌باشد. به علاوه باید دقت داشت که این فریم ورک کاملا سورس باز است.


نصب و راه اندازی IdentityServer 4

همان مثال «قسمت دوم - ایجاد ساختار اولیه‌ی مثال این سری» را در نظر بگیرید. داخل آن پوشه‌های جدید src\IDP\DNT.IDP را ایجاد می‌کنیم.


نام دلخواه DNT.IDP، به پوشه‌ی جدیدی اشاره می‌کند که قصد داریم IDP خود را در آن برپا کنیم. نام آن را نیز در ادامه‌ی نام‌های پروژه‌های قبلی که با ImageGallery شروع شده‌اند نیز انتخاب نکرده‌ایم؛ از این جهت که یک IDP را قرار است برای بیش از یک برنامه‌ی کلاینت مورد استفاده قرار دهیم. برای مثال می‌توانید از نام شرکت خود برای نامگذاری این IDP استفاده کنید.

اکنون از طریق خط فرمان به پوشه‌ی src\IDP\DNT.IDP وارد شده و دستور زیر را صادر کنید:
dotnet new web
این دستور، یک پروژه‌ی جدیدی را از نوع «ASP.NET Core Empty»، در این پوشه، بر اساس آخرین نگارش SDK نصب شده‌ی بر روی سیستم شما، ایجاد می‌کند. از این جهت نوع پروژه خالی درنظر گرفته شده‌است که قرار است توسط اجزای IdentityServer 4 به مرور تکمیل شود.

اولین کاری را که در اینجا انجام خواهیم داد، مراجعه به فایل Properties\launchSettings.json آن و تغییر شماره پورت‌های پیش‌فرض آن است تا با سایر پروژه‌های وبی که تاکنون ایجاد کرده‌ایم، تداخل نکند. برای مثال در اینجا شماره پورت SSL آن‌را به 6001 تغییر داده‌ایم.

اکنون نوبت به افزودن میان‌افزار IdentityServer 4 به پروژه‌ی خالی وب است. اینکار را نیز توسط اجرای دستور زیر در پوشه‌ی src\IDP\DNT.IDP انجام می‌دهیم:
 dotnet add package IdentityServer4

در ادامه نیاز است این میان‌افزار جدید را معرفی و تنظیم کرد. به همین جهت فایل Startup.cs پروژه‌ی خالی وب را گشوده و به صورت زیر تکمیل می‌کنیم:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
                .AddDeveloperSigningCredential();
        }
- متد الحاقی AddIdentityServer، کار ثبت و معرفی سرویس‌های توکار IdentityServer را به سرویس توکار تزریق وابستگی‌های ASP.NET Core انجام می‌دهد.
- متد الحاقی AddDeveloperSigningCredential کار تنظیمات کلید امضای دیجیتال توکن‌ها را انجام می‌دهد. در نگارش‌های قبلی IdentityServer، اینکار با معرفی یک مجوز امضاء کردن توکن‌ها انجام می‌شد. اما در این نگارش دیگر نیازی به آن نیست. در طول توسعه‌ی برنامه می‌توان از نگارش Developer این مجوز استفاده کرد. البته در حین توزیع برنامه به محیط ارائه‌ی نهایی، باید به یک مجوز واقعی تغییر پیدا کند.


تعریف کاربران، منابع و کلاینت‌ها

مرحله‌ی بعدی تنظیمات میان‌افزار IdentityServer4، تعریف کاربران، منابع و کلاینت‌های این IDP است. به همین جهت یک کلاس جدید را به نام Config، در ریشه‌ی پروژه ایجاد و به صورت زیر تکمیل می‌کنیم:
using System.Collections.Generic;
using System.Security.Claims;
using IdentityServer4.Models;
using IdentityServer4.Test;

namespace DNT.IDP
{
    public static class Config
    {
        // test users
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    SubjectId = "d860efca-22d9-47fd-8249-791ba61b07c7",
                    Username = "User 1",
                    Password = "password",

                    Claims = new List<Claim>
                    {
                        new Claim("given_name", "Vahid"),
                        new Claim("family_name", "N"),
                    }
                },
                new TestUser
                {
                    SubjectId = "b7539694-97e7-4dfe-84da-b4256e1ff5c7",
                    Username = "User 2",
                    Password = "password",

                    Claims = new List<Claim>
                    {
                        new Claim("given_name", "User 2"),
                        new Claim("family_name", "Test"),
                    }
                }
            };
        }

        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>();
        }
    }
}
توضیحات:
- این کلاس استاتیک، اطلاعاتی درون حافظه‌ای را برای تکمیل دموی جاری ارائه می‌دهد.

- ابتدا در متد GetUsers، تعدادی کاربر آزمایشی اضافه شده‌اند. کلاس TestUser در فضای نام IdentityServer4.Test قرار دارد. در کلاس TestUser، خاصیت SubjectId، بیانگر Id منحصربفرد هر کاربر در کل این IDP است. سپس نام کاربری، کلمه‌ی عبور و تعدادی Claim برای هر کاربر تعریف شده‌اند که بیانگر اطلاعاتی اضافی در مورد هر کدام از آن‌ها هستند. برای مثال نام و نام خانوادگی جزو خواص کلاس TestUser نیستند؛ اما منعی هم برای تعریف آن‌ها وجود ندارد. اینگونه اطلاعات اضافی را می‌توان توسط Claims به سیستم اضافه کرد.

- بازگشت Claims توسط یک IDP مرتبط است به مفهوم Scopes. برای این منظور متد دیگری به نام GetIdentityResources تعریف شده‌است تا لیستی از IdentityResource‌ها را بازگشت دهد که در فضای نام IdentityServer4.Models قرار دارد. هر IdentityResource، به یک Scope که سبب دسترسی به اطلاعات Identity کاربران می‌شود، نگاشت خواهد شد. در اینجا چون از پروتکل OpenID Connect استفاده می‌کنیم، ذکر IdentityResources.OpenId اجباری است. به این ترتیب مطمئن خواهیم شد که SubjectId به سمت برنامه‌ی کلاینت بازگشت داده می‌شود. برای بازگشت Claims نیز باید IdentityResources.Profile را به عنوان یک Scope دیگری مشخص کرد که در متد GetIdentityResources مشخص شده‌است.

- در آخر نیاز است کلاینت‌های این IDP را نیز مشخص کنیم (در مورد مفهوم Clients در قسمت قبل بیشتر توضیح داده شد) که اینکار در متد GetClients انجام می‌شود. فعلا یک لیست خالی را بازگشت می‌دهیم و آن‌را در قسمت‌های بعدی تکمیل خواهیم کرد.


افزودن کاربران، منابع و کلاینت‌ها به سیستم

پس از تعریف و تکمیل کلاس Config، برای معرفی آن به IDP، به کلاس آغازین برنامه مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddTestUsers(Config.GetUsers())
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryClients(Config.GetClients());
        }
در اینجا لیست کاربران و اطلاعات آن‌ها توسط متد AddTestUsers، لیست منابع و Scopes توسط متد AddInMemoryIdentityResources و لیست کلاینت‌ها توسط متد AddInMemoryClients به تنظیمات IdentityServer اضافه شده‌اند.


افزودن میان افزار IdentityServer به برنامه

پس از انجام تنظیمات مقدماتی سرویس‌های برنامه، اکنون نوبت به افزودن میان‌افزار IdentityServer است که در کلاس آغازین برنامه به صورت زیر تعریف می‌شود:
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIdentityServer();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }

آزمایش IDP

اکنون برای آزمایش IDP، به پوشه‌ی src\IDP\DNT.IDP وارد شده و دستور dotnet run را اجرا کنید:


همانطور که ملاحظه می‌کنید، برنامه‌ی IDP بر روی پورت 6001 قابل دسترسی است. برای آزمایش Web API آن، آدرس discovery endpoint این IDP را به صورت زیر در مرورگر وارد کنید:
 https://localhost:6001/.well-known/openid-configuration


در این تصویر، مفاهیمی را که در قسمت قبل بررسی کردیم مانند authorization_endpoint ،token_endpoint و غیره، مشاهده می‌کنید.


افزودن UI به IdentityServer

تا اینجا میان‌افزار IdentityServer را نصب و راه اندازی کردیم. در نگارش‌های قبلی آن، UI به صورت پیش‌فرض جزئی از این سیستم بود. در این نگارش آن‌را می‌توان به صورت جداگانه دریافت و به برنامه اضافه کرد. برای این منظور به آدرس IdentityServer4.Quickstart.UI مراجعه کرده و همانطور که در readme آن ذکر شده‌است می‌توان از یکی از دستورات زیر برای افزودن آن به پروژه‌ی IDP استفاده کرد:
الف) در ویندوز از طریق کنسول پاورشل به پوشه‌ی src\IDP\DNT.IDP وارد شده و سپس دستور زیر را وارد کنید:
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))
ب) و یا درmacOS و یا Linux، دستور زیر را اجرا کنید:
\curl -L https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.sh | bash

یک نکته: در ویندوز اگر در نوار آدرس هر پوشه، عبارت cmd را وارد و enter کنید، کنسول خط فرمان ویندوز در همان پوشه باز خواهد شد. همچنین در اینجا از ورود عبارت powershell هم پشتیبانی می‌شود:


بنابراین در نوار آدرس پوشه‌ی src\IDP\DNT.IDP، عبارت powershell را وارد کرده و سپس enter کنید. پس از آن دستور الف را وارد (copy/paste) و اجرا کنید.


به این ترتیب فایل‌های IdentityServer4.Quickstart.UI به پروژه‌ی IDP جاری اضافه می‌شوند.
- پس از آن اگر به پوشه‌ی Views مراجعه کنید، برای نمونه ذیل پوشه‌ی Account آن، Viewهای ورود و خروج به سیستم قابل مشاهده هستند.
- در پوشه‌ی Quickstart آن، کدهای کامل کنترلرهای متناظر با این Viewها قرار دارند.
بنابراین اگر نیاز به سفارشی سازی این Viewها را داشته باشید، کدهای کامل کنترلرها و Viewهای آن هم اکنون در پروژه‌ی IDP جاری در دسترس هستند.

نکته‌ی مهم: این UI اضافه شده، یک برنامه‌ی ASP.NET Core MVC است. به همین جهت در انتهای متد Configure، ذکر میان افزارهای UseStaticFiles و همچنین UseMvcWithDefaultRoute انجام شدند.

اکنون اگر برنامه‌ی IDP را مجددا با دستور dotnet run اجرا کنیم، تصویر زیر را می‌توان در ریشه‌ی سایت، مشاهده کرد که برای مثال لینک discovery endpoint در همان سطر اول آن ذکر شده‌است:


همچنین همانطور که در قسمت قبل نیز ذکر شد، یک IDP حتما باید از طریق پروتکل HTTPS در دسترس قرار گیرد که در پروژه‌های ASP.NET Core 2.1 این حالت، جزو تنظیمات پیش‌فرض است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
نظرات اشتراک‌ها
پروژه Help Desk در ابعاد کوچک
با سلام
برای Hosting این نرم افزار در سمت سرور و کلاینت این وب سایت‌های از Firebase و Heroku استفاده شده است و حساب کاربری رایگان هر دو وب سایت دارای یک سری محدودیت‌ها است و یکی از این محدودیت‌ها سمت وب سایت Heroku خاموش شدن وب سایت بعد از 30 دقیقه است اما اگر برای بار اول نتوانستید وارد شوید مجددا سعی نمایید.
همچنین نام کاربری و کلمه عبور خود را جهت تست و در صورت تمایل به ایمیل بنده ارسال نمایید. (دسترسی به کلمه‌های عبور برای بنده امکان پذیر نمی‌باشد)
مطالب
پیاده سازی یک سیستم دسترسی Role Based در Web API و AngularJs - بخش اول
در این مجموعه مقالات قصد دارم یک روش را برای پیاده سازی سیستم Role Based سفارشی شده، به صورت پروژه محور، در اختیار شما دوستان قرار دهم. این مجموعه مقالات در هر دو بخش سرور و کلاینت، در قالب یک پروژه‌ی واقعی ارائه خواهد شد. در سمت سرور از Web API و سیستم Identity 2.1 و در سوی کلاینت از تکنولوژی قدرتمند AngularJs کمک گرفته شده‌است. در این مجموعه قصد دارم تا مراحل کلی و اصلی این سیستم دسترسی را تشریح کنم.

مقدمه ای بر سیستم مبتنی بر نقش کاربران

شاید برای شما هم پیش آمده باشد که برای وب سایت یا وب اپلیکیشن خود، به دنبال یک سیستم دسترسی بوده‌اید که بتوانید در آن نقش‌های گوناگونی را تعریف کنید و هر نقش شامل یک سری دسترسی خاص باشد و همچنین با اضافه شدن هر کاربر به این سیستم، بتوانید آنها را به یک نقش خاص انتساب دهید. برای اجرای این امر، روش‌های گوناگونی وجود دارد. یکی از این روش‌ها در سایت CodeProject توسط آقای Stefan Wloch تشریح شده است. در این مقاله کلیت و هدف یک سیستم Role Based، ساختار نقش‌ها و کاربران و دسترسی‌های آنها به خوبی بیان شده است.
 
و اما کمی درباره روشهای موجود حول توسعه یک سیستم مبتنی بر نقش (Role Based) صحبت کنیم. در ابتدا لازم است یک تعریف کلی از یک سیستم Role Based داشته باشیم:
یک سیستم مدیریت کاربر مبتنی بر نقش، سیستمی است که در آن نقش‌های گوناگونی تعریف می‌گردد. به گونه‌ای که هر نقش شامل یک سری دسترسی هایی است که به کاربر اجازه می‌دهد تا به بخشی از سیستم دسترسی داشته باشد. هر کاربر در این سیستم می‌تواند یک و یا چند نقش متفاوت داشته باشد و بر اساس آن نقش‌ها و دسترسی‌های تعریف شده، درون هر نقش به قسمتی از سیستم دسترسی داشته باشد.
بر اساس این تعریف ما در نهایت سه موجودیت نقش (Role)، دسترسی (Permission) و کاربر (User) را خواهیم داشت. در این سیستم دسترسی را اینگونه تعریف میکنیم:
دسترسی در یک سیستم، مجموعه‌ای از حوزه‌ها و یا ناحیه‌هایی است که در سیستم تعریف می‌شود. این حوزه‌ها می‌توانند شامل یک متد یا مجموعه‌ای از متدهای نوشته شده و یا فراتر از آن، شامل مجموعه‌ای از کنترلرها باشد که کاربر اجازه‌ی فراخوانی آنها را دارد.

بدیهی است کاربر برای ما موجودیتی مشخص است. یک کاربر ویژگی‌هایی مانند نام، نام کاربری، سن، رمز عبور و ... خاص خود را داراست که به واسطه این ویژگی‌ها از کاربری دیگر تمییز داده میشود. کاربر در سیستم طی دو مرحله جداگانه Authentication و Authorization مجاز است تا از بخش‌هایی از سیستم استفاده کند. مرحله Authentication به طور خلاصه شامل مرحله‌ای است که هویت کاربر (به عنوان مثال نام کاربری و رمز عبور) تایید صلاحیت میشود. این مرحله در واقع تایید کننده کاربر است و اما بخش بعدی که ما قصد داریم تا در این مورد راهکاری را ارائه دهیم بخش Authorization است. در این بخش به کاربر بر اساس نقش وی دسترسی‌هایی اعطا می‌گردد و کاربر را به استفاده از بخش‌هایی از سیستم مجاز میدارد.

دیاگرام زیر نمود سه موجودیت کاربر، نقش و دسترسی میباشد.

برای شرح دیاگرام فوق این چنین میتوان گفت که هر کاربر مجاز است چندین نقش داشته باشد و هر نقش نیز میتواند به چندین کاربر انتساب شود. در مورد دسترسی‌ها نیز به همین صورت، هر دسترسی نیز میتواند به چندین نقش انتساب شود.

ارائه یک سیستم مبتنی بر نقش کاربران با استفاده از تکنولوژی Web API و AngularJs

حال که از محتوا و عملکرد یک سیستم مبتنی بر نقش (Role Based) دانش کافی را کسب کردیم، قصد داریم تا به پیاده سازی کامل این سیستم بپردازیم. شاید برای شما سوال باشد که چرا ما قصد داریم تا این معماری را در Web API و AngularJs پیاده سازی کنیم. در جواب به این سوال باید بگوییم که پیاده سازی این روشها در تکنولوژی‌هایی مانند ASP.NET MVC و تکنولوژی‌ةای مشابه آن که صفحه را در سمت سرور می‌سازند، در بسیاری از منابع و مقالات اینترنتی موجود است. ما قصد داریم تا آنچه را که در یک Single Page Application پیاده سازی کرده‌ایم، به اشتراک بگذاریم. بنابراین قصد داریم تا علاوه بر تشریح کامل این سیستم که قسمت اعظم آن در سمت سرور انجام میشود، به تشریح نحوه تبادل اطلاعات و مدیریت دسترسی‌ها در سمت کلاینت نیز بپردازیم.
همانطور که میدانید ما در یک سیستم بر پایه SPA، برای ساخت یک صفحه ممکن است چندین API را فراخوانی کنیم. بنابراین APIهای نوشته شده به تعداد زیاد ولی با عملکرد اتمیک خواهند بود. به عنوان مثال در یک سیستم فروشگاه اینترنتی، برای ویرایش یک محصول، ممکن است دو یا چند متد API (فراخوانی آن محصول برای قرار دادن در فیلدهای ویرایش، ارسال اطلاعات پس از ویرایش به سرور و ...) فراخوانی گردند. با این حساب ما در تعریف و ایجاد دسترسی‌ها دو راه را خواهیم داشت.
  1. تعریف هر یک از متدهای اتمیک به عنوان یک مجوز دسترسی: در این روش نام کنترلر و نام متد به عنوان یک دسترسی تعریف خواهد شد. در این روش به ازای هر متد ما یک آیتم جدید را باید به جدول Permissions، اضافه نماییم و در نهایت برای تعریف یک نقش و انتساب دسترسی‌ها به یک کاربر، بایستی یک مجموعه چک باکس را که در نهایت به یک متد API ختم میشود، فعال یا غیر فعال کنیم.
  2. تعریف ناحیه‌های مختلف و کنترل‌های قابل انجام در آن ناحیه: در این روش ما قصد داریم تا مجموعه‌ای از متد‌ها را که هدف مشترکی را انجام خواهند داد، در یک ناحیه و یک کنترل بگنجانیم و از به وجود آمدن تعداد زیادی مجوز دسترسی جلوگیری نماییم. به عنوان مثال فرض کنید میخواهیم یک سطح دسترسی را با نام ویرایش کاربران تعریف کنیم. همانگونه که گفتیم ممکن است برای یک عملیات، دو و یا چندین متد درون یک کنترلر تعریف شوند. حال ما این متد‌ها را درون یک ناحیه دسترسی قرار خواهیم داد و آن را در یک حوزه و یک کنترل (Area & Control) میگنجانیم.
در بخش بعدی سعی میکنیم تا این مبحث را بازتر کنیم و به صورت عملی مراحل توسعه این مدل را تشریح نماییم.
نظرات مطالب
بهینه سازی برنامه‌های وب ASP.NET برای موتورهای جستجو (SEO)
سلام.
هنگام جستجو در گوگل گاهی اوقات سایتی باز میشه که خود اون سایت هم کلمه کلیدی شما رو جستجو کرده و نتایج رو به شما نشون میده.
مثلا یک فروشگاه خوب هنگامی که در گوگل قیمت رم لپ تاپ رو جستجو کنید چنین صفحه ای داره.
به نظر شما دوستان چطور میشه چنین صفحه ای در سایت خود داشته باشیم؟
مطالب
نوشتن افزونه برای مرورگرها: قسمت اول : کروم
افزونه چیست؟
افزونه‌ها جزء مهمترین قسمت‌های یک مرورگر توسعه پذیر به شمار می‌آیند. افزونه‌ها سعی دارند تا قابلیت هایی را به مرورگر شما اضافه کنند. افزونه‌ها از آخرین فناوری‌های html,CSS و جاوااسکریپت تا به آنجایی که مرورگر آن‌ها را پشتیبانی کند، استفاده می‌کنند.
در این سری سعی خواهیم کرد برای هر مرورگر شناخته شده، یک افزونه ایجاد کنیم و ابتدا از آنجا که خودم از کروم استفاده می‌کنم، اولین افزونه را برای کروم خواهم نوشت.

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

فایل manifest.json
این فایل برای ذخیره سازی اطلاعاتی در مورد افزونه به کار می‌رود که شامل نام افزونه، توضیح کوتاه در مورد افزونه و ورژن و ... به کار می‌رود که همه این اطلاعات در قالب یا فرمت json نوشته می‌شوند و در بالاترین حد استفاده برای تعریف اهداف افزونه و اعطای مجوز به افزونه از آن استفاده می‌کنیم. این فایل بخش‌های زیر را در یک افزونه تعریف می‌کند که به مرور با آن آشنا می‌شویم.


کد زیر را در فایل manifest.json می‌نویسیم:
{
  "manifest_version": 2,

  "name": "Dotnettips Updater",
  "description": "This extension keeps you updated on current activities on dotnettips.info",
  "version": "1.0",
  "icons": { "16": "icon.png",
           "48": "icon.png",
          "128": "icon.png" },

  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "activeTab",
    "https://www.dntips.ir"
  ]
}
اطلاعات اولیه شامل نام و توضیح و ورژن افزونه است. ورژن برنامه برای به روزآوری افزونه بسیار مهم است. موقعی که ورژن جدیدی از افزونه ارائه شود، گوگل وب استور اعلان آپدیت جدیدی را برای افزونه میکند. آیکن قسمت‌های مختلف افزونه هم با icons مشخص می‌شود که در سه اندازه باید ارائه شوند و البته اگه اندازه آن نباشد scale می‌شود. قسمت بعدی تعریف UI برنامه هست که گوگل کروم، به آن Browser Action می‌گوید. در اینجا یک آیکن و همچنین یک صفحه اختصاصی برای تنظیمات افزونه معرفی می‌کنیم. این آیکن کنار نوار آدرس نمایش داده می‌شود و صفحه popup موقعی نشان داده می‌شود که کاربر روی آن کلیک می‌کند. آیکن‌ها برای browser action در دو اندازه 19 و 38 پیکسلی هستند و در صورتی که تنها یک آیکن تعریف شود، به صورت خودکار عمل scale و تغییر اندازه صورت می‌گیرد. برای تعیین عکس برای هر اندازه می‌توانید کد را به صورت زیر بنویسید:
"default_icon": {                    // optional
            "19": "images/icon19.png",           // optional
            "38": "images/icon38.png"            // optional
          }
قسمت popup برای نمایش تنظیمات به کار می‌رود و درست کردن این صفحه همانند صفحه همیشگی html هست و خروجی آن روی پنجره popup افزونه رندر خواهد شد.
گزینه default_title نیز یکی از دیگر خصیصه‌های مهم و پرکاربرد این قسمت هست که متن tooltip می‌باشد و موقعی که که کاربر، اشاره‌گر را روی آیکن ببرد نمایش داده می‌شود و در صورتی که نوشته نشود، کروم نام افزونه را نمایش می‌دهد؛ برای همین ما هم چیزی ننوشتیم.

صفحات پس‌زمینه
اگر بخواهید برای صفحه popup کد جاوااسکریت بنویسید یا از jquery استفاده کنید، مانند هر صفحه‌ی وبی که درست می‌کنید آن را کنار فایل popup قرار داده و در popup آنها را صدا کرده و از آن‌ها استفاده کنید. ولی برای پردازش هایی که نیاز به UI وجود ندارد، می‌توان از صفحات پس زمینه استفاده کرد. در این حالت ما دو نوع صفحه داریم:
  1. صفحات مصر یا Persistent Page
  2. صفحات رویدادگرا یا Events Pages
اولین نوع صفحه، همواره فعال و در حال اجراست و دومی موقعی فعال می‌شود که به استفاده از آن نیاز است. گوگل توصیه می‌کند که  تا جای ممکن از نوع دوم استفاده شود تا  مقدار حافظه مصرفی  حفظ شود  و کارآیی مروگر بهبود بخشیده شود. کد زیر یک صفحه پس زمینه را از نوع رویدادگرا می‌سازد. به وضوح روشن است در صورتی که خاصیت Persistent با true مقداردهی شود، این صفحه مصرانه در تمام وقت باز بودن مرورگر، فعال خواهد بود:
"background": {
    "scripts": ["background.js"],
    "persistent": false
}

Content Script یا اسکریپت محتوا
در صورتی که بخواهید با هر صفحه‌ای که باز یا رفرش می‌شود، به DOM آن دسترسی پیدا کنید، از این خصوصیت استفاده کنید. در کد زیر برای پردازش اطلاعات DOM از فایل جاوااسکریپت بهره برده و در قسمت matches می‌گویید که چه صفحاتی باید از این کد استفاده کنند که در اینجا از پروتکل‌های HTTP استفاده میشود و اگر مثلا نوع FTP یا file صدا زده شود کد مورد نظر اجرا نخواهد شد. در مورد اینکه matches چگونه کار می‌کند و چگونه می‌توان آن را نوشت، از این صفحه استفاده کنید.
"content_scripts": [
    {
        "matches": ["http://*/*", "https://*/*"],
        "js": ["content.js"]
    }
]

آغاز کدنویسی (رابط‌های کاربری)


اجازه دهید بقیه موارد را در حین کدنویسی تجربه کنیم و هر آنچه ماند را بعدا توضیح خواهیم داد. در اینجا من از یک صفحه با کد HTML زیر بهره برده ام که یک فرم دارد به همراه چهار چک باکس و در نهایت یک دکمه جهت ذخیره مقادیر. نام صفحه را popup.htm گذاشته ام و یک فایل popup.js هم دارم که در آن کد jquery نوشتم. قصد من این است که بتوان یک action browser به شکل زیر درست کنم:


کد html آن به شرح زیر است:
<html>
<head>
<meta charset="utf-8"/>

<script src="jquery.min.js"></script> <!-- Including jQuery -->
<script type="text/javascript" src="popup.js"></script>
</head>
<body style="direction:rtl;width:250px;">
<form >
<input type="checkbox" id="chkarticles" value="" checked="true">آخرین مطالب سایت</input><br/>
<input type="checkbox" id="chkarticlescomments" value="" >آخرین نظرات مطالب سایت</input><br/>
<input type="checkbox" id="chkshares" value="" >آخرین اشتراک‌های سایت</input><br/>
<input type="checkbox"  id="chksharescomments" value="" >آخرین نظرات اشتراک‌های سایت</input><br/>
<input id="btnsave" type="button" value="ذخیره تغییرات" />
    <div id="messageboard" style="color:green;"></div>
</form>

</body>
</html>
کد popup.js هم به شرح زیر است:
$(document).ready(function () {
    $("#btnsave").click(function() {
        var articles = $("#chkarticles").is(':checked');
        var articlesComments = $("#chkarticlescomments").is(':checked');
        var shares = $("#chkshares").is(':checked');
        var sharesComments = $("#chksharescomments").is(':checked');

        chrome.storage.local.set({ 'articles': articles, 'articlesComments': articlesComments, 'shares': shares, 'sharesComments': sharesComments }, function() {
            $("#messageboard").text( 'تنظیمات جدید اعمال شد');
        });
    });
});
در کد بالا موقعی که کاربر بر روی دکمه ذخیره، کلیک کند رویداد کلیک jquery فعال شده و مقادیر چک باکس‌ها را در متغیرهای مربوطه نگهداری می‌کند. نهایتا با استفاده از کلمه کلیدی کروم به ناحیه ذخیره سازی داده‌های کروم دست پیدا کرده و درخواست ذخیره مقادیر چک باکس را بر اساس ساختار نام و مقدار، ذخیره می‌کنیم و بعد از اعمال، توسط یک تابع callback به کاربر اعلام می‌کنیم که اطلاعات ذخیره شده است.
اولین مورد جدیدی که در بالا دیدیم، کلمه‌ی کلیدی chrome است. کروم برای توسعه دهندگانی که قصد نوشتن افزونه دارند api هایی را تدارک دیده است که میتوانید با استفاده از آنها به قسمت‌های مختلف مرورگر مثل بوک مارک یا تاریخچه فعالیت‌های مرورگر و ... دست پیدا کنید. البته برای اینکار باید در فایل manifest.json هم مجوز اینکار را درخواست نماییم. این ویژگی باید برای برنامه نویسان اندروید آشنا باشد. برای آشنایی هر چه بیشتر با مجوزها این صفحه را ببینید.
برای دریافت مجوز، کد زیر را به manifest اضافه می‌کنیم:
"permissions": [
    "storage"
  ]
مجوزی که در بالا درخواست کرده‌ایم مجوز دسترسی به ناحیه ذخیره سازی است. بعد از کلمه کلیدی chrome، کلمه‌ی local آمده است و می‌گوید که باید داده‌ها به صورت محلی و لوکال ذخیره شوند ولی اگر میخواهید داده‌ها در گوگل سینک شوند، باید به جای لوکال از کلمه کلیدی sync استفاده کنید یعنی:
chrome.storage.sync.set
فایل manifest نهایی:
{
  "manifest_version": 2,

  "name": "Dotnettips Updater",
  "description": "This extension keeps you updated on current activities on dotnettips.info",
  "version": "1.0",

  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "storage"
  ]
}
الان باید 4 فایل داشته باشید: فایل آیکن، popup.htm,popup.js و manifest.json. همه را داخل یک دایرکتوری قرار داده و در مروگر کروم به قسمت extensions بروید و گزینه Developer mode را فعال کنید تا یک تستی از کد نوشته شده بگیریم. گزینه Load Unpacked Extension را بزنید و آدرس دایرکتوری ایجاد شده را به آن بدهید.
chrome://extensions

الان باید مانند تصویر بالا یک آیکن کنار نوار آدرس یا به قول گوگل، Omni box ببینید. گزینه‌ها را تیک بزنید و روی دکمه ذخیره کلیک کنید. باید پیام مقادیر ذخیره شدند، نمایش پیدا کند. الان یک مشکل وجود دارد؛ داده‌ها ذخیره می‌شوند ولی موقعی که دوباره تنظیمات افزونه را باز کنید حالت اولیه نمایش داده میشود. پس باید تنظیمات ذخیره شده را خوانده و به آن‌ها اعمال کنیم. کد زیر را جهت دریافت مقادیر ذخیره شده می‌نویسیم. اینبار به جای استفاده از متد set از متد get استفاده می‌کنیم. به صورت آرایه، رشته نام مقادیر را درخواست می‌کنیم و در تابع callback، مقادیر به صورت آرایه برای ما برگشت داده می‌شوند.
    chrome.storage.local.get(['articles', 'articlesComments', 'shares', 'sharesComments'], function ( items) {
        console.log(items[0]);
        $("#chkarticles").attr("checked", items["articles"]);
        $("#chkarticlescomments").attr("checked", items["articlesComments"]);
        $("#chkshares").attr("checked", items["shares"]);
        $("#chksharescomments").attr("checked", items["sharesComments"]);

    });
حالا برای اینکه افزونه‌ی شما متوجه تغییرات شود، به تب extensions رفته و در لیست افزونه‌ها به دنبال افزونه خود بگردید و گزینه Reload را انتخاب نمایید تا افزونه تغییرات را متوجه شود و صفحه را تست کنید.

Page Action
روش دیگر برای ارائه یک رابط کاربری، page action هست. این روش دقیقا مانند روش قبلی است، ولی جای آیکن عوض می‌شود. قبلا بیرون از نوار آدرس بود، ولی الان داخل نوار آدرس قرار می‌گیرد. جالب‌ترین نکته در این مورد این است که این آیکن در ابتدا مخفی شده است و شما تصمیم می‌گیرید که این آیکن چه موقع نمایش داده شود. مثلا آیکن RSS تنها موقعی نمایش داده می‌شود که وب سایتی که باز شده است، دارای محتوای RSS باشد یا بوک مارک کردن یک آدرس برای همه‌ی سایت‌ها باز باشد و سایر موارد.
کد زیر نحوه‌ی تعریف یک page action را در manifest نشان می‌دهد. ما در این مثال یک page action را به طور موقت اضافه می‌کنیم و موقعی هم آن را نشان میدهیم که سایت dotnettips.info باز باشد. دلیل اینکه موقت اضافه می‌کنیم این است که باید یکی از دو گزینه رابط کاربری که تا به حال گفتیم، استفاده شود. در غیر این صورت کروم در هنگام خواندن فایل manifest در هنگام افزودن افزونه به مرورگر، پیام خطا خواهد داد و این مطلب را به شما گوشزد می‌کند. پس نمی‌توان دو گزینه را همزمان داشت و من میخواهم افزونه را در حالت browser action ارائه کنم. پس در پروژه نهایی، این مطلب page action نخواهد بود. برای داشتن یک page action کد زیر را در manifest بنویسید.
  "page_action": {
    "default_icon": {
        "19": "images/icon19.png",
        "38": "images/icon38.png"
    },
    "default_popup": "popup.html"
گزینه page action تعریف شد حالا باید کاری کنیم تا هر موقع صفحه‌ای باز می‌شود چک کند آیا سایت مورد نظر است یا خیر، اینکار را توسط صفحه‌ی پردازشی انجام می‌دهیم. پس تکه کد زیر را هم به manifest اضافه می‌کنیم:
"background": {
    "scripts": ["page_action_validator.js"]
}

تا اینجا فایل جاوااسکریپت معرفی شد که کد زیر را دارد و در پس زمینه شروع به اجرا می‌کند.
function UrlValidation(tabId, changeInfo, tab) {
if (tab.url.indexOf('dotnettips.info') >-1) {
chrome.pageAction.show(tabId);
}
};
chrome.tabs.onUpdated.addListener(UrlValidation);
چون از api در این کد بهره برده‌ایم و آن هم مدیریت بر روی تب هاست، پس باید مجوز آن هم گرفته شود. کلمه "tabs" را در قسمت permissions اضافه کنید.
یک listener برای tabها ایجاد کرده‌ایم که اگر تب جدید ایجاد شد، یا تب قبلی به آدرس جدیدی تغییر پیدا کرد تابع UrlValidation را اجرا کند و در این تابع چک می‌کنیم که اگر url این تب شامل نام وب سایت می‌شود، page action روی این تب ظاهر شود. پس از انجام تغییرات، مجددا افزونه را بارگذاری می‌کنیم و تغییرات اعمال شده را می‌بینیم. سایت dotnettips را باز کنید یا صفحه را مجددا رفرش کنید تا تغییر اعمال شده را ببینید.

تغییرات موقت را حذف و کدها را به حالت قبلی یعنی browser action بر میگردانم.

OmniBox
omnibox یک کلمه کلیدی است که در نوار آدرس مرورگر وارد می‌شود و در واقع می‌توانیم آن را نوع دیگری از رابط کاربری بنامیم. موقعی که شما کلمه کلیدی رزرو شده را وارد می‌کنید، در نوار آدرس کلماتی نشان داده میشود که کاربر میتواند یکی از آن‌ها را انتخاب کند تا عملی انجام شود. ما هم قرار است این کار را انجام دهیم. به این مثال دقت کنید:
میخواهیم موقعی که کاربرکلمه net. را تایپ می‌کند، 5 عبارت آخرین مطالب و آخرین اشتراک‌ها و آخرین نظرات مطالب و آخرین نظرات اشتراک‌ها و صفحه اصلی سایت نمایش داده شود و با انتخاب هر کدام، کاربر به سمت آن صفحه هدایت شود.
برای افزودن کلمه کلیدی در manifest خطوط زیر را اضافه کنید:
"omnibox": { "keyword" : ".net" }
با نوشتن خط بالا کلمه net. در مرورگر یک کلمه‌ی کلیدی به حساب خواهد آمد و موقعی که کاربر این کلمه را وارد کند، در سمت راست نوشته خواهد شد. در این حالت باید کلید تب را بزند تا به محیط دستوری آن برود.

در این حین می‌توانیم همزمان با تایپ کاربر، دستوراتی را به آن نشان بدهیم. من دوست دارم موقعی که کاربر حرفی را وارد کرد، لیستی از نام صفحات نوشته شود.

 
 برای اینکار باید کدنویسی کنیم ، پس یک فایل پس زمینه را به manifest معرفی کنید:
"background": {
    "scripts": ["omnibox.js"]
در فایل ominbox.js دستوراتی که مرتبط با omnibox است را می‌نویسیم و کد زیر را به آن اضافه می‌کنیم:
chrome.omnibox.onInputChanged.addListener(function(text, suggest) {
    suggest([
  {content: ".net tips Home Page", description: "صفحه اصلی"},
      {content: ".net tips Posts", description: "آخرین مطالب"},
      {content: ".net tips News", description: "آخرین نظرات مطالب"},
      {content: ".net tips Post Comments", description: "آخرین اشتراک ها"},
      {content: ".net tips News Comments", description: "آخرین نظرات اشتراک ها"}
    ]);
});
chrome.omnibox شامل 4 رویداد می‌شود:
 onInputStarted   بعد از اینکه کاربر کلمه کلیدی را وارد کرد اجرا می‌شود
onInputChanged 
  بعد از وارد کردن کلمه کلیدی هربار که کاربر تغییری در ورودی نوارد آدرس می‌دهد اجرا می‌شود.
 onInputEntered   کاربر ورودی خود را تایید می‌کند. مثلا بعد از وارد کردن، کلید enter را می‌فشارد
 onInputCancelled  کاربر از وارد کردن ورودی منصرف شده است؛ مثلا کلید ESC را فشرده است. 
با نوشتن chrome.omnibox.onInputChanged.addListener ما یک listener ساخته‌ایم تا هر بار کاربر ورودی را تغییر داد، یک تابع callback که دو آرگومان را دارد، صدا بزند. این آرگومان‌ها یکی متن ورودی‌است و دیگری آرایه‌ی suggest که شما با تغییر آرایه می‌توانید عباراتی که همزمان با تایپ به کاربر پیشنهاد می‌شود را نشان دهید. البته می‌توانید با تغییر کد کاری کنید تا بر اساس حروفی که تا به حال تایپ کرده‌اید، دستورات را نشان دهد؛ ولی من به دلیل اینکه 5 دستور بیشتر نبود و کاربر راحت باشد، چنین کاری نکردم. همچنین وقتی شما برای هر یک description تعریف کنید، به جای نام پیشنهادی، توضیح آن را نمایش می‌دهد.
حالا وقت این است که کد زیر را جهت اینکه اگر کاربر یکی از کلمات پیشنهادی را انتخاب کرد، به صفحه‌ی مورد نظر هدایت شود، اضافه کنیم:
chrome.omnibox.onInputEntered.addListener(function (text) {

var location="";
    switch(text)
{
case ".net tips Posts":
location="https://www.dntips.ir/postsarchive";
break;
case ".net tips News":
location="https://www.dntips.ir/newsarchive";
break;
case ".net tips Post Comments":
location="https://www.dntips.ir/commentsarchive";
break;
case".net tips News Comments":
location="https://www.dntips.ir/newsarchive/comments";
break;
default:
location="https://www.dntips.ir/";
}

    chrome.tabs.getSelected(null, function (tab) {
        chrome.tabs.update(tab.id, { url: location });
    });
});
ابتدا یک listener برای روی رویداد onInputEntered قرار داده تا وقتی کاربر عبارت وارد شده را تایید کرد، اجرا شود. در مرحله بعد چک می‌کنیم که عبارت وارد شده چیست و به ازای هر عبارت مشخص شده، آدرس آن صفحه را در متغیر location قرار می‌دهیم. در نهایت با استفاده از عبارت chrome.tabs.getSelected تب انتخابی را به یک تابع callback بر میگردانیم. اولین آرگومان windowId است، برای زمانی که چند پنجره کروم باز است که می‌توانید وارد نکنید تا پنجره فعلی و تب فعلی محسوب شود. برای همین نال رد کردیم. در تابع برگشتی، شیء tab شامل اطلاعات کاملی از آن تب مانند url و id و title می‌باشد و در نهایت با استفاده از دستور chrome.tabs.update اطلاعات تب را به روز می‌کنیم. آرگومان اول id تب را میدهیم تا بداند کدام تب باید تغییر کند و آرگومان بعدی می‌توانید هر یک از ویژگی‌های تب از قبیل آدرس فعلی یا عنوان آن و ... را تغییر دهید که ما آدرس آن را تغییر داده ایم.

Context Menu
یکی دیگر از رابط‌های کاربری، منوی کانتکست هست که توسط chrome.contextmenus ارائه می‌شود و به مجوز "contextmenus" نیاز دارد. فعال سازی منوی کانتکست در قسمت‌های زیر ممکن است:
all, page, frame, selection, link, editable, image, video, audio 
من گزینه‌ی dotenettips.info را برای باز کردن سایت، به Contextmenus اضافه می‌کنم. کد را در فایلی به اسم contextmenus.js ایجاد می‌کنم و در قسمت background آن را معرفی می‌کنم. برای باز کردن یک تب جدید برای سایت، نیاز به chrome.tabs داریم که البته  نیاز به مجوز tabs هم داریم.
محتوای فایل contextmenus.js
var root = chrome.contextMenus.create({
    title: 'Open .net tips',
    contexts: ['page']
}, function () {
    var Home= chrome.contextMenus.create({
        title: 'Home',
        contexts: ['page'],
        parentId: root,
        onclick: function (evt) {
            chrome.tabs.create({ url: 'https://www.dntips.ir' })
        }
    });
var Posts = chrome.contextMenus.create({
        title: 'Posts',
        contexts: ['page'],
        parentId: root,
        onclick: function (evt) {
            chrome.tabs.create({ url: 'https://www.dntips.ir/postsarchive/' })
        }
    });
});
در کد بالا یک گزینه به context menu اضافه میشود و دو زیر منو هم دارد که یکی صفحه‌ی اصلی سایت را باز میکند و دیگری هم صفحه‌ی مطالب سایت را باز می‌کند.

تا به اینجا ما قسمت ظاهری کار را آماده کرده ایم و به دلیل اینکه مطلب طولانی نشود، این مطلب را در دو قسمت ارائه خواهیم کرد. در قسمت بعدی نحوه خواندن RSS و اطلاع رسانی و دیگر موارد را بررسی خواهیم کرد.
مطالب
آشنایی با چالش های امنیتی در توسعه برنامه‌های تحت وب، بخش اول
در پروژه‌های بزرگ نرم افزاری، از قدیم بحث تامین امنیت پروژه، یکی از چالش‌های مهم بوده است. از دیدگاه شخصی بنده، یک مدیر نرم افزار یا حتی یک توسعه دهنده‌ی برنامه‌های تحت وب، لازم است علاوه بر صرف وقت مطالعاتی و آشنایی و تسلط بر مباحث طراحی معماری سیستم‌های تحت وب، که از اهمیت بالا و مقیاس بزرگی برخوردارند آشنایی لازم را با چالش‌های امنیتی در پیاده سازی اینگونه سیستم‌ها داشته باشد. امنیت در یک سیستم بزرگ و ارائه دهنده خدمات، باعث می‌شود تا کاربر علاوه بر یک تجربه کاربری (user experience) خوب از سیستم که حاصل پیاده سازی صحیح سیستم می‌باشد، اعتماد ویژه‌ای به سیستم مذکور داشته باشد. گاها کاربران به علت بی اعتمادی به شرایط امنیتی حاکم بر یک سیستم، از تجربه کاربری خوب یک سیستم چشم پوشی می‌کنند. اهمیت این مسئله تا جاییست که غول‌های تکنولوژی دنیا همچون Google درگیر این چالش می‌باشند و همیشه سعی بر تامین امنیت کاربران علاوه بر ایجاد تجربه کاربری خوب دارند. پس عدم توجه به این موضوع میتواند خسارات وارده جبران ناپذیری را به یک سیستم از جهت‌های مختلف وارد کند.

در این سری از مقالات، بنده سعی دارم تا حد توان در رابطه با چالش‌های امنیتی موجود در زمینه توسعه برنامه‌های تحت وب، مطالبی را منتشر کنم. از این رو امیدوارم تا این سری از مقالات برای دوستان مفید واقع گردد.

در این سری از مقالات چالش‌های امنیتی زیر مورد بحث و بررسی واقع خواهند گردید 

XSS , LDAPi ,RFI ,LFI ,SQLi ,RFD ,LFD ,SOF ,BSQLI ,DNN ,BOF ,CRLF ,CSRF ,SSI ,PCI ,SCD ,AFD ,RCE

در بخش اول از این سری مقالات ، به بررسی آسیب پذیری Cross-site scripting میپردازیم .

واژه XSS مخفف Cross-site scripting، نوعی از آسیب پذیریست که در برنامه‌های تحت وب نمود پیدا میکند. به طور کلی و خلاصه، این آسیب پذیری به فرد نفوذ کننده اجازه تزریق اسکریپت‌هایی را به صفحات وب، می‌دهد که در سمت کاربر اجرا می‌شوند ( Client Side scripts ) . در نهایت این اسکریپت‌ها توسط سایر افرادی که از صفحات مورد هدف قرار گرفته بازدید می‌کنند اجرا خواهد شد.

هدف از این نوع حمله :

بدست آوردن اطلاعات کوکی‌ها و سشن‌های کاربران ( مرتبط با آدرسی که صفحه آلوده شده در آن قرار دارد ) است. سپس فرد نفوذ کننده متناسب با اطلاعات بدست آمده می‌تواند به اکانت شخصی کاربران مورد هدف قرار گرفته، نفوذ کرده و از اطلاعات شخصی آن‌ها سوء استفاده کند .

به صورت کلی دو طبقه بندی برای انواع حملات Cross-site scripting وجود دارند.

حملات XSS ذخیره سازی شده ( Stored XSS Attacks ) :

در این نوع ، کدهای مخرب تزریق شده، در سرور سایت قربانی ذخیره میشوند. محل ذخیره سازی می‌تواند دیتابیس سایت یا هر جای دیگری که داده‌ها توسط سایت یا برنامه تحت وب بازیابی می‌شوند و نمایش داده می‌شوند باشد. اما اینکه چگونه کدهای مخرب در منابع یاد شده ذخیره میشوند؟

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

حملات XSS منعکس شده ( Reflected XSS Attacks ) :

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

در بسیاری از سایت‌ها، انجمن‌ها و سیستم‌های سازمانی تحت وب، مشاهده می‌شود که مثلا در بخش جستجو، یک فیلد برای وارد کردن عبارت جستجو وجود دارد. پس از وارد کردن عبارت جستجو و submit فرم، علاوه بر نمایش نتایج جستجو، عبارت جستجو شده نیز به نمایش گذاشته میشود و بعضا در بسیاری از سیستم‌ها این عبارت قبل از نمایش اعتبار سنجی نمی‌شود که آیا شامل کدهای مخرب می‌باشد یا خیر. همین امر سبب میشود تا اگر عبارت جستجو شامل کدهای مخرب باشد، آن‌ها به همراه نتیجه‌ی جستجو اجرا شوند.

اما این موضوع چگونه مورد سوء استفاده قرار خواهد گرفت؟ مگر نه اینکه این عبارت ذخیره نمیشود پس با توضیحات فوق، کد فقط بر روی سیستم مهاجم که کد جستجو را ایجاد می‌کند اجرا می‌شود، درست است؟ بله درست است ولی به نقطه ضعف زیر توجه کنید ؟

www.test.com/search?q=PHNjcmlwdD5hbGVydChkb2N1bWVudC5jb29raWUpOzwvc2NyaXB0Pg==

این آدرس حاصل submit  شدن فرم جستجو وب‌سایت test (نام وب‌سایت واقعی نیست و برای مثال است )  و ارجاع به صفحه نتایج جستجو میباشد. در واقع این لینک برای جستجوی یک کلمه یا عبارت توسط این وبسایت تولید شده و از هر کجا به این لینک مراجعه کنید عبارت مورد نظر مورد جستجو واقع خواهد شد. در واقع عبارت جستجو به صورت Base64 به عنوان یک query String به وبسایت ارسال می‌شود؛ علاوه بر نمایش نتایج، عبارت جستجو شده نیز به کاربر نشان داده شده و اگر آسیب پذیری مورد بحث وجود داشته باشد و عبارت شامل کدهای مخرب باشد، کدهای مخرب بر روی مرورگر فردی که این لینک را باز کرده اجرا خواهد شد!

در این صورت کافیست فرد مهاجم لینک مخرب را به هر شکلی به فرد مورد هدف بدهد ( مثلا ایمیل و ... ). حال در صورتیکه فرد لینک را باز کند (با توجه به اینکه لینک مربوط به یک سایت معروف است و عدم آگاهی کاربر از آسیب پذیری موجود در لینک، باعث باز کردن لینک توسط کاربر می‌شود)، کدها بر روی مرورگرش اجرا شده و کوکی‌های سایت مذکور برای مهاجم ارسال خواهد شد ... به این نوع حمله XSS   ، نوع انعکاسی می‌گویند که کاملا از توضیحات فوق الذکر، دلیل این نامگذاری مشخص می‌باشد.

اهمیت مقابله با این حمله :

برای نمونه این نوع باگ حتی تا سال گذشته در سرویس ایمیل یاهو وجود داشت. به شکلی که یکی از افراد انجمن hackforums به صورت Private این باگ را به عنوان Yahoo 0-Day XSS Exploit در محیط زیر زمینی و بازار سیاه هکرها به مبلغ چند صد هزار دلار به فروش می‌رساند. کاربران مورد هدف کافی بود تا فقط یک ایمیل دریافتی از هکر را باز کنند تا کوکی‌های سایت یاهو برای هکر ارسال شده و دسترسی ایمیل‌های فرد قربانی برای هکر فراهم شود ... ( در حال حاظر این باگ در یاهو وجو ندارد ).

چگونگی جلوگیری از این آسیب پذیری

در این سری از مقالات کدهای پیرامون سرفصل‌ها و مثال‌ها با ASP.net تحت فریم ورک MVC و به زبان C# خواهند بود. هر چند کلیات مقابله با آسیب پذیری هایی از این دست در تمامی زبان‌ها و تکنولوژی‌های تحت وب یکسان میباشند.

خوشبختانه کتابخانه‌ای قدرتمند برای مقابله با حمله مورد بحث وجود دارد با نام AntiXSS که میتوانید آخرین نسخه آن را با فرمان زیر از طریق nugget به پروژه خود اضافه کنید. البته ذکر این نکته حائز اهمیت است که Asp.net و فریم ورک MVC به صورت توکار تا حدودی از بروز این حملات جلوگیری می‌کند. برای مثال به این صورت که در View ‌ها شما تا زمانی که از MvcHtmlString استفاده نکنید تمامی محتوای مورد نظر برای نمایش به صورت Encode شده رندر می‌شوند. این داستان برای Url ‌ها هم که به صورت پیش فرض encode میشوند صدق می‌کند. ولی گاها وقتی شما برای ورود اطلاعات مثلا از یک ادیتور WYSWYG استفاده می‌کنید و نیاز دارید داده‌ها را بدون encoding رندر کنید. آنگاه به ناچار مجاب بر اعمال یک سری سیاست‌های خاص‌تر بر روی داده مورد نظر برای رندر می‌شوید و نمی‌توانید از encoding توکار فوق الذکر استفاده کنید. آنگاه این کتابخانه در اعمال سیاست‌های جلوگیری از بروز این آسیب پذیری می‌تواند برای شما مفید واقع شود.

 PM> Install-Package AntiXSS
این کتابخانه مجموعه‌ای از توابع کد کردن عبارات است که از مواردی همچون Html, XML, Url, Form, LDAP, CSS, JScript and VBScript پشتیبانی می‌کند. استفاده از آن بسیار ساده می‌باشد. کافیست ارجاعات لازم را به پروژه خود افزوده و به شکل زیر از توابع ارائه شده توسط این کتابخانه استفاده کنید: 
…
var reviewContent = model.UserReview;
reviewContent = Microsoft.Security.Application.Encoder.HtmlEncode(review);
…

امیدوارم در اولین بخش از این سری مقالات، به صورت خلاصه مطالب مهمی که باعث ایجاد فهم کلی در رابطه با حملات Xss وجود دارد، برای دوستان روشن شده و پیش زمینه فکری برای مقابله با این دست از حملات برایتان به وجود آمده باشد. 

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



- کلمه عبور و نام کاربری داده شده مربوط به زمانی است که سورس پروژه دریافتی رو در VS.NET اجرا کردید (حتما یکبار نظرات پیشین رو هم برای دریافت پیشنیازها مطالعه کنید) و قصد دارید مثلا به برنامه لاگین کنید.