در این مطلب نحوهی یکپارچه سازی 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()
در اکشن متد 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; }
private readonly IUsersService _usersService; public ExternalController( // ... IUsersService usersService) { // ... _usersService = usersService; }
الف) در متد FindUserFromExternalProvider
سطر قدیمی
var user = _users.FindByExternalProvider(provider, providerUserId);
var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
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)
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); } }
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); }
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); }
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"/>
@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); // ... } }
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد 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 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
- 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
در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.
ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف میکنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، میتوانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.
لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان میدهد.
- aspnet_Roles --> AspNetRoles
- aspnet_Users, aspnet_Membership --> AspNetUsers
- aspnet_UsersInRoles --> AspNetUserRoles
ساختن مدلها و صفحات عضویت
کلاس 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(); }
ایجاد صفحات جدید مدیریت کاربران
- فایلهای Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده میکنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
- همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
- لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار میتوانید پیش از ساختن حسابهای کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
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, …};
اگر استاندارد 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
اولین کاری را که در اینجا انجام خواهیم داد، مراجعه به فایل 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(); }
- متد الحاقی 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()); }
افزودن میان افزار 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'))
\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 در ابعاد کوچک
مقدمه ای بر سیستم مبتنی بر نقش کاربران
یک سیستم مدیریت کاربر مبتنی بر نقش، سیستمی است که در آن نقشهای گوناگونی تعریف میگردد. به گونهای که هر نقش شامل یک سری دسترسی هایی است که به کاربر اجازه میدهد تا به بخشی از سیستم دسترسی داشته باشد. هر کاربر در این سیستم میتواند یک و یا چند نقش متفاوت داشته باشد و بر اساس آن نقشها و دسترسیهای تعریف شده، درون هر نقش به قسمتی از سیستم دسترسی داشته باشد.بر اساس این تعریف ما در نهایت سه موجودیت نقش (Role)، دسترسی (Permission) و کاربر (User) را خواهیم داشت. در این سیستم دسترسی را اینگونه تعریف میکنیم:
دسترسی در یک سیستم، مجموعهای از حوزهها و یا ناحیههایی است که در سیستم تعریف میشود. این حوزهها میتوانند شامل یک متد یا مجموعهای از متدهای نوشته شده و یا فراتر از آن، شامل مجموعهای از کنترلرها باشد که کاربر اجازهی فراخوانی آنها را دارد.
بدیهی است کاربر برای ما موجودیتی مشخص است. یک کاربر ویژگیهایی مانند نام، نام کاربری، سن، رمز عبور و ... خاص خود را داراست که به واسطه این ویژگیها از کاربری دیگر تمییز داده میشود. کاربر در سیستم طی دو مرحله جداگانه Authentication و Authorization مجاز است تا از بخشهایی از سیستم استفاده کند. مرحله Authentication به طور خلاصه شامل مرحلهای است که هویت کاربر (به عنوان مثال نام کاربری و رمز عبور) تایید صلاحیت میشود. این مرحله در واقع تایید کننده کاربر است و اما بخش بعدی که ما قصد داریم تا در این مورد راهکاری را ارائه دهیم بخش Authorization است. در این بخش به کاربر بر اساس نقش وی دسترسیهایی اعطا میگردد و کاربر را به استفاده از بخشهایی از سیستم مجاز میدارد.
دیاگرام زیر نمود سه موجودیت کاربر، نقش و دسترسی میباشد.
برای شرح دیاگرام فوق این چنین میتوان گفت که هر کاربر مجاز است چندین نقش داشته باشد و هر نقش نیز میتواند به چندین کاربر انتساب شود. در مورد دسترسیها نیز به همین صورت، هر دسترسی نیز میتواند به چندین نقش انتساب شود.
ارائه یک سیستم مبتنی بر نقش کاربران با استفاده از تکنولوژی Web API و AngularJs
- تعریف هر یک از متدهای اتمیک به عنوان یک مجوز دسترسی: در این روش نام کنترلر و نام متد به عنوان یک دسترسی تعریف خواهد شد. در این روش به ازای هر متد ما یک آیتم جدید را باید به جدول Permissions، اضافه نماییم و در نهایت برای تعریف یک نقش و انتساب دسترسیها به یک کاربر، بایستی یک مجموعه چک باکس را که در نهایت به یک متد API ختم میشود، فعال یا غیر فعال کنیم.
- تعریف ناحیههای مختلف و کنترلهای قابل انجام در آن ناحیه: در این روش ما قصد داریم تا مجموعهای از متدها را که هدف مشترکی را انجام خواهند داد، در یک ناحیه و یک کنترل بگنجانیم و از به وجود آمدن تعداد زیادی مجوز دسترسی جلوگیری نماییم. به عنوان مثال فرض کنید میخواهیم یک سطح دسترسی را با نام ویرایش کاربران تعریف کنیم. همانگونه که گفتیم ممکن است برای یک عملیات، دو و یا چندین متد درون یک کنترلر تعریف شوند. حال ما این متدها را درون یک ناحیه دسترسی قرار خواهیم داد و آن را در یک حوزه و یک کنترل (Area & Control) میگنجانیم.
{ "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" ] }
"default_icon": { // optional "19": "images/icon19.png", // optional "38": "images/icon38.png" // optional }
صفحات پسزمینه
- صفحات مصر یا Persistent Page
- صفحات رویدادگرا یا Events Pages
"background": { "scripts": ["background.js"], "persistent": false }
Content Script یا اسکریپت محتوا
"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>
$(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( 'تنظیمات جدید اعمال شد'); }); }); });
"permissions": [ "storage" ]
chrome.storage.sync.set
{ "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" ] }
chrome://extensions
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"]); });
"page_action": { "default_icon": { "19": "images/icon19.png", "38": "images/icon38.png" }, "default_popup": "popup.html"
"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);
"omnibox": { "keyword" : ".net" }
"background": { "scripts": ["omnibox.js"]
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: "آخرین نظرات اشتراک ها"} ]); });
onInputStarted | بعد از اینکه کاربر کلمه کلیدی را وارد کرد اجرا میشود |
onInputChanged | بعد از وارد کردن کلمه کلیدی هربار که کاربر تغییری در ورودی نوارد آدرس میدهد اجرا میشود. |
onInputEntered | کاربر ورودی خود را تایید میکند. مثلا بعد از وارد کردن، کلید enter را میفشارد |
onInputCancelled | کاربر از وارد کردن ورودی منصرف شده است؛ مثلا کلید ESC را فشرده است. |
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 }); }); });
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/' }) } }); });
واژه 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
… var reviewContent = model.UserReview; reviewContent = Microsoft.Security.Application.Encoder.HtmlEncode(review); …
امیدوارم در اولین بخش از این سری مقالات، به صورت خلاصه مطالب مهمی که باعث ایجاد فهم کلی در رابطه با حملات Xss وجود دارد، برای دوستان روشن شده و پیش زمینه فکری برای مقابله با این دست از حملات برایتان به وجود آمده باشد.
مشکل دریافتن محل دریافت فایلها
- کلمه عبور و نام کاربری داده شده مربوط به زمانی است که سورس پروژه دریافتی رو در VS.NET اجرا کردید (حتما یکبار نظرات پیشین رو هم برای دریافت پیشنیازها مطالعه کنید) و قصد دارید مثلا به برنامه لاگین کنید.