آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

برای مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» و پروژه‌ی آن، یک چنین رابط کاربری آزمایشی تهیه شده‌است:


اکنون در ادامه قصد داریم این موارد را تبدیل به چندین درخواست به هم مرتبط postman کرده و در نهایت آن‌ها را به صورت یک collection قابل آزمایش مجدد، ذخیره کنیم.

مرحله 1: خاموش کردن بررسی مجوز SSL برنامه

چون مجوز SSL برنامه‌های ASP.NET Core که در حالت local اجرا می‌شوند از نوع self-signed certificate است، توسط postman پردازش نخواهند شد. به همین جهت نیاز است به منوی File -> Settings آن مراجعه کرده و این بررسی را خاموش کنیم:



مرحله 2: ایجاد درخواست login و دریافت توکن‌ها


در اینجا این مراحل طی شده‌اند:
- ابتدا آدرس درخواست به https://localhost:5001/api/account/login تنظیم شده‌است.
- سپس نوع درخواست نیز به Post تغییر کرده‌است.
- چون اکنون نوع درخواست، Post است، می‌توان بدنه‌ی آن‌را مقدار دهی کرد و چون نوع آن JSON است، گزینه‌ی raw و سپس contentType صحیح انتخاب شده‌اند. در ادامه مقدار زیر تنظیم شده‌است:
{
    "username": "Vahid",
    "password": "12345"
}
این مقداری‌است که اکشن متد login می‌پذیرد. البته اگر برنامه را اجرا کنید، کلمه‌ی عبور پیش‌فرض آن 1234 است.
- پس از این تنظیمات اگر بر روی دکمه‌ی send کلیک کنیم، توکن‌های دریافتی را در قسمت response می‌توان مشاهده کرد.


مرحله‌ی 3: ذخیره سازی توکن‌های دریافتی در متغیرهای سراسری

برای دسترسی به منابع محافظت شده‌ی سمت سرور، نیاز است access_token را به همراه هر درخواست، به سمت سرور ارسال کرد. بنابراین نیاز است در همینجا این دو توکن را در متغیرهایی برای دسترسی بعدی، ذخیره کنیم:

var jsonData = pm.response.json();
pm.globals.set("access_token", jsonData.access_token);
pm.globals.set("refresh_token", jsonData.refresh_token);
محل تعریف این کدها نیز در قسمت Tests درخواست جاری است تا پس از پایان کار درخواست، اطلاعات آن استخراج شده و ذخیره شوند.


مرحله‌ی 3: ذخیره سازی مراحل انجام شده

برای این منظور، بر روی دکمه‌ی Save کنار Send کلیک کرده، نام Login را وارد و سپس یک Collection جدید را با نام دلخواه JWT Sample ایجاد می‌کنیم:


سپس این درخواست را در این مجموعه ذخیره خواهیم کرد.


مرحله‌ی 4: دسترسی به منابع محافظت شده‌ی سمت سرور

برای این منظور بر روی دکمه‌ی + موجود در کنار اولین برگه‌ای که مشغول به تکمیل آن هستیم، کلیک می‌کنیم تا یک برگه‌ی جدید ایجاد شود. سپس مشخصات آن را به صورت زیر تکمیل خواهیم کرد:


ابتدا آدرس درخواست از نوع Get را به https://localhost:5001/api/MyProtectedApi تنظیم خواهیم کرد.
سپس گزینه‌ی هدرهای این درخواست را انتخاب کرده و key/value جدیدی با مقادیر Authorization و Bearer {{access_token}} ایجاد می‌کنیم. در اینجا {{access_token}} همان متغیر سراسری است که پس از لاگین، تنظیم می‌شود. اکنون از این متغیر جهت تنظیم هدر Authorization استفاده کرده‌ایم.
در آخر اگر بر روی دکمه‌ی Send این درخواست کلیک کنیم، response فوق را می‌توان مشاهده کرد.

فراخوانی مسیر api/MyProtectedAdminApi نیز دقیقا به همین نحو است.

یک نکته: روش دومی نیز برای تنظیم هدر Authorization در postman وجود دارد. برای این منظور، گزینه‌ی Authorization یک درخواست را انتخاب کنید (تصویر زیر). سپس نوع آن‌را به Bearer token تغییر دهید و مقدار آن‌را به همان متغیر سراسری {{access_token}} تنظیم کنید. به این صورت هدر مخصوص JWT را به صورت خودکار ایجاد و ارسال می‌کند و در این حالت دیگر نیازی به تنظیم دستی برگه‌ی هدرها نیست.



مرحله‌ی 5: ارسال Refresh token و دریافت یک سری توکن جدید

این مرحله، نیاز به ارسال anti-forgery token را هم دارد و گرنه از طرف سرور و برنامه، برگشت خواهد خورد. اگر به کوکی‌های تنظیم شده‌ی توسط برگه‌ی لاگین دقت کنیم:


کوکی با نام XSRF-TOKEN نیز تنظیم شده‌است. بنابراین آن‌را توسط متد pm.cookies.get، در قسمت Tests برگه‌ی لاگین خوانده و در یک متغیر سراسری تنظیم می‌کنیم:
var jsonData = pm.response.json();
pm.globals.set("access_token", jsonData.access_token);
pm.globals.set("refresh_token", jsonData.refresh_token);
pm.globals.set('XSRF-TOKEN', pm.cookies.get('XSRF-TOKEN'));
سپس برگه‌ی جدید ایجاد درخواست refresh token به صورت زیر تنظیم می‌شود:
ابتدا در قسمت بدنه‌ی درخواست از نوع post به آدرس https://localhost:5001/api/account/RefreshToken، در قسمت raw آن، با انتخاب نوع json، این refresh token را که در قسمت لاگین خوانده و ذخیره کرده بودیم، به سمت سرور ارسال خواهیم کرد:


همچنین دو هدر زیر را نیز باید ارسال کنیم:


یکی هدر مخصوص Authorization است که در مورد آن بحث شد و دیگر هدر X-XSRF-TOKEN که در سمت سرور بجای anti-forgery token پردازش می‌شود. مقدار آن‌را نیز در برگه‌ی login با خواندن کوکی مرتبطی، پیشتر تنظیم کرده‌ایم که در اینجا از متغیر سراسری آن استفاده شده‌است.

اکنون اگر بر روی دکمه‌ی send این برگه کلیک کنید، دو توکن جدید را دریافت خواهیم کرد:


که نیاز است مجددا در برگه‌ی Tests آن، متغیرهای سراسری پیشین را بازنویسی کنند. به همین جهت دقیقا همان 4 سطری که اکنون در برگه‌ی Tests درخواست لاگین وجود دارند، باید در اینجا نیز تکرار شوند تا عملیات refresh token واقعا به تمام برگه‌های موجود، اعمال شود.


مرحله‌ی آخر: پیاده سازی logout

در اینجا نیاز است refreshToken را به صورت یک کوئری استرینگ به سمت سرور ارسال کرد که به کمک متغیر {{refresh_token}}، در برگه‌ی params تعریف می‌شود:


همچنین هدر Authorization را نیز باید درج کرد:


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



ذخیره سازی مجموعه‌ی جاری به صورت یک فایل JSON

برای گرفتن خروجی از این مجموعه و به اشتراک گذاشتن آن‌، اگر اشاره‌گر ماوس را بر روی نام یک مجموعه حرکت دهیم، یک دکمه‌ی جدید با برچسب ... ظاهر می‌شود. با کلیک بر روی آن، یکی از گزینه‌های آن، export نام دارد که جزئیات تمام درخواست‌های یک مجموعه را به صورت یک فایل JSON ذخیره می‌کند. برای نمونه فایل JSON خروجی این قسمت را می‌توانید از اینجا دریافت کنید: JWT-Sample.postman_collection.json
  • #
    ‫۴ سال و ۱۰ ماه قبل، پنجشنبه ۱۶ آبان ۱۳۹۸، ساعت ۱۷:۳۶
    با سلام;
    در قسمت RefreshToken  وقتی درخواست ارسال می‌شود نتیجه به صورت تصویر پیوست نشان داده می‌شود، در صورتی که در بقیه موارد(login, logout, MyProtectedApi) درست کار میکند.
    قسمت Test رو هم با موارد 
    var jsonData = pm.response.json();
    pm.globals.set("access_token", jsonData.access_token);
    pm.globals.set("refresh_token", jsonData.refresh_token);
    pm.globals.set('XSRF-TOKEN', pm.cookies.get('XSRF-TOKEN'));

    از JToken ارسالی به سمت تابع RefreshToken ایراد میگیرد.



    • #
      ‫۴ سال و ۱۰ ماه قبل، پنجشنبه ۱۶ آبان ۱۳۹۸، ساعت ۱۸:۰۰
      آن پروژه برای NET Core 2.2. تنظیم شده. احتمالا پروژه را به نگارش 3 ارتقاء دادید که خطای زیر را دریافت کردید:
      The collection type 'Newtonsoft.Json.Linq.JToken' is not supported
      علت آن‌را در مطلب «معرفی System.Text.Json در NET Core 3.0.» و قسمت «روش بازگشت به Json.NET در ASP.NET Core 3x» آن، مطالعه کنید.
      • #
        ‫۴ سال و ۱۰ ماه قبل، پنجشنبه ۱۶ آبان ۱۳۹۸، ساعت ۱۸:۳۴
        بله حرف شما درسته؛ من از ورژن 3 استفاده میکنم. ولی در پروژه ای که دارم از تابع : 
                [HttpPost("[action]")]
                public async Task<IActionResult> RefreshToken([FromBody]JToken jsonBody)
                {
                    var refreshTokenValue = jsonBody.Value<string>("refreshToken");
          استفاده میکنم، و اگر بخواهم معادل را در System.Text. Json استفاده کنم باید به چه صورتی کار کنم؟ (معادل JToken)
        • #
          ‫۴ سال و ۱۰ ماه قبل، پنجشنبه ۱۶ آبان ۱۳۹۸، ساعت ۱۸:۵۳
          public class Token
          {
              [JsonPropertyName("refreshToken")]
              public string RefreshToken { get; set; }
          }
          
          public async Task<IActionResult> RefreshToken([FromBody]Token model)
  • #
    ‫۴ سال و ۷ ماه قبل، دوشنبه ۱۴ بهمن ۱۳۹۸، ساعت ۱۵:۴۹
    تنظیمات آغازین برنامه:
    public void ConfigureServices(IServiceCollection services)
            {
                // بقیه کدها جهت سهولت در خوانایی حذف شده اسند
                 services.AddAntiforgery(opt =>
                {
                    opt.Cookie.Name = ".Middleware.Antiforgery";
                    opt.HeaderName = "X-XSRF-TOKEN";
                    opt.SuppressXFrameOptionsHeader = false;
                });
            }
    فراخوانی کنترلرهایی که دارای POST VERB هستند با استفاده از POSTMAN بدرستی در حال انجام هستند ولی فراخوانی همان کنترلها توسط ajax یا سایر کتابخانه‌ها مانند Restsharp با تکه کد زیر با خطای badrequest همراه میشود.
    var loginUrl = new RestClient("https://XXXXXXXXXXXX/account/login");
                var loginRequest = new RestRequest(Method.POST);
                loginRequest.AddJsonBody(new { Username = txtUsername.Text, Password = txtPassword.Text });
                var loginResponse = loginUrl.Execute(loginRequest);
                var loginContent = JToken.Parse(loginResponse.Content);
                var loginCookies = loginResponse.Cookies;
                var antiforgerytokeCookie = loginCookies[1].Value;
                _accessToken = loginContent.Value<string>("access_token");
    
                var authenticateUrl = new RestClient("https://XXXXXXXXXXXX/account/ad/authenticate");
                var authRequest = new RestRequest(Method.POST);
                authRequest.AddHeader("X-XSRF-TOKEN", antiforgerytokeCookie);
                authRequest.AddHeader("Authorization", "Bearer " + _accessToken);
                var authResponse = authenticateUrl.Execute(authRequest);
                var infoContent = authResponse.Content;
    • #
      ‫۴ سال و ۷ ماه قبل، دوشنبه ۱۴ بهمن ۱۳۹۸، ساعت ۱۶:۰۳
      - یک نمونه مثال کامل کنسول «ASPNETCore Jwt Authentication.ConsoleClient» که کوکی‌های آنتی‌فورجری پس از لاگین را دریافت و پردازش می‌کند؛ مرتبط با پروژه‌ی مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» که در بحث جاری به آن پرداخته شد. مثال Ajax ای آن هم در همان پروژه موجود است.
      - اگر «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» را پیاده سازی کنید و مباحث «تکمیل مستندات محافظت از API» را لحاظ کنید، با استفاده از ابزارهایی می‌توانید « تولید خودکار کدهای سمت کلاینت» را هم انجام دهید.
      • #
        ‫۴ سال و ۷ ماه قبل، دوشنبه ۱۴ بهمن ۱۳۹۸، ساعت ۱۶:۳۶
        آیا این روش برای فراخوانی‌های CORS نیز امکان پذیر است؟
        • #
          ‫۴ سال و ۷ ماه قبل، دوشنبه ۱۴ بهمن ۱۳۹۸، ساعت ۱۶:۵۲
          بله. «... اگر درخواست Ajax ایی را به دومین دیگری ارسال کنید، به صورت پیش‌فرض به همراه کوکی‌های مرتبط نخواهد بود. برای رفع این مشکل نیاز است خاصیت withCredentials را به true تنظیم کنید ...»
          $.ajax('http://someotherdomain.com', {
            method: 'POST',
            contentType: 'text/plain',
            data: 'sometext',
            beforeSend: function(xmlHttpRequest) {
               xmlHttpRequest.withCredentials = true;
            }
           });
          • #
            ‫۴ سال و ۷ ماه قبل، دوشنبه ۱۴ بهمن ۱۳۹۸، ساعت ۱۸:۵۱
            این روش فراخوانی برروی CORS‌ها رو چندین بار قبلا نیز بررسی کرده بودم ولی نمی‌دونم چرا وقتی درخواست از سرور دیگر انجام میشه هیچ کوکی برروی مرورگر ذخیره نمیشه بنابراین متد  getCookie فایل نمونه ajax ای نمی‌تونه کوکی مربوط به antiforgerytoken رو دریافت کنه و پیغام badrequest صادر میشه.
              • #
                ‫۴ سال و ۷ ماه قبل، سه‌شنبه ۱۵ بهمن ۱۳۹۸، ساعت ۱۹:۲۴
                با نکته اشاره شده در بالا و تغییرات زیر در سرویس AntiForgeryCookieService این پروژه نیز در مرورگر کوکی درج نشد.
                var httpContext = _contextAccessor.HttpContext;
                            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme));
                            var tokens = _antiforgery.GetAndStoreTokens(httpContext);
                            httpContext.Response.Cookies.Append(
                                  key: XsrfTokenKey,
                                  value: tokens.RequestToken,
                                  options: new CookieOptions
                                  {
                                      SameSite = SameSiteMode.Unspecified,
                                      IsEssential = true,
                                      Secure = httpContext.Request.IsHttps,
                                      HttpOnly = false // Now JavaScript is able to read the cookie
                                  });

                • #
                  ‫۴ سال و ۷ ماه قبل، چهارشنبه ۱۶ بهمن ۱۳۹۸، ساعت ۰۱:۳۵
                  فرض کنید دومینی به نام contoso.com وجود دارد. به دلایل امنیتی فقط برای این دومین جاری و همچنین تمام زیر دومین‌های آن مانند first_subdomain.contoso.com و second_subdomain.contoso.com می‌توانید کوکی را ایجاد کنید و نمی‌توانید از دومین abc.com برای دومین xyz.com کوکی را تنظیم کنید. به علاوه اگر قرار است این کوکی برای زیر دومین‌های آن قابل خواندن باشد باید این تنظیم ویژه را هم در کدهای سمت سرور اضافه کنید:
                  options.Cookie.Domain = ".contoso.com";