مطالب
پیاده سازی 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 کنید.
نظرات مطالب
EF Code First #12
- می‌شود به ازای هر سال یک Context مجزا با Entity‌های مجزا درست کرد. فایل مثالی که با دو Context کار می‌کند در نظرات همان مطلب «استفاده از چندین Context در EF 6 Code first»  پیوست شده‌است: Sample25.cs 
ولی این روش سبب خواهد شد مجبور شوید به ازای هر سال، کوئری‌های LINQ مختلفی را هم بنویسید. یعنی لایه سرویس برنامه را باید هربار بازنویسی کنید، فقط برای اینکه نمی‌خواهید ساختار بانک اطلاعاتی را به روز کنید. چرا؟
- EF با استفاده از امکانات Migration به سادگی ساختار بانک‌های اطلاعاتی را به صورت خودکار می‌تواند به روز کند. باید هم اینکار را انجام بدهید چون کوئری‌های مختلف LINQ شما نهایتا به SQL ترجمه شده و چون یک سری از فیلدها در بانک اطلاعاتی سال قبل حضور ندارند، عملا برنامه کار نخواهد کرد. یعنی قسمت عمده‌ای از برنامه شما (کل لایه سرویس) از کار می‌افتد. کامپایل شدن برنامه در این حالت مهم نیست. آیا مثلا تنها کوئری GetAll ایی که تهیه شده، بر روی تمام سال‌ها و با ساختارهای مختلف اجرا می‌شود؟ خیر.
- سپس برای کار با بانک‌های اطلاعاتی دارای یک ساختار و مربوط به سال‌های مختلف، امکان تعیین رشته اتصالی به ازای هر Context هست:
context.Database.Connection.ConnectionString = "...";
مطالب
WF:Windows Workflow #2
ایجاد یک گردش ساده

در این دوره آموزشی قصد آموزش WF4  را داریم. برای ایجاد یک پروژه از نوع WF4  نیاز به VS2010 یا VS2012 است.
زمانیکه ویژوال استودیو را باز می‌کنید و بر روی گزینه ایجاد پروژه جدید کلیک می‌نمائید، در قسمت Workflow، چندین نوع پروژه وجود دارد که هر کدام از آن‌ها را به نوبت بررسی خواهیم کرد.
ابتدا یک پروژه از نوع Workflow Console Application  را ایجاد کنید:

پس ایجاد پروژه، اگر دقت داشته باشید، فایل ایجاد شده با پسوند .XAML  می‌باشد و یک کلاس Program.cs  هم در پروژه ایجاد گردیده که حاوی کلاس Main است و کار آن دقیقا مثل برنامه‌های کنسول معمولی می‌باشد.
اگر به قسمت پایین ویژوال استودیو دقت کنید، سه گزینه به نام‌های Variables ,Arguments ,Imports قابل مشاهده هستند:
  1. Variables : همین طور که از نام این گزینه بر می‌آید، برای تعریف متغیر‌ها در طول یک فرآیند می‌باشد.
  2. Arguments : این گزینه هم مانند قسمت قبل عمل کرده، ولی تفاوت‌هایی باهم دارند. به عنوان مثال در گزینه ۱ می‌توان برای متغیر، محدوده ایجاد کرد. ولی در گزینه 2 هیچگونه محدودیتی وجود ندارد و در تمام  Workflowها می‌توان از آن استفاده کرد و دیگر اینکه در گزینه ۲ می‌توان به متغیر گفت که از نوع ورودی In یا از نوع خروجی Out ویا هر دو  In/Out و یا اینکه از نوع Property باشد.
  3. Imports : این قسمت فضا‌های نامی که در برنامه استفاده می شوند، نشان داده می‌شوند .

برای ایجاد یک Flow ابتدا باید از  Toolbox، قسمت Control Flow، کنترل  Sequence را به داخل صفحه کشید و  پس از آن می‌توانیم از سایر کنترل‌ها استفاده نمائیم. پس از این کار، از قسمت  Primitives کنترل  WriteLine را به درون Sequence انتقال می‌دهیم .


این کنترل برای چاپ مقادیر در خروجی می‌باشد. مانند کدی که در زیر نوشته شده است عمل کرده است و این کار را از طریق خاصیتی به نام Text انجام می‌دهد.
 Console.WriteLine("Hello Word");
نکته: توجه کنید برای تغییر نام اجزایی که به درون صفحه کشیده می‌شوند، می‌توان برروی نام آن دوبار کلیک کرده و نام آن را تغییر دهید و یا با انتخاب آن، در پنجره Properties نام آن را عوض کنیم. برای این کار کافی است مقدار گزینه DisplayName  را با مقدار مورد نظر عوض نمائیم. این گزینه در تمام اجزای Workflow  موجود می‌باشد.
حال باید برنامه را اجرا کنیم تا خروجی را که چاپ رشته  "Hello Word"  است، مشاهده نمائیم. ولی قبل از آن باید تغییراتی در فایل Program.cs داده شود که به شرح زیر می‌باشد:
WorkflowInvoker.Invoke( new Workflow1());

 Console.WriteLine("Press ENTER to exit"); 
 Console.ReadLine();
از کلاس  WorkflowInvoker برای اجرای Flow مورد نظر استفاده شده و این‌کار از طریق متد Invoke انجام داده می‌شود.
نظرات مطالب
EF Code First #12
- DbContext در زمان شروع برنامه در دسترس است. مانند کلاس Sample07Context مثال این قسمت.
- اگر قرار است به ازای هر موجودیت یک دیتابیس داشته باشید یعنی چندین کلاس DbContext مجزا نیاز هست با تعاریف مختلف DbSetها در ابتدای کار. هر کدام را باید جداگانه در شروع برنامه با روش زیر مدیریت کرد. مثلا:
public partial class MyCtx1 : DbContext
{
    public MyCtx1(string connectionString) : base(connectionString) { }
}
و در این حالت بدیهی است هر کدام در Context مختلفی کار می‌کنند و بحث اعمال الگوی واحد کار در یک Context معنا دارد نه در چندین Context. در EF مباحث Tracking فقط در یک Context کار می‌کنند.
و اگر قرار است تمام موجودیت‌ها در یک کلاس DbContext باشند، خوب ... همگی نهایتا در زمان فعال سازی migrations باید در دیتابیس مشترکی قرار گیرند و گرنه برنامه آغاز نخواهد شد یا اینکه migrations را کلا از کار انداخت.
- به علاوه در همین مثال جاری در زمان تزریق وابستگی‌ها می‌شود نوشت:
                x.For<IUnitOfWork>().HttpContextScoped().Use(() =>
                {
                    var ctx = new Sample07Context();
                    ctx.Database.Connection.ConnectionString = "...";
                    return ctx;
                });
مطالب
استفاده از افزونه MD.BootstrapPersianDateTimePicker در گریدهای Kendo UI
در این مطلب قصد داریم نحوه‌ی یکپارچه سازی افزونه‌ی انتخاب تاریخ و زمان MD.BootstrapPersianDateTimePicker را با گریدهای Kendo UI، در دو حالت ویرایش به صورت popup و یا inline، بررسی کنیم:



پیشنیازها

برای این مطلب از دو کتابخانه‌ی moment-jalaali، برای تبدیل تاریخ، از میلادی به شمسی و برعکس، استفاده خواهیم کرد. همچنین کنترل انتخاب تاریخ نیز از کتابخانه‌ی MD.BootstrapPersianDateTimePicker تامین می‌شود.
moment-jalaali را می‌توانید به صورت ذیل از طریق npm نصب کنید:
 npm install moment-jalaali --save
سپس دو مدخل ذیل را باید به مجموعه‌ی مداخل اسکریپت‌های صفحه‌ی خود اضافه کنید:
node_modules/moment/min/moment.min.js
node_modules/moment-jalaali/build/moment-jalaali.js

MD.BootstrapPersianDateTimePicker را یا از طریق نیوگت نصب کنید و یا مخزن کد آن‌را به صورت کامل دریافت کنید:
 Install-Package MD.BootstrapPersianDateTimePicker
پس از دریافت این بسته، فایل jquery.Bootstrap-PersianDateTimePicker.css آن‌را به مداخل cssهای خود اضافه کنید.
همچنین فایل‌های jalaali.js و jquery.Bootstrap-PersianDateTimePicker.js آن‌را نیز به مداخل اسکریپت‌های صفحه‌ی خود اضافه نمائید.

در کل اگر از ASP.NET Core استفاده می‌کنید، فایل bundleconfig.json یک چنین شکلی را پیدا خواهد کرد. در این حالت فایل layout برنامه تنها این دو مدخل نهایی را نیاز خواهد داشت:
<link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" />
<script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>


فعالسازی کنترل انتخاب تاریخ شمسی بجای کامپوننت پیش فرض انتخاب تاریخ Kendo UI

پس از افزودن ارجاعات مورد نیاز، اکنون فرض ما بر این است که ستون تاریخ، دقیقا با فرمت میلادی از سمت سرور دریافت می‌شود و addDate نام دارد.
پس از آن، مرحله‌ی اول کار، نمایش این تاریخ میلادی به صورت شمسی است:
{
   field: "addDate", title: "تاریخ ثبت",
   template: "#=moment(addDate).format('jYYYY/jMM/jDD')#",
این‌کار را با تعریف یک template و سپس ارسال مقدار addDate که همان نام خاصیت ستون جاری است به کتابخانه‌ی moment-jalaali، انجام می‌دهیم. در این حالت همواره تاریخ‌های میلادی تنظیم شده‌ی در این ستون، در حالت نمایش معمولی با فرمت شمسی ظاهر می‌شوند.

در ادامه‌ی تکمیل خواص ستون جاری، خاصیت editor را اضافه خواهیم کرد:
 editor: function(container, options) {
}
توسط پارامتر اول آن می‌توان کار سفارشی سازی کنترل ویرایش این ستون را انجام داد و خاصیت دوم آن، نام فیلد جاری و همچنین مقادیر مدل آن‌را در اختیار ما قرار می‌دهد.
در ادامه نیاز است یک input field سفارشی را در اینجا درج کنیم تا کار نمایش کنترل انتخاب تاریخ شمسی را انجام دهد:
// دریافت تاریخ میلادی و تبدیل آن به شمسی جهت نمایش در تکست باکس
var persianAddDate = moment(options.model.addDate).format('jYYYY/jMM/jDD');
// ایجاد کنترل انتخاب تاریخ سفارشی با مقدار تاریخ شمسی دریف جاری
var input = $('<div dir="ltr" class="input-group">'+
'<div class="input-group-addon" data-name="datepicker1" data-mddatetimepicker="true" data-trigger="click" data-targetselector="#' + options.field + '" data-fromdate="false" data-enabletimepicker="false" data-englishnumber="true" data-placement="left">'+
'<span class="glyphicon glyphicon-calendar"></span>'+
'</div>'+
'<input type="text" value="'+ persianAddDate +'" class="form-control" id="' + options.field + '" placeholder="از تاریخ" data-mddatetimepicker="true" data-trigger="click" data-targetselector="#' + options.field + '" data-englishnumber="true" data-fromdate="true" data-enabletimepicker="false" data-placement="right" />'+
'</div>');
// افزودن کنترل جدید به صفحه
input.appendTo(container);
کاری که در اینجا انجام شده، افزودن یک input مزین شده با ویژگی‌های مخصوص MD.BootstrapPersianDateTimePicker است تا توسط آن شناسایی شود. همچنین چون مقدار options.model.addDate در اصل میلادی است، توسط کتابخانه‌ی moment-jalaali به شمسی تبدیل و بجای value این کنترل ارائه می‌شود تا زمانیکه این ستون در حالت ویرایش قرار گرفت، این تاریخ شمسی توسط کنترل انتخاب تاریخ، به درستی پردازش شده و نمایش داده شود.
متد input.appendTo، کار افزودن این input جدید را به container یا همان محل نمایش ستون جاری، انجام می‌دهد.

در این حالت اگر برنامه را اجرا کنید، هرچند ظاهر Input جدید تغییر کرده‌است، اما سبب نمایش کنترل انتخاب تاریخ نمی‌شود؛ چون این فیلد ویرایشی، پس از رندر صفحه، به صفحه اضافه شده‌است. به همین جهت نیاز است متد EnableMdDateTimePickers نیز فراخوانی شود. این متد کار فعالسازی input جدید را انجام خواهد داد:
 // با خبر سازی کتابخانه انتخاب تاریخ از تکست باکس جدید
EnableMdDateTimePickers();

تا اینجا موفق شدیم بجای کنترل انتخاب تاریخ میلادی، کنترل انتخاب تاریخ شمسی را نمایش دهیم. اما تاریخی که انتخاب می‌شود نیز شمسی است و تاریخی که به سمت سرور ارسال خواهد شد، میلادی می‌باشد. به همین جهت تغییرات این کنترل را تحت نظر قرار داده و هر زمانیکه کاربر تاریخی را انتخاب کرد، آن‌را به میلادی تبدیل کرده و بجای فیلد addDate اصلی تنظیم می‌کنیم:
// هر زمانیکه کاربر تاریخ جدیدی را وارد کرد، آن‌را به میلادی تبدیل کرده و در مدل ردیف جاری ثبت می‌کنیم
// در نهایت این مقدار میلادی است که به سمت سرور ارسال خواهد شد
$('#' + options.field).change(function(){
   var selectedPersianDate = $(this)[0].value;
   var gregorianAddDate = moment(selectedPersianDate, 'jYYYY/jMM/jDD').format('YYYY-MM-DD');
   options.model.set('addDate', gregorianAddDate);
});

تنها مرحله‌ی باقیمانده، مخفی کردن این کنترل نمایش تاریخ، با از دست رفتن فوکوس است:
// با از دست رفتن فوکوس نیاز است این کنترل مخفی شود
$('#' + options.field).blur(function(){
   $('[data-name="datepicker1"]').MdPersianDateTimePicker('hide');
});
اگر این کار را انجام ندهیم، در حالت popup با بسته شدن آن، هنوز کنترل نمایش تاریخ نمایان خواهد بود و بسته نمی‌شود یا در حالت ویرایش inline، با کلیک بر روی دکمه‌ی لغو ویرایش ردیف، باز هم شاهد باقی ماندن این کنترل در صفحه خواهیم بود که تجربه‌ی کاربری مطلوبی به شمار نمی‌رود.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: با این کنترلر و این View
بازخوردهای دوره
بوت استرپ (نگارش 3) چیست؟
- همانطور که عنوان شد، در سایت اصلی دریافت بوت استرپ، امکان سفارشی سازی و کار پویا با نگارش LESS آن وجود دارد. برای مثال قسمت تغییر پویای پیش فرض‌های navbar در اینجا
- اگر خودتان مستقیما می‌خواهید این موارد را تغییر دهید، کلیه فایل‌های LESS آن در مخزن اصلی کدهای بوت استرپ موجود هستند. در اینجا . برای نمونه فایل navbar.less آن.
اگر به فایل‌های less آن دقت کنید، تمام آن‌ها پارامتری هستند و هیچکدام دارای مقدار پیش فرضی نیستند. مقادیر پیش فرض مورد استفاده در فایل variables.less تعریف شده‌اند. برای تغییر کلی فایل CSS نهایی فقط کافی است که مقادیر این فایل را تغییر دهید.
فایل نهایی که سبب کامپایل تمام تغییرات خواهد شد، فایل bootstrap.less است.
- استفاده از فایل‌های LESS در نگارش‌های جدید VS.NET ساده‌است و به صورت توکار و خودکار، تغییرات را کامپایل کرده و فایل CSS نهایی را تولید می‌کند:


روی فایل bootstrap.less دوبار کلیک کنید (تمام فایل‌های پوشه less سورس بوت استرپ به VS.NET اضافه شده‌اند). اندکی صبر کنید تا کار کامپایل تمام شود. در صفحه‌ای که باز می‌شود، دو ستون قابل مشاهده است. ستون سمت چپ، فایل less اصلی است و ستون سمت راست، خروجی CSS کامپایل شده نهایی.
مطالب
React 16x - قسمت 34 - توزیع برنامه
در قسمت آخر این سری، نگاهی خواهیم داشت به نحوه‌ی توزیع برنامه‌های React و نکات مرتبط با آن.


افزودن متغیرهای محیطی

در برنامه‌ی نمایش لیست فیلم‌هایی که تا قسمت 29 آن‌را بررسی کردیم، از فایل src\config.json برای ذخیره سازی اطلاعات تنظیمات برنامه استفاده شد. هرچند این روش کار می‌کند اما بر اساس محیط‌های مختلف توسعه، متغیر نیست. اغلب برنامه‌ها باید بتوانند حداقل در سه محیط توسعه، آزمایش و تولید، بر اساس متغیرها و تنظیمات خاص هر کدام، کار کنند. برای مثال بر روی سیستمی که کار توسعه در آن انجام می‌شود، می‌خواهیم apiUrl متفاوتی را نسبت به حالتیکه برنامه توزیع می‌شود، داشته باشیم.
برای رفع این مشکل، برنامه‌هایی که توسط create-react-app تولید می‌شوند، دارای پشتیبانی توکاری از متغیرهای محیطی هستند. برای این منظور نیاز است در ریشه‌ی پروژه (جائیکه فایل package.json قرار دارد) فایل جدید env. را ایجاد کرد. در ویندوز برای ایجاد یک چنین فایل‌هایی که فقط از یک پسوند تشکیل می‌شوند، باید نام فایل را به صورت .env. وارد کرد؛ سپس خود ویندوز نقطه‌ی نهایی را حذف می‌کند. البته اگر از ادیتور VSCode برای ایجاد این فایل استفاده می‌کنید، نیازی به درج نقطه‌ی انتهایی نیست. در این فایل environment ایجاد شده می‌توان تمام متغیرهای محیطی مورد نیاز را با مقادیر پیش‌فرض آن‌ها درج کرد. همچنین می‌توان این مقادیر پیش‌فرض را بر اساس محیط‌های مختلف کاری، بازنویسی کرد. برای مثال می‌توان فایل env.development. را اضافه کرد؛ به همراه فایل‌های env.test. و env.production.


متغیرهای محیطی به صورت key=value درج می‌شوند. این کلیدها نیر باید با REACT_APP_ شروع شوند؛ در غیر اینصورت، کار نخواهند کرد. برای مثال در فایل env.، دو متغیر پیش‌فرض زیر را تعریف می‌کنیم:
REACT_APP_NAME=My App
REACT_APP_VERSION=1
اکنون برای خواندن این متغیرها برای مثال در فایل index.js (و یا هر فایل جاوا اسکریپتی دیگری در برنامه)، سطر زیر را درج می‌کنیم:
console.log(process.env);
process به معنای پروسه‌ی جاری برنامه‌است (و مرتبط است به پروسه‌ی node.js ای که برنامه‌ی React را اجرا می‌کند) و خاصیت env، به همراه تمام متغیرهای محیطی برنامه می‌باشد. در این حالت اگر برنامه را اجرا کنیم، در کنسول توسعه دهندگان مرورگر، به یک چنین خروجی خواهیم رسید:


در این خروجی، متغیر "NODE_ENV: "development به صورت خودکار با تولید بسته‌های مخصوص ارائه‌ی نهایی، به production تنظیم می‌شود. سایر متغیرهای محیطی تعریف شده را نیز در اینجا ملاحظه می‌کنید. با توجه به خواص شیء env، برای مثال جهت دسترسی به نام برنامه می‌توان از مقدار process.env.REACT_APP_NAME استفاده کرد.


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


نحوه‌ی پردازش متغیرهای محیطی درج شده‌ی در برنامه

اگر همان سطر لاگ کردن خروجی process.env را به صورت زیر تغییر دهیم:
console.log("My App Name", process.env.REACT_APP_NAME);
و برنامه را مجددا اجرا کنیم، با مراجعه‌ی به برگه‌ی Sources و انتخاب مسیر localhost:3000/static/js/main.chunk.js و سپس جستجوی "My App Name" ای که در اینجا اضافه کردیم (با فشردن دکمه‌های Ctrl+F)، به خروجی زیر خواهیم رسید:


همانطور که مشاهده می‌کنید، فراخوانی console.log ما، دیگر به همراه متغیر process.env.REACT_APP_NAME نیست؛ بلکه مقدار اصلی این متغیر در اینجا درج شده‌است. بنابراین اگر در در حین توسعه‌ی برنامه، از متغیرهای محیطی استفاده شود، این متغیرها با مقادیر اصلی آن‌ها در حین پروسه‌ی Build نهایی، جایگزین می‌شوند.


Build برنامه‌های React برای محیط تولید

اجرای دستور npm start، سبب ایجاد یک Build مخصوص محیط توسعه می‌شود که بهینه سازی نشده‌است و به همراه اطلاعات اضافی قابل توجهی جهت دیباگ ساده‌تر برنامه‌است. برای رسیدن به یک خروجی بهینه سازی شده‌ی مخصوص محیط تولید و ارائه‌ی نهایی باید دستور npm run build را در خط فرمان اجرا کرد. خروجی نهایی این دستور، در پوشه‌ی جدید build واقع در ریشه‌ی پروژه، قرار می‌گیرد. اکنون می‌توان کل محتویات این پوشه را جهت ارائه‌ی نهایی در وب سرور خود، مورد استفاده قرار داد.
پس از پایان اجرای دستور npm run build، پیام «امکان ارائه‌ی آن توسط static server زیر نیز وجود دارد» ظاهر می‌شود:
> npm install -g serve
> serve -s build
اگر علاقمند باشید تا خروجی حالت production تولید شده را نیز به صورت محلی آزمایش کنید، ابتدا باید static server یاد شده را توسط دستور npm install فوق نصب کنید. سپس ریشه‌ی پروژه را در خط فرمان باز کرده و دستور serve -s build را صادر کنید (البته اگر با خط فرمان به پوشه‌ی build وارد شدید، دیگر نیازی به ذکر پوشه‌ی build نخواهد بود). اکنون می‌توانید برنامه را در آدرس http://localhost:5000 در مرورگر خود بررسی نمائید.

البته با توجه به اینکه backend سرور برنامه‌های ما نیز در همین آدرس قرار دارد و در صورت ورود این آدرس، به صورت خودکار به https://localhost:5001/index.html هدایت خواهید شد، می‌توان این پورت پیش‌فرض را با اجرای دستور  serve -s build -l 1234 تغییر داد. اکنون می‌توان آدرس جدید http://localhost:1234 را در مرورگر آزمایش کرد که ... با خطای زیر کار نمی‌کند:
Access to XMLHttpRequest at 'https://localhost:5001/api/genres' from origin 'http://localhost:1234' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
روش رفع این مشکل را در قسمت 23 بررسی کردیم و در اینجا جهت بهبود آن می‌توان متد WithOrigins فایل Startup.cs را به صورت زیر تکمیل کرد:
WithOrigins("http://localhost:3000", "http://localhost:1234")

یک نکته: زمانیکه از دستور npm start استفاده می‌شود، متغیرهای محیطی از فایل env.development. خوانده خواهند شد و زمانیکه از دستور npm run build استفاده می‌شود، این متغیرها از فایل env.production. تامین می‌شوند. در این حالت‌ها اگر متغیری در این دو فایل درج نشده بود، از مقدار پیش‌فرض موجود در فایل env. استفاده می‌گردد. از فایل env.test. با اجرای دستور npm test، به صورت خودکار استفاده می‌شود.


آماده سازی برنامه‌ی React، برای توزیع نهایی

تا اینجا برنامه‌ی React تهیه شده، اطلاعات apiUrl خودش را از فایل config.json دریافت می‌کند. اکنون می‌خواهیم بر اساس حالات مختلف توسعه و تولید، از apiUrlهای متفاوتی استفاده شود. به همین جهت به فایل env.production. مراجعه کرده و تنظیمات ذیل را به آن اضافه می‌کنیم:
REACT_APP_API_URL=https://localhost:5001/api
REACT_APP_ADMIN_ROLE_NAME=Admin
البته فعلا همین متغیرها را به فایل env.development. نیز می‌توان اضافه کرد؛ چون backend سرور ما در هر دو حالت، در این مثال، در آدرس فوق قرار دارد.

اکنون به برنامه مراجعه کرده و در هرجائی که ارجاعی به فایل config.json وجود دارد، سطر import آن‌را حذف می‌کنیم. با این تغییر، تمام آدرس‌هایی مانند:
const apiEndpoint = apiUrl + "/users";
را به صورت زیر ویرایش می‌کنیم:
const apiEndpoint =  "/users";
در ادامه برای تامین مقدار apiUrl به صورت خودکار، به فایل src\services\httpService.js مراجعه کرده و در ابتدای آن، یک سطر زیر را اضافه می‌کنیم:
 axios.defaults.baseURL = process.env.REACT_APP_API_URL;
به این ترتیب تمام درخواست‌های ارسالی توسط Axios، دارای baseURL ای خواهند شد که از فایل متغیر محیطی جاری تامین می‌شود. همانطور که پیش‌تر نیز عنوان شد، این مقدار در زمان Build، با مقدار ثابتی که از فایل env جاری خوانده می‌شود، جایگزین خواهد شد.
همچنین adminRoleName مورد نیاز در فایل src\services\authService.js را نیز از همان فایل env جاری تامین می‌کنیم:
const adminRoleName =  process.env.REACT_APP_ADMIN_ROLE_NAME;
پس از این تغییرات، نیاز است برای حالت توسعه، یکبار دیگر دستور npm start و یا برای حالت تولید، دستور npm run build را اجرا کرد تا اطلاعات درج شده‌ی در فایل‌های env.، پردازش و جایگزین شوند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-34-frontend.zip و sample-34-backend.zip