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

پرسش پاسخ
برای ایجاد یک وبلاگ بلاگری از کجا شروع کنم؟ با داشتن یک اکانت جی میل، به آدرس زیر مراجعه کرده و هر تعداد که مایل بودید می‌توانید وبلاگ ایجاد نمائید:
قالب‌های پیش فرض گوگل چنگی به دل نمی‌زنند و بود و نبود آن‌ها احساس نمی‌شود! چه باید کرد؟ برای تهیه قالب‌های فارسی به آدرس زیر مراجعه نمائید:
تاریخ فارسی را چه کنم؟ روش دوستان ، روش من
برنامه نویسم! کدهای من یا نمایش داده نمی‌شود یا فرمت آن‌ها هنگام ارسال به هم می‌ریزد. چه باید کرد؟ روش مورد علاقه‌ی من ، روش مایکروسافت (+ و + و افزونه‌ای برای این‌کار)
آخرین نظرات کاربران سایت را چگونه در ستون کناری سایت نمایش دهم؟ یکی از ویجت‌های پرکاربرد گوگل در بلاگر، ویجت فید است. هر بلاگ دارای دو فید مطالب و نظرات است. ویجت فید را اضافه کرده و سپس آدرس فید نظرات سایت خود را به آن معرفی کنید. همیشه آخرین 5 نظر ارسالی را نمایش می‌دهد. این ویجت کاربردهای قابل توجهی می‌تواند داشته باشد.
آمار مراجعان به سایت را باید در کجا ملاحظه کرد؟ جای پیش فرضی وجود ندارد! یک اکانت Analytics برای خود تهیه کرده و آن‌را به سایت اضافه کنید. همچنین پس از آن یک اکانت فید برنر را نیز برای خود تهیه کنید. فیدبرنر اکنون جزیی از گوگل است و پس از معرفی آدرس فید سایت خود به آن، یک آدرس جدید به همراه آیکونی برای نمایش در سایت به شما می‌دهد که می‌توانید تگ‌های آن تصویر ‌را توسط ویجت html نمایش دهید. همچنین از همین طریق نیز می‌توان اشتراک از طریق ایمیل را به وبلاگ اضافه کرد.
کامنت‌های سایت من در بیرون از سایت باز می‌شوند. چرا؟ به این مطلب مراجعه نمائید:
چگونه برای بلاگ خود قسمت ارسال نظرات را تهیه نمایم؟ من از سرویس فرم ساز Zoho‌ استفاده می‌کنم که در سال 2008 جزو برترین‌های وب شناخته شده بود. بعد از ایجاد فرم خود و دریافت کد مربوطه، یک مطلب جدید را ارسال کنید و در بدنه‌ی آن ، کدی را که Zoho به شما ارائه می‌دهد قرار دهید. سپس لینک این مطلب را توسط یک ویجت html به کنار سایت اضافه کنید تا همیشه در دسترس باشد.

مطالب
دادن «حق فراموش شدن» به کاربران در ASP.NET Core Identity 2.1
برنامه‌هایی که بخواهند سازگار با GDPR باشند، باید اصل 17 ام آن را که حق فراموش شدن است «Right to erasure ('right to be forgotten') »، پیاده سازی کنند؛ به علاوه اشخاص باید بتوانند اطلاعات شخصی خودشان را نیز بدون درنگ از آن سایت دریافت کنند. به عبارتی اگر شخصی در سایت شما ثبت نام کرد، باید قسمتی را هم در آن درنظر بگیرید که کاربر با فشردن یک کلیک، بتواند اکانت فعلی خودش را برای همیشه محو و نابود کند؛ بدون اینکه اثری از آن باقی بماند. همچنین پیش از این عمل نیز باید بتواند با کلیک بر روی یک دکمه، کلیه اطلاعات شخصی خودش را دانلود و ذخیره کند. از زمان ASP.NET Core Identity 2.1، هر دو مورد جزئی از این فریم ورک شده‌اند:



روش پیاده سازی دریافت اطلاعات شخصی در ASP.NET Core Identity 2.1

اگر به IdentityUser نگارش‌های اخیر ASP.NET Core Identity دقت کنید، شامل ویژگی جدید PersonalData نیز هست:
public class IdentityUser<TKey> where TKey : IEquatable<TKey>
{
   [PersonalData]
   public virtual TKey Id { get; set; }
PersonalData یک نشانه‌گذار است و هدف از آن، مشخص کردن خواصی است که شخص می‌تواند درخواست دریافت اطلاعات مرتبط با آن‌ها را بدهد.
سپس زمانیکه در تصویر فوق بر روی دکمه‌ی download کلیک کرد، ابتدا توسط userManager.GetUserAsync، اطلاعات کاربر جاری از بانک اطلاعاتی دریافت شده و سپس حلقه‌ای بر روی تمام خواص شیء IdentityUser تشکیل می‌شود. در اینجا هر کدام که مزین به PersonalDataAttribute بود، مقدارش دریافت شده و به دیکشنری personalData اضافه می‌شود.
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(TUser).GetProperties().Where(
    prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
    personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
در آخر این اطلاعات با فرمت JSON، جهت دریافت به کاربر ارائه خواهد شد:
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");


روش پیاده سازی دادن «حق فراموش شدن» به کاربران

اگر به تصویر ارسال شده دقت کنید، یک دکمه‌ی Delete نیز در آن قرار دارد. کار آن، حذف واقعی و فیزیکی کاربر جاری و تمام اطلاعات وابسته‌ی به او از بانک اطلاعاتی است. این دکمه نیز بدون هیچ واسطی در اختیار خود کاربر قرار گرفته‌است. یعنی به این صورت نیست که ابتدا فرمی را پر کند و سپس شخص دیگری اکانت او را حذف کند.
روش پیاده سازی آن نیز به صورت زیر است:
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
    return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

RequirePassword = await _userManager.HasPasswordAsync(user);
if (RequirePassword)
{
    if (!await _userManager.CheckPasswordAsync(user, Input.Password))
    {
        ModelState.AddModelError(string.Empty, "Password not correct.");
        return Page();
    }
}
برای اینکه کاربر بتواند درخواست حذف اکانت خود را صادر کند، باید ابتدا کلمه‌ی عبور خود را نیز وارد کند. همانطور که مشاهده می‌کنید توسط متد CheckPasswordAsync سرویس userManager، کار صحت کلمه‌ی عبور درخواستی بررسی می‌شود. سپس سطر زیر، اکانت او را حذف می‌کند:
var result = await _userManager.DeleteAsync(user);
موجودیت کاربر با سایر موجودیت‌های دیگری که در ارتباط است، به صورت OnDelete(DeleteBehavior.Cascade) تنظیم شده‌است. یعنی اگر این کاربر حذف شود، تمام اطلاعات او در سایر جداول وابسته نیز به صورت خودکار حذف خواهند شد.
و در آخر، کوکی جاری شخص لاگین شده را نیز حذف می‌کند تا برای همیشه با او خداحافظی کند!
await _signInManager.SignOutAsync();

 

پ.ن.
ما در این سایت هیچگاه شما را فراموش نخواهیم کرد!
مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- پیشنیار بحث «معرفی JSON Web Token»

پیاده سازی‌های زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو می‌توانید پیدا کنید. اما مشکلی که تمام آن‌ها دارند، شامل این موارد هستند:
- چون توکن‌های JWT، خودشمول هستند (در پیشنیاز بحث مطرح شده‌است)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی می‌تواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمه‌ی عبور او تغییر کند و یا سطح دسترسی‌های او کاهش یابند ... مهم نیست! باز هم می‌تواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بی‌معنا است. یعنی اگر برنامه‌ای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکن‌ها جایی ذخیره نمی‌شوند، عملا این logout بی‌مفهوم است و مجددا می‌توان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشده‌است که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکان‌های مختلفی می‌توان دسترسی لازم را جهت استفاده‌ی از قسمت‌های محافظت شده‌ی برنامه یافت (در صورت دسترسی، چندین نفر می‌توانند از آن استفاده کنند).

به همین جهت راه حلی عمومی برای ذخیره سازی توکن‌های صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که می‌تواند به عنوان پایه مباحث Authentication و Authorization برنامه‌های تک صفحه‌ای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شده‌است (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمی‌کند. سرویس‌های آن برای بکارگیری انواع و اقسام روش‌های ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.


نگاهی به برنامه


در اینجا تمام قابلیت‌های این پروژه را مشاهده می‌کنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژه‌ای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیه‌ی لاگین، برطرف می‌کند.
- پیاده سازی logout


بسته‌های پیشنیاز برنامه

پروژه‌ای که در اینجا بررسی شده‌است، یک پروژه‌ی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بسته‌ی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.Jwt
PM> Install-Package structuremap
PM> Install-Package structuremap.web
بسته‌ی Microsoft.Owin.Host.SystemWeb سبب می‌شود تا کلاس OwinStartup به صورت خودکار شناسایی و بارگذاری شود. این کلاسی است که کار تنظیمات اولیه‌ی JSON Web token را انجام می‌دهد و بسته‌ی Microsoft.Owin.Security.Jwt شامل تمام امکاناتی است که برای راه اندازی توکن‌های JWT نیاز داریم.
از structuremap هم برای تنظیمات تزریق وابستگی‌های برنامه استفاده شده‌است. به این صورت قسمت تنظیمات اولیه‌ی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویس‌های برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.


دریافت کدهای کامل برنامه

کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید. در ادامه صرفا قسمت‌های مهم این کدها را بررسی خواهیم کرد.


بررسی کلاس AppJwtConfiguration

کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکن‌های برنامه در فایل web.config، ایجاد شده‌است. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration
    tokenPath="/login"
    expirationMinutes="2"
    refreshTokenExpirationMinutes="60"
    jwtKey="This is my shared key, not so secret, secret!"
    jwtIssuer="http://localhost/"
    jwtAudience="Any" />
این قسمت جدید بر اساس configSection ذیل که به کلاس AppJwtConfiguration اشاره می‌کند، قابل استفاده شده‌است (بنابراین اگر فضای نام این کلاس را تغییر دادید، باید این قسمت را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود):
 <configSections>
    <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" />
</configSections>
- در اینجا tokenPath، یک مسیر دلخواه است. برای مثال در اینجا به مسیر login تنظیم شده‌است. برنامه‌ای که با Microsoft.Owin.Security.Jwt کار می‌کند، نیازی ندارد تا یک قسمت لاگین مجزا داشته باشد (مثلا یک کنترلر User و بعد یک اکشن متد اختصاصی Login). کار لاگین، در متد GrantResourceOwnerCredentials کلاس AppOAuthProvider انجام می‌شود. اینجا است که نام کاربری و کلمه‌ی عبور کاربری که به سمت سرور ارسال می‌شوند، توسط Owin دریافت و در اختیار ما قرار می‌گیرند.
- در این تنظیمات، دو زمان منقضی شدن را مشاهده می‌کنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد این‌ها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکن‌ها در سمت سرور استفاده می‌شود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.

یک نکته
جهت سهولت کار تزریق وابستگی‌ها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شده‌است و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفاده‌ی مستقیم از کلاس AppJwtConfiguration استفاده شده‌است.


بررسی کلاس OwinStartup

شروع به کار تنظیمات JWT و ورود آن‌ها به چرخه‌ی حیات Owin از کلاس OwinStartup آغاز می‌شود. در اینجا علت استفاده‌ی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگی‌های لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوه‌ی تهیه‌ی access token و همچنین refresh token ذکر می‌شوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفاده‌ی جهت امضای توکن‌های صادر شده، ذکر می‌شوند.


حداقل‌های بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکن‌های آن‌ها

همانطور که در ابتدای بحث عنوان شد، می‌خواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار می‌کند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافه‌تری به نام SerialNumber نیز در اینجا درنظر گرفته شده‌است. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمه‌ی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).

ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکن‌های صادر شده صورت می‌گیرد. توکن‌های صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آن‌ها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال می‌کند، ابتدا token او را دریافت کرده و سپس بررسی می‌کنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شده‌است یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی می‌کنیم که تمام توکن‌های یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفاده‌ی مجدد را نخواهد داشت.


مدیریت بانک اطلاعاتی و کلاس‌های سرویس برنامه

در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService
{
    void CreateUserToken(UserToken userToken);
    bool IsValidToken(string accessToken, int userId);
    void DeleteExpiredTokens();
    UserToken FindToken(string refreshTokenIdHash);
    void DeleteToken(string refreshTokenIdHash);
    void InvalidateUserTokens(int userId);
    void UpdateUserToken(int userId, string accessTokenHash);
}
کار سرویس TokenStore، ذخیره سازی و تعیین اعتبار توکن‌های صادر شده‌است. در اینجا ثبت یک توکن، بررسی صحت وجود یک توکن رسیده، حذف توکن‌های منقضی شده، یافتن یک توکن بر اساس هش توکن، حذف یک توکن بر اساس هش توکن، غیرمعتبر کردن و حذف تمام توکن‌های یک شخص و به روز رسانی توکن یک کاربر، پیش بینی شده‌اند.
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.

یک نکته:
در سرویس ذخیره سازی توکن‌ها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken)
{
   InvalidateUserTokens(userToken.OwnerUserId);
   _tokens.Add(userToken);
}
استفاده از InvalidateUserTokens در اینجا سبب خواهد شد با لاگین شخص و یا صدور توکن جدیدی برای او، تمام توکن‌های قبلی او حذف شوند. به این ترتیب امکان لاگین چندباره و یا یافتن دسترسی به منابع محافظت شده‌ی برنامه در سرور توسط چندین نفر (که به توکن شخص دسترسی یافته‌اند یا حتی تقاضای توکن جدیدی کرده‌اند)، میسر نباشد. همینکه توکن جدیدی صادر شود، تمام لاگین‌های دیگر این شخص غیرمعتبر خواهند شد.


ب) سرویس UsersService
public interface IUsersService
{
    string GetSerialNumber(int userId);
    IEnumerable<string> GetUserRoles(int userId);
    User FindUser(string username, string password);
    User FindUser(int userId);
    void UpdateUserLastActivityDate(int userId);
}
از کلاس سرویس کاربران، برای یافتن شماره سریال یک کاربر استفاده می‌شود. در مورد شماره سریال پیشتر بحث کردیم و هدف آن مشخص سازی وضعیت تغییر این موجودیت است. اگر کاربری اطلاعاتش تغییر کرد، این فیلد باید با یک Guid جدید مقدار دهی شود.
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمه‌ی عبور او (جهت مدیریت مرحله‌ی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شده‌اند.

ج) سرویس SecurityService
public interface ISecurityService
{
   string GetSha256Hash(string input);
}
در قسمت‌های مختلف این برنامه، هش SHA256 مورد استفاده قرار گرفته‌است که با توجه به مباحث تزریق وابستگی‌ها، کاملا قابل تعویض بوده و برنامه صرفا با اینترفیس آن کار می‌کند.


پیاده سازی قسمت لاگین و صدور access token

در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شده‌است. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی می‌شود. قسمت‌های مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر می‌توانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع می‌شود. چون با یک برنامه‌ی وب در حال کار هستیم، ClientId آن‌را نال درنظر می‌گیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت می‌تواند ClientId ویژه‌ای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهم‌ترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی می‌شود. اگر به کدهای آن دقت کنید،  خود owin دارای خاصیت‌های user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت می‌کند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login می‌توانید مشاهده کنید:
function doLogin() {
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            username: "Vahid",
            password: "1234",
            grant_type: "password"
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
url آن به همان مسیری که در فایل web.config تنظیم کردیم، اشاره می‌کند. فرمت data ایی که به سرور ارسال می‌شود، دقیقا باید به همین نحو باشد و content type آن نیز مهم است و owin فقط حالت form-urlencoded را پردازش می‌کند. به این ترتیب است که user name و password توسط owin قابل شناسایی شده و grant_type آن است که سبب فراخوانی GrantResourceOwnerCredentials می‌شود و مقدار آن نیز دقیقا باید password باشد (در حین لاگین).
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمه‌ی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقش‌های او نیز به عنوان Claim جدید به توکن اضافه می‌شوند.

در اینجا یک Claim سفارشی هم اضافه شده‌است:
 identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
کار آن ذخیره سازی userId کاربر، در توکن صادر شده‌است. به این صورت هربار که توکن به سمت سرور ارسال می‌شود، نیازی نیست تا یکبار از بانک اطلاعاتی بر اساس نام او، کوئری گرفت و سپس id او را یافت. این id در توکن امضاء شده، موجود است. نمونه‌ی نحوه‌ی کار با آن‌را در کنترلرهای این API می‌توانید مشاهده کنید. برای مثال در MyProtectedAdminApiController این اطلاعات از توکن استخراج شده‌اند (جهت نمایش مفهوم).

در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شده‌ی برای کاربر، استفاده کرده‌ایم. هش این access token را در بانک اطلاعاتی ذخیره می‌کنیم (جستجوی هش‌ها سریعتر هستند از جستجوی یک رشته‌ی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هش‌ها برای مهاجم قابل استفاده نیست).

اگر بخواهیم اطلاعات ارسالی به کاربر را پس از لاگین، نمایش دهیم، به شکل زیر خواهیم رسید:


در اینجا access_token همان JSON Web Token صادر شده‌است که برنامه‌ی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.

بنابراین خلاصه‌ی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد  ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام می‌شود. برای مثال نقش‌های او به توکن صادر شده اضافه می‌شوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام می‌دهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژه‌ای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال می‌شود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی قسمت صدور Refresh token

در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده می‌کنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمه‌ی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر می‌گیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحه‌ی لاگین هدایت کنیم. می‌توانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امن‌تر است چون نیازی به ارسال کلمه‌ی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شده‌ی در زمان لاگین، به صورت زیر عمل می‌شود:
function doRefreshToken() {
    // obtaining new tokens using the refresh_token should happen only if the id_token has expired.
    // it is a bad practice to call the endpoint to get a new token every time you do an API call.
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            grant_type: "refresh_token",
            refresh_token: refreshToken
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
در اینجا نحوه‌ی عملکرد، همانند متد login است. با این تفاوت که grant_type آن اینبار بجای password مساوی refresh_token است و مقداری که به عنوان refresh_token به سمت سرور ارسال می‌کند، همان مقداری است که در عملیات لاگین از سمت سرور دریافت کرده‌است. آدرس ارسال این درخواست نیز همان tokenPath تنظیم شده‌ی در فایل web.config است. بنابراین مراحلی که در اینجا طی خواهند شد، به ترتیب زیر است:
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی می‌شود. اکنون در متد ReceiveAsync این refresh token ذخیره شده‌ی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize می‌کنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز می‌شود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافه‌تری وجود داشت، می‌توان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام می‌دهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync  بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع می‌شود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیره‌ی هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute

در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف می‌کنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی می‌شود که آیا درخواست رسیده‌ی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان می‌رسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی می‌کنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشده‌است.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آن‌را با نمونه‌ی موجود در دیتابیس مقایسه می‌کنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع می‌شود.
- همچنین در آخر بررسی می‌کنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشده‌است.

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

و نکته‌ی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
 [JwtAuthorize(Roles = "Admin")]
public class MyProtectedAdminApiController : ApiController


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

تا اینجا کار صدور توکن‌های برنامه به پایان می‌رسد. برای استفاده‌ی از این توکن‌ها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی می‌شوند:
var jwtToken;
var refreshToken;
 
function doLogin() {
    $.ajax({
     // same as before
    }).then(function (response) {
        jwtToken = response.access_token;
        refreshToken = response.refresh_token; 
    }
از متغیر jwtToken به ازای هربار درخواستی به یکی از کنترلرهای سایت، استفاده می‌کنیم و از متغیر refreshToken در متد doRefreshToken برای درخواست یک access token جدید. برای مثال اینبار برای اعتبارسنجی درخواست ارسالی به سمت سرور، نیاز است خاصیت headers را به نحو ذیل مقدار دهی کرد:
function doCallApi() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/MyProtectedApi",
        type: 'GET'
    }).then(function (response) {
بنابراین هر درخواست ارسالی به سرور باید دارای هدر ویژه‌ی Bearer فوق باشد؛ در غیراینصورت با پیام خطای 401، از دسترسی به منابع سرور منع می‌شود.


پیاده سازی logout سمت سرور و کلاینت

پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده می‌کنید. در اینجا در اکشن متد Logout، کار حذف توکن‌های کاربر از بانک اطلاعاتی انجام می‌شود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شده‌است یا خیر. چون هش آن دیگر در جدول توکن‌ها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شده‌است:
function doLogout() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/user/logout",
        type: 'GET'
تنها کاری که در اینجا انجام شده، فراخوانی اکشن متد logout سمت سرور است. البته ذکر jwtToken نیز در اینجا الزامی است. زیرا این توکن است که حاوی اطلاعاتی مانند userId کاربر فعلی است و بر این اساس است که می‌توان رکوردهای او را در جدول توکن‌ها یافت و حذف کرد.


بررسی تنظیمات IoC Container برنامه

تنظیمات IoC Container برنامه را در پوشه‌ی IoCConfig می‌توانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگی‌ها در کنترلرهای Web API استفاده می‌شود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگی‌ها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شده‌اند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهله‌های مورد استفاده و تنظیم طول عمر آن‌ها انجام شده‌است. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده می‌شود و Owin هربار آن‌را وهله سازی نمی‌کند. همین مساله سبب شده‌است که معرفی وابستگی‌ها در سازنده‌ی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider(
   Func<IUsersService> usersService,
   Func<ITokenStoreService> tokenStoreService,
   ISecurityService securityService,
   IAppJwtConfiguration configuration)
در کلاسی که طول عمر singleton دارد، وابستگی‌های تعریف شده‌ی در سازنده‌ی آن هم تنها یکبار در طول عمر برنامه نمونه سازی می‌شوند. برای رفع این مشکل و transient کردن آن‌ها، می‌توان از Func استفاده کرد. به این ترتیب هر بار ارجاهی به usersService، سبب وهله سازی مجدد آن می‌شود و این مساله برای کار با سرویس‌هایی که قرار است با بانک اطلاعاتی کار کنند ضروری است؛ چون باید طول عمر کوتاهی داشته باشند.
در اینجا سرویس IAppJwtConfiguration  با Func معرفی نشده‌است؛ چون طول عمر تنظیمات خوانده شده‌ی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی می‌شود، یک سازنده‌ی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
مطالب
تکنیک‌های ایجاد سایت‌های چند زبانه
امروزه چند زبانه بودن سایت‌ها، از اهمیت بالایی برخوردار شده است و هر سایتی که نیاز داشته باشد در سایر نقاط جهان شناخته شود و کاربران مناطق مختلف، به راحتی از آن استفاده کنند، سایت‌های خود را بر پایه‌ی چندین زبان ایجاد می‌کنند. در این نوشتار سعی داریم بر این موضوع بررسی اجمالی داشته باشیم و نکات زیر را بررسی نماییم.

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

طراحی مدل دیتابیس

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

مزایا:

  1. پیاده سازی آسان

معایب:

  1. در این روش با زیاد شدن هر زبان، تعداد ستون‌ها افزایش می‌یابد که باعث می‌شود طراحی مناسبی نداشته باشد.
  2. در ضمن این مورد باید توسط برنامه نویس مرتبا اضافه گردد یا اینکه برنامه نویس این امکان را در سیستم قرار دهد که مدیر سایت بتواند در پشت صحنه کوئری افزودن ستون را ایجاد کند که باید جدول مرتبا مورد alter گرفتن قرار بگیرد.
  3. ممکن است همیشه برای هر زبانی مطلبی قرار نگیرد و این مورد باعث می‌شود بی جهت فضایی برای آن در نظر گرفته شود.

پی نوشت: با اینکه امروزه بحث فیلدهای sparse Column وجود دارد ولی این فیلد‌ها در هر شرایطی مورد استفاده قرار نمی‌گیرند وبیشتر متعلق به زمانی است که می‌دانیم آن فیلد به شدت کم مورد استفاده قرار می‌گیرد.

پی نوشت دوم : در صورتی که فیلد شما مانند متن مقاله که عموما از نوع داده (varchar(max است استفاده می‌کنید و در صورتی که زبان مورد استفاده قرار نگیرد در خیلی از اوقات بی جهت فیلد‌های Blob ساخته اید که بهینه سازی آن را نیز باید در نظر بگیرید.


در مرحله‌ی بعد برای رفع مشکلات بالا یک جدول از زبان‌ها، مانند جدول زیر را ایجاد می‌کنیم:
 
 ID  کد
 Language  زبان
 ISO  کد دو رقمی آن زبان
 Flag  پرچم آن کشور
بعد از آن هر مقاله برای یک زبان ایجاد خواهد شد؛ چیزی مانند تصویر زیر:

مزایا:

  1. پیاده سازی آسان

معایب:

  1. ایجاد رکوردهای تکراری، هر مقاله برای بعضی از اطلاعاتش که چند زبانه نیستند داده‌های تکراری خواهد داشت.
  2. هر مقاله یک مقاله‌ی جدا شناخته می‌شود و ارتباطی میان آنان نخواهد بود. بدین ترتیب توانایی ایجاد گزارش‌هایی چون هر گروه از مقاله و دسته بندی آن‌ها از بین خواهد رفت. در ضمن مدیر عموما در یک سیستم مدیریتی می‌خواهد تنها یک لینک را به یک مقاله بدهد و سایت بنا به تشخیص در زبان مزبور، یکی از این مقالات را به کاربر نمایش دهد؛ نه اینکه مرتبا مدیر برای هر زبان، لینکی را مهیا کند و در این حالت چنین چیزی ممکن نخواهد بود.
  3. در یک سیستم فروشگاهی همانند تصویر بالا کار هم سخت‌تر می‌شود و هر رکورد، یک محصول جدا شناخته می‌شود و ویرایش‌ها هم برای هر کدام باید جداگانه صورت بگیرد که در عمل این طرح را رد می‌کند.


سومین راه حل این است که سه جدول ایجاد کنیم:

یک. جدول زبان‌ها (که بالاتر ایجاد شده بود)

دو . جدول نام مقاله به همراه اطلاعات پایه و فیلدها بی نیاز به چند زبانه بودن

سه : یک جدول که هر دو ستون آن کدهای کلید دو جدول بالا را دارند و فیلدهای چند زبانه در آن وجود دارند.

جدول پایه

 ID  کد
 Name  نام مقاله
 CreationDate  تاریخ ایجاد
 Writer  نویسنده
 Visibilty  وضعیت نمایش
 جدول مقالات
 LanguageCode کد زبان
 ArticleID  کد مقاله
 CreationDate  تاریخ ایجاد
 Visibility  وضعیت نمایش مقاله
Title
عنوان مقاله
ContentText
متن مقاله

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

مزایا:

  1. گزارش گیری آسان برای هر دسته مقاله با زبان‌های مختلف و ارتباط و یکپارچگی
  2. آسان در افزودن زبان.

معایب:

  1. ایجاد کوئری‌های پیچیده‌تر و جوین دار که به نسبت روش‌های قبلی کوئری‌ها پیچیده‌تر شده اند.
  2. کدنویسی زیادتر.

استفاده از ساختارهای XML یا JSON برای ذخیره سازی اطلاعات چند زبانه مانند ساختارهای زیر:

XML
<Articles>
<Article>
this is english text
</Article>
<Article>
این یک متن فارسی است
</Article>
</Articles>

یا 
<Articles>
<en-us>
this is english text
</en-us>
<fa-ir>
این یک متن فارسی است
</fa-ir>
</Articles>
JSON
"Articles":["en-us':{"title":"this is english text","content":" english content"},"fa-ir":{"title":"متن فارسی","content":"محتوای فارسی"}]
ازSQL Server 2005 به بعد از نوع داده xml پشتیبانی می‌شود و در نسخه‌ی 2016 آن نیز پشتیبانی از Json اضافه شده است که حتی شامل اندیکس‌های اختصاصی هم برای این دو نوع می‌باشد.
از مزایای این روش ذخیره‌ی همه داده‌ها در یک ستون و یک جدول است و نیازی به ستون‌های اضافه یا جداول اضافه نیست ولی معایب این روش استفاده از کوئری‌های پیچیده‌تر جهت ارتباط و خواندن است.

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

نکاتی که در یک سایت چند زبانه باید به آن‌ها توجه کرد.

یک . زبان آن صفحه را معرفی کنید: این کار هم به موتورهای جست و جو برای ثبت سایت شما کمک می‌کند و هم برای معلولین که از ابزارهای صفحه خوان استفاده می‌کنند، کمک بزرگی است. در این روش، صفحه خوان‌ها و دستگاه‌های خط بریل که زبان صفحه را تشخیص نمی‌دهند با خواندن کد زبان می‌توانند زبان صفحه را تشخیص دهند. با استفاده از خط زیر میتوانید زبان اصلی صفحه‌ی خود را تنظیم نمایید:
<html lang="en">

اگر از XHTML استفاده می‌کنید خاصیت زیر را فراموش نکنید. دریافت W3C Validation بدون آن امکان پذیر نخواهد بود.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
با تغییر زبان هر صفحه، باید تنظیم زبان آن تغییر یابد:



دو. چند زبانه بودن صفحه: در بالا یاد گرفتیم که چگونه زبان اصلی صفحه را تنظیم کنیم، ولی گاهی اوقات صفحه به غیر از زبان اصلی، شامل زبان‌های دیگر هم می‌شود؛ مثل نقل قول‌ها یا موارد دیگر. برای این‌کار می‌توانید از خصوصیت lang که در اکثر تگ‌ها پشتیبانی می‌شود، استفاده کنید. مثال پایین یک نقل قول فرانسوی است که ما آن را به خصوصیت lang، جهت تایید زبانش مزین کرده‌ایم:
<blockquote lang=”fr”>

<p>Le plus grand faible des hommes, c'est l'amour qu'ils ont de la vie.</p>

</blockquote>

سه. لینک ها : اگر دارید در صفحه‌ای لینک به جایی می‌دهید که متفاوت از زبان شماست، حتما باید زبان صفحه یا سایت مقصد را مشخص کنید. مثلا لینک زیر برای صفحه‌ای است که از یک زبان غیر فرانسوی به یک صفحه‌ی با زبان فرانسوی هدایت می‌شود:
<a href="" hreflang="fr">French</a>

همچنین اگر متن لینک شما هم به زبان فرانسوی باشد خیلی خوب می‌شود که آن را هم بیان کنید و از خاصیت lang و هم hreflang همزمان استفاده کنید:
<a href="" hreflang="fr">Francais</a>

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

پنج. انکودینگ صفحه را مشخص کنید: برای اینکه نحوه‌ی رمزگذاری و رمزگشایی حروف و نمادها مشخص گردد، باید انکودینگ تنظیم شود و حتی برای بعضی از موتورهای جست و جو که ممکن است با وب سایت شما به مشکل بر بخورند. امروزه بیشتر از صفحات یونیکد استفاده می‌شود که سطح وسیعی از کاراکترها را پشتیبانی می‌کند.
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

HTML5
<meta charset="UTF-8">

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

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

:lang(zh) {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}

خط زیر تعیین میکند که از استایل اول استفاده شود:
<html lang="en">
و خط زیر تعیین می‌کند که از استایل دوم استفاده شود:
<html lang="zh">
البته این کد بالا در مرورگرهای فایرفاکس، اپرا و IE8 به بالا پاسخ می‌دهد. برای سایر مروگرها چون کروم و نسخه‌های پیشین IE باید از شیوه‌ی زیر بهره ببرید:
<body class="english"> or <body class="chinese">

و استایل:
.english {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

.chinese {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}
در این شیوه برای تگ مربوطه یک کلاس با نام آن زبان ایجاد کرده که محتوای آن تنظیمات قلم آن زبان می‌باشد.

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

هشت : زمان را نیز تغییر دهید: یکی از مواردی که در کمتر سایت چند زبانه‌ای به چشم می‌خورد و به نظر بنده می‌تواند بسیار مهم باشد این است که time zone منطقه‌ی هر زبان را بدانید. به عنوان مثال برای مقاله‌ی خود، تاریخ ایجاد را به صورت UTC ذخیره کنید و سپس نمایش را بر اساس زبان یا حتی بهتر و دقیق‌تر از طریق IP کشور مربوطه به دست آورید. برای کاربران ثبت نام شده این تاریخ می‌تواند دقیق‌تر باشد همانند انجمن‌های وی بولتین.


شیوه‌های تشخیص زبان سایت

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

دومین راه، استفاده از IP کاربر مراجعه کننده است تا بر اساس آن زبان مورد نظر را انتخاب کنید.

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

پی نوشت: فراموش نگردد که امکان تغییر زبان همیشه برای کاربر مهیا باشد و طوری نباشد که کاربر در آینده نتواند زبان سایت را تغییر دهد؛ حتی اگر تشخیص خودکار سایت برای زبان فعال باشد.

پی نوشت: در روش‌های بالا بهتر است همان مرتبه‌ی اول اطلاعات را در یک کوکی ذخیره کنید تا مراحل پیگیری راحت‌تر و آسان‌تر شود.


پلاگین‌ها و ابزارهای مدیریت زبان

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

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

پلاگین بعدی International Telephone Input است که  پیاده سازی پلاگین بالا می‌باشد. برای مواردی مفید است که شما نیاز دارید کد تلفنی کشوری را انتخاب کنید.

در مقاله‌های زیر که در سایت جاری است در مورد Globalization و به خصوص استفاده از ریسورس‌ها مطالب خوبی بیان شده است:

قسمت بیست و دوم آموزش MVC که مبحث Globalization را دنبال می‌کند.

قسمت اول از شش قسمت مباحث Globalization  که دنباله‌ی آن را می‌توانید در مقاله‌ی خودش دنبال کنید.

نظرات مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- قسمت web api برنامه با mvc یکپارچه هست؟ اگر بله، برای کار با web api نیازی به این روش ندارید. همینقدر که کاربر برای مثال از طریق روش‌هایی مانند ASP.NET Identity و یا Forms authentication به سایت وارد شده باشد، کوکی حاصل از اعتبارسنجی آن‌ها، به همراه اعمال Ajax ای مختص Web API هم به صورت خودکار ارسال می‌شود و ... کاربر با موفقیت اعتبارسنجی خواهد شد. پروژه‌ی مطلب «اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity» یک قسمت Web API محافظت شده هم دارد؛ با این view که کاملا عادی به نظر می‌رسد. چون سیستم یکپارچه هست.
- کاربرد JWT بیشتر در برنامه‌های SPA مانند Angular است: «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular»
در اینجا کاربر پس از لاگین (از طریق صفحه‌ی لاگینی، کاملا عادی و معمولی که هیچ اطلاعاتی هم از پیش در آن ذخیره نشده)، چیزی را که سمت کلاینت ذخیره می‌کند (برای مثال در local storage مرورگر)، فقط توکن‌های دریافتی پس از اعتبارسنجی موفق است و نه کلمه‌ی عبور و نه هیچ اطلاعات دیگری. سپس در هر درخواست به منابع محافظت شده‌ی سمت سرور، صرفا این توکن‌ها را ارسال می‌کند تا توسط آن، کار اعتبارسنجی خودکار کاربر صورت گیرد و هویت و همچنین سطوح دسترسی آن مشخص شوند.
- مثالی که در این پروژه ارائه شده، نمونه‌ی دیگر «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» است؛ جهت ارائه‌ی یک سری مفهوم.
مطالب
روش‌هایی برای بهبود تجربه‌ی کاربری صفحات لاگین و ثبت نام
عموما زمانیکه به طراحی صفحه‌ی لاگین و یا ثبت نام می‌رسیم، ورودی کلمه‌ی عبور را با "type="password علامتگذاری می‌کنیم و ... همین! فارغ از اینکه در سال‌های اخیر، مرورگرها چه امکانات قابل توجهی را در جهت غنی سازی همین یک ورودی ویژه، تدارک دیده‌اند تا کار ثبت نام و یا ورود به یک سایت و برنامه را ساده‌تر و امن‌تر کنند.


کمک به مرورگر، در جهت تمایز بین صفحات ورود و ثبت نام

مرورگرهای جدید قادرند برای صفحه‌ی لاگین، پر کردن خودکار فیلدهای نام کاربری و کلمه‌ی عبور و برای صفحه‌ی ثبت نام، تولید خودکار کلمات عبور قوی، به همراه به‌خاطر سپاری آن‌را ارائه دهند. برای فعال سازی یک چنین قابلیت‌های ویژه‌ای، نیاز است تا یک سری ویژگی را به فیلدهای ورود کلمات عبور اضافه کرد:
الف) نیاز است autocomplete را به نحو صحیحی مقدار دهی کرد:
- اگر مقدار آن مساوی new-password درنظر گرفته شود، یعنی صفحه‌ی جاری، صفحه‌ی ثبت نام است و نیاز است تا تولید کننده‌ی خودکار کلمات عبور مرورگر و بخاطر سپاری آن فعال شوند.


- اگر مقدار آن مساوی current-password درنظر گرفته شود، یعنی صفحه‌ی جاری، صفحه‌ی لاگین است و کاربر نیاز دارد تا کلمه‌ی عبوری را که پیشتر در حین ثبت نام و یا حتی لاگین موفق قبلی وارد کرده‌است، بتواند به سادگی از طریق password manager مرورگر دریافت کند.


ب) بنابراین تا اینجا روش تمایز بین فیلدهای پسورد صفحات لاگین و ثبت نام مشخص شد. اما در مورد نام کاربری چطور؟
برای این حالت فقط کافی است مقدار autocomplete را به username تنظیم کرد تا مرورگر بداند که چگونه باید اطلاعات password manager خودش را به صورت خودکار تامین کند. البته "autocomplete="username در اصل برای معرفی ایمیل‌ها طراحی شده‌است، اما در استفاده‌های برنامه‌های کاربردی ما، جهت معرفی مقدار نام کاربری که کاربر قرار است توسط آن به برنامه وارد شود، کفایت می‌کند.


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

عنوان شد که اگر مقدار autocomplete در صفحات ثبت نام، به مقدار new-password تنظیم شود، یک چنین فیلد ورودی به صورت خودکار به همراه قابلیت ویژه‌ی «تولید کننده‌ی خودکار کلمات عبور مرورگر» نیز خواهد بود. می‌توان این ابزار توکار مرورگرها را نیز سفارشی سازی کرد و بر روی نحوه‌ی تولید کلمات عبور آن تاثیر گذاشت:
<input type="password" autocomplete="new-password" 
  passwordrules="required: upper; required: lower; required: digit; 
                 minlength: 25; allowed: [-().&@?'#,/&quot;+]; max-consecutive: 2">
برای اینکار از ویژگی passwordrules استفاده می‌شود. برای مثال تنظیمات فوق به این معنا هستند:
- کلمه‌ی عبور تولیدی توسط مرورگر باید حداقل به همراه یک عدد، یک حرف بزرگ و یک حرف کوچک باشد.
- حداقل 25 حرف باشد.
- استفاده‌ی از حروف ویژه‌ی -().&@?'#,/"+ در آن مجاز است.
- حداکثر تعداد حروف مشابه متوالی آن باید 2 حرف باشد.


نیاز به خاموش کردن تصحیح‌های هوشمند مرورگرها

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


کمک به مرورگرها در جهت تعویض کلمات عبور ضعیف و فاش شده‌!

برای نمونه مرورگر کروم به همراه قسمتی است که بر اساس password manager توکار خودش و اطلاعات فاش شده‌ی بر روی اینترنت، عنوان می‌کند که کدامیک از کلمات عبور شما دیگر امن نیستند و بهتر است آن‌ها را عوض کنید. این صفحه را می‌توانید در آدرس ویژه‌ی chrome://settings/passwords مرورگر کروم مشاهده کنید.


نکته‌ی مهم اینجا است که این مرورگر، دکمه‌ی Change password و لینکی را نیز به سایت و برنامه‌ی مدنظر، برای تعویض کلمه‌ی عبور ارائه می‌دهد و این لینک، قالب ثابت و ویژه‌ای دارد: https://example.com/.well-known/change-password
یعنی هموار کاربر را به آدرس ثابت well-known/change-password. در سایت شما هدایت می‌کند و در این حالت بهتر است یک route ویژه‌ی خاص آن‌را تعریف کرده و کاربر را به صورت خودکار به صفحه‌ی اصلی تعویض کلمه‌ی عبور، برای مثال به آدرس فرضی https://example.com/settings/change-password به صورت خودکار هدایت کنید.

یک نمونه مثال این هدایت خودکار در یک برنامه‌ی ASP.NET Core به صورت زیر است:
app.UseEndpoints(endpoints =>
{
  // TODO: Add it to your app!
  // Improves chrome://settings/passwords page by managing `Change password` button next to a password.
  endpoints.MapGet("/.well-known/change-password",
                   context =>
                   {
                     // `/.well-known/change-password` address will be called by the `Change password` button of the Chrome.
                     // Now our Web-API app redirects the user to the `/change-password` address of the App.
                      context.Response.Redirect("/change-password", true);
                      return Task.CompletedTask;
                    });
  endpoints.MapRazorPages();
  // ...
});
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت اول - نیاز به تامین کننده‌ی هویت مرکزی
- Identity server الزاما یک تامین کننده‌ی هویت خارج از شبکه‌ی شما مثل گوگل نیست و به صورت یک برنامه‌ی مجزای داخلی در IIS شبکه‌ی شما قابل نصب و استفاده است (در قسمت‌های بعدی جزئیات نصب و راه اندازی آن بررسی می‌شوند).
- برنامه‌ی شما پس از احراز هویت توسط Identity server، دسترسی به Id کاربر لاگین شده را خواهد داشت. سایر اطلاعات او را هم از طریق Claims دریافت می‌کند. مابقی آن مانند قبل است (کار با Claims هم در قسمت‌های بعدی بررسی می‌شوند).
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
در «قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی»، اطلاعات TestUser تنظیم شده‌ی در کلاس Config برنامه‌ی IDP را به بانک اطلاعاتی منتقل کردیم که در نتیجه‌ی آن سه جدول Users، UserClaims و UserLogins، تشکیل شدند. در اینجا می‌خواهیم سایر قسمت‌های کلاس Config را نیز به بانک اطلاعاتی منتقل کنیم.


تنظیم مجوز امضای توکن‌های IDP

namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
تا اینجا تنظیمات کلاس آغازین برنامه چنین شکلی را دارد که AddCustomUserStore آن‌را در قسمت دهم به آن افزودیم.
مرحله‌ی بعد، تغییر AddDeveloperSigningCredential به یک نمونه‌ی واقعی است. استفاده‌ی از روش فعلی آن چنین مشکلاتی را ایجاد می‌کند:
- اگر برنامه‌ی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا می‌کند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست می‌کند. یعنی با ری‌استارت شدن Application pool، کلیدهای مجوز امضای توکن‌ها تغییر می‌کنند که در نهایت سبب شکست اعتبارسنجی توکن‌های صادر شده‌ی توسط IDP می‌شوند.

بنابراین برای انتشار نهایی برنامه نمی‌توان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامه‌ی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده می‌کنیم. این فایل را از پوشه‌ی src\IDP\DNT.IDP\MakeCert نیز می‌توانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
 makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
در اینجا تاریخ شروع و پایان اعتبار مجوز ذکر شده‌اند. همچنین نتیجه‌ی آن به صورت خودکار در LocalMachine certificate store ذخیره می‌شود. به همین جهت اجرای آن نیاز به دسترسی admin را دارد.
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینه‌ی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحه‌ی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون می‌توانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:


در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.

چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفته‌است که باید این انتقال را انجام داد. در غیراینصورت می‌توان توسط آن توکن‌های صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینه‌ی All tasks -> export را انتخاب کنید. نکته‌ی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینه‌ی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمی‌کند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینه‌ی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.

پس از ایجاد مجوز امضای توکن‌ها و انتقال آن به Trusted Root Certification Authorities، نحوه‌ی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
        }

        private X509Certificate2 loadCertificateFromStore()
        {
            var thumbPrint = Configuration["CertificateThumbPrint"];
            using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true);
                if (certCollection.Count == 0)
                {
                    throw new Exception("The specified certificate wasn't found.");
                }
                return certCollection[0];
            }
        }

        private X509Certificate2 loadCertificateFromFile()
        {
            // NOTE:
            // You should check out the identity of your application pool and make sure
            // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work.
            var certificate = new X509Certificate2(
                fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", 
                    Configuration["X509Certificate:FileName"]),
                password: Configuration["X509Certificate:Password"],
                keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet |
                                 X509KeyStorageFlags.Exportable);
            return certificate;
        }
    }
}
متد کمکی loadCertificateFromStore، بر اساس thumbPrint مجوز تولید شده، آن‌را بارگذاری می‌کند. سپس این مجوز، توسط متد AddSigningCredential به IdentityServer معرفی خواهد شد و یا اگر فایل pfx ای را دارید، می‌توانید از متد loadCertificateFromFile استفاده کنید. این متد برای اینکه در IIS به درستی کار کند، نیاز است در خواص Application pool سایت IDP، گزینه‌ی Load user profile را انتخاب کرده باشید (مهم!).

پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آن‌را در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration
https://localhost:6001/.well-known/openid-configuration/jwks


انتقال سایر قسمت‌های فایل Config برنامه‌ی IDP به بانک اطلاعاتی

قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر داده‌های فایل Config، مانند Resources و Clients برنامه‌ی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامه‌های متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آن‌ها داخل فایل Config برنامه‌ی IDP، مشکلی را ایجاد نمی‌کند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامه‌ی IDP توسط یک صفحه‌ی مدیریتی داشته باشیم، نیاز است آن‌ها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر می‌کند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام این‌ها اکنون در حافظه ذخیره می‌شوند که با ری‌استارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفاده‌ی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمده‌ی این کار توسط خود تیم IdentityServer توسط بسته‌ی IdentityServer4.EntityFramework انجام شده‌است که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بسته‌ی نیوگت را مستقیما مورد استفاده قرار نمی‌دهیم. از این جهت که نیاز به 2 رشته‌ی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شده‌است و ترجیح می‌دهیم که اطلاعات آن‌را با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آن‌را از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases

انتقال موجودیت‌ها به پروژه‌ی DNT.IDP.DomainClasses

در این بسته‌ی دریافتی، در پوشه‌ی src\IdentityServer4.EntityFramework\Entities آن، کلاس‌های تعاریف موجودیت‌های متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایل‌ها را از این پروژه استخراج کرده و به پروژه‌ی DNT.IDP.DomainClasses در پوشه‌ی جدید IdentityServer4Entities اضافه می‌کنیم.
البته در این حالت پروژه‌ی DNT.IDP.DomainClasses نیاز به این وابستگی‌ها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" />
    <PackageReference Include="IdentityServer4" Version="2.2.0" />
  </ItemGroup>
</Project>
پس از این انتقال، فضاهای نام این کلاس‌ها را نیز اصلاح می‌کنیم؛ تا با پروژه‌ی جاری تطابق پیدا کنند.


انتقال تنظیمات روابط بین موجودیت‌ها، به پروژه‌ی DNT.IDP.DataLayer

در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بسته‌ی دریافتی، تعاریف تنظیمات این موجودیت‌ها به همراه نحوه‌ی برقراری ارتباطات بین آن‌ها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژه‌ی DNT.IDP.DataLayer اضافه می‌کنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کرده‌ایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایل‌های IEntityTypeConfiguration را معرفی می‌کنیم.


تعاریف DbSetهای متناظر با موجودیت‌های منتقل و تنظیم شده در پروژه‌ی DNT.IDP.DataLayer

پس از انتقال موجودیت‌ها و روابط بین آن‌ها، دو فایل DbContext را در این بسته‌ی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیت‌های تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار می‌دهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیت‌های ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار می‌دهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.

ما در اینجا DbSetهای هر دوی این DbContext‌ها را در ApplicationDbContext خود، خلاصه و ادغام می‌کنیم.


انتقال نگاشت‌های AutoMapper بسته‌ی دریافتی به پروژه‌ی جدید DNT.IDP.Mappings

در پوشه‌ی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشت‌های AutoMapper، برای تبدیلات بین موجودیت‌های برنامه و IdentityServer4.Models انجام شده‌است. کل محتویات این پوشه را به یک پروژه‌ی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آن‌را نیز اصلاح می‌کنیم.


انتقال src\IdentityServer4.EntityFramework\Options به پروژه‌ی DNT.IDP.Models

در پوشه‌ی Options بسته‌ی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینت‌ها است. نیازی به آن نداریم؛ چون زمانیکه موجودیت‌ها و تنظیمات آن‌ها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکن‌ها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دوره‌ای توکن‌های قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options
{
    public class OperationalStoreOptions
    {
        public bool EnableTokenCleanup { get; set; } = false;
        public int TokenCleanupInterval { get; set; } = 3600;
        public int TokenCleanupBatchSize { get; set; } = 100;
    }
}
از این تنظیمات در سرویس TokenCleanup استفاده می‌شود. به همین جهت همین سه مورد را به پروژه‌ی DNT.IDP.Models منتقل کرده و سپس بجای اینکه این کلاس را مستقیما در سرویس TokenCleanup تزریق کنیم، آن‌را از طریق سیستم Configuration و فایل appsettings.json به این سرویس تزریق می‌کنیم؛ به کمک سرویس توکار IOptions خود ASP.NET Core:
public TokenCleanup(
  IServiceProvider serviceProvider, 
  ILogger<TokenCleanup> logger, 
  IOptions<OperationalStoreOptions> options)
ج) Options\TableConfiguration.cs
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفته‌است. نیازی به آن نداریم.


انتقال سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP.Services

بسته‌ی دریافتی، شامل دو پوشه‌ی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویس‌های آن‌را تشکیل می‌دهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویس‌ها را نیز مستقیما از این پوشه‌ها به پروژه‌ی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بسته‌ی دریافتی در سرویس‌های آن، تعریف شده‌اند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایه‌ی سرویس قرار می‌گیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.


انتقال متدهای الحاقی معرفی سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP

پس از انتقال قسمت‌های مختلف IdentityServer4.EntityFramework به لایه‌های مختلف برنامه‌ی جاری، اکنون نیاز است سرویس‌های آن‌را به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظه‌ای کلاس آغازین برنامه‌ی IDP می‌شوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام می‌دهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژه‌ی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاس‌های تنظیمات را حذف کردیم و نیازی به آن‌ها نداریم، آن‌ها را نیز به طور کامل از این فایل حذف می‌کنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آن‌را از فایل appsettings.json دریافت می‌کنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بسته‌ی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفته‌اند. ما در اینجا آن‌ها را نیز با تک Context برنامه‌ی خود تعویض می‌کنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شده‌است. آن‌را نیز به لایه‌ی سرویس‌ها منتقل می‌کنیم. همچنین در امضای سازنده‌ی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شده‌ی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده می‌کنید.


افزودن متدهای الحاقی جدید به فایل آغازین برنامه‌ی IDP

پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژه‌ی DNT.IDP، اکنون نوبت به استفاده‌ی از آن است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddConfigurationStore()
                .AddOperationalStore();
به این ترتیب متدهای الحاقی جدید AddConfigurationStore و AddOperationalStore جهت معرفی محل‌های ذخیره سازی اطلاعات کاربران، منابع و توکن‌های IdentityServer مورد استفاده قرار می‌گیرند.


اجرای Migrations در پروژه‌ی DNT.IDP.DataLayer

پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیت‌ها و روابط جدید بین آن‌ها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشه‌ی src\IDP\DNT.IDP.DataLayer را اجرا می‌کنیم تا کلاس‌های Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا می‌کنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.


انتقال اطلاعات فایل درون حافظه‌ای Config، به بانک اطلاعاتی برنامه

تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمی‌کند. چون جداول کلاینت‌ها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظه‌ای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کرده‌ایم:
    public interface IConfigSeedDataService
    {
        void EnsureSeedDataForContext(
            IEnumerable<IdentityServer4.Models.Client> clients,
            IEnumerable<IdentityServer4.Models.ApiResource> apiResources,
            IEnumerable<IdentityServer4.Models.IdentityResource> identityResources);
    }
این سرویس به کمک اطلاعات Mappings مخصوص AutoMapper این پروژه، IdentityServer4.Models تعریف شده‌ی در کلاس Config درون حافظه‌ای را به موجودیت‌های اصلی DNT.IDP.DomainClasses.IdentityServer4Entities تبدیل و سپس در بانک اطلاعاتی ذخیره می‌کند.
برای استفاده‌ی از آن، به کلاس آغازین برنامه‌ی IDP مراجعه می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            initializeDb(app);
            seedDb(app);
    // ...
        }
        
        private static void seedDb(IApplicationBuilder app)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>();
                configSeedDataService.EnsureSeedDataForContext(
                    Config.GetClients(),
                    Config.GetApiResources(),
                    Config.GetIdentityResources()
                    );
            }
        }
در اینجا توسط متد seedDb، متدهای درون حافظه‌ای کلاس Config به سرویس ConfigSeedDataService ارسال شده، توسط Mappings تعریف شده، به معادل‌های موجودیت‌های برنامه تبدیل و سپس در بانک اطلاعاتی ذخیره می‌شوند.


آزمایش برنامه

پس از اعمال این تغییرات، برنامه‌ها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شده‌اند ملاحظه می‌کنید:


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


و یا قسمت ذخیره سازی خودکار توکن‌های تولیدی توسط آن نیز به درستی کار می‌کند:





کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
پاسخ به بازخورد‌های پروژه‌ها
رمز شش حرفی نیاز است
ممنون بابت گزارش
در نسخه‌ی جدید که بر روی سایت قرار داده شده اصلاح شده و کلمه عبور پیش فرض بر روی 123456 قرار گرفته است.
مطالب
ASP.NET MVC #18

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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



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

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

using System.Web.Mvc;

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

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

@{
ViewBag.Title = "Index";
}

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

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

<authentication mode="Windows" />

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



بررسی حالت Forms Authentication

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

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

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

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

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

using System.ComponentModel.DataAnnotations;

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

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

public bool RememberMe { get; set; }
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[Authorize(Users="Vahid")]

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

using System;
using System.Web.Security;

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

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

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

return new string[] { };
}

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

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

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

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

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

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

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

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

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

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

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


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

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