نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
میخوام یه پروژه MVC رو به تمپلیت Skoruba به عنوان یک کلاینت جدید اضافه کنم. تو تنظیمات Sturtup پروژه Mvc اینگونه عمل کردم:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            IdentityModelEventSource.ShowPII = true;
            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
       options.DefaultSignOutScheme = "oidc";
                })
                .AddCookie("Cookies", options =>
                {
                    options.AccessDeniedPath = "/Authorization/AccessDenied";
                    // set session lifetime
                    options.ExpireTimeSpan = TimeSpan.FromHours(8);
                    // sliding or absolute
                    options.SlidingExpiration = false;
                    // host prefixed cookie name
                    options.Cookie.Name = "MVC";
                    // strict SameSite handling
                    options.Cookie.SameSite = SameSiteMode.Strict;
                })
                .AddOpenIdConnect("oidc", options =>
                {
                    options.SignInScheme = "Cookies";
                    options.Authority = Configuration["IDPBaseAddress"]; 
                    options.ClientId = Configuration["ClientId"];
                    options.ClientSecret = Configuration["ClientSecret"];                      
                    options.ResponseType = "code id_token";
                    options.ResponseMode = "query";

                    options.RequireHttpsMetadata = false;
                    options.CallbackPath = new PathString("/Home/");
                    options.SignedOutCallbackPath = new PathString("/Home/");

                    options.MapInboundClaims = true;

                    options.Scope.Clear();
                    options.Scope.Add("openid");
                    options.Scope.Add("profile");
                    options.Scope.Add("roles");
                    options.Scope.Add("PS.WebApi.Read");
                    options.Scope.Add("offline_access");
                    
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    //options.UsePkce = true;
                    //options.ClaimActions.MapJsonKey(claimType: "role", jsonKey: "role"); // for having 2 or more roles
                    
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = JwtClaimTypes.GivenName,
                        RoleClaimType = JwtClaimTypes.Role
                    };
                });
            //ServicePointManager.Expect100Continue = true;
            //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
            //                                       | SecurityProtocolType.Tls11
            //                                       | SecurityProtocolType.Tls12
            //                                       | SecurityProtocolType.Ssl3;
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "areas",
                    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
                );
                //.RequireAuthorization();

                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}"
                );
                //.RequireAuthorization();
            });

            //[HttpPost]
            //public IActionResult Logout()
            //{
            //    return SignOut("Cookies", "oidc");
            //}
        }

پروژه‌های بنده به ترتیب Endpoint هاشون اینگونه هست:
Skoruba.IdentityServer4.Admin  = https://localhost:44303
Skoruba.IdentityServer4.STS.Identity   = https://localhost:44310
Skoruba.IdentityServer4.Admin.Api    = https://localhost:44356 
Mvc_Client_Project = https://localhost:44332
تنظیمات کانفیک پروژه MVC: (appsettings.json)
  "WebApiBaseAddress": "https://localhost:44356",
  "IDPBaseAddress": "https://localhost:44310",
  "ClientId": "Mvc_ClientId",
  "ClientSecret": "WebMvc"
محتویات فایل Identityserverdata.json:

{
    "IdentityServerData": {
        "IdentityResources": [
            {
                "Name": "roles",
                "Enabled": true,
                "DisplayName": "Roles",
                "UserClaims": [
                    "role"
                ]
            },
            {
                "Name": "openid",
                "Enabled": true,
                "Required": true,
                "DisplayName": "Your user identifier",
                "UserClaims": [
                    "sub"
                ]
            },
            {
                "Name": "profile",
                "Enabled": true,
                "DisplayName": "User profile",
                "Description": "Your user profile information (first name, last name, etc.)",
                "Emphasize": true,
                "UserClaims": [
                    "name",
                    "family_name",
                    "given_name",
                    "middle_name",
                    "nickname",
                    "preferred_username",
                    "profile",
                    "picture",
                    "website",
                    "gender",
                    "birthdate",
                    "zoneinfo",
                    "locale",
                    "updated_at"
                ]
            },
            {
                "Name": "email",
                "Enabled": true,
                "DisplayName": "Your email address",
                "Emphasize": true,
                "UserClaims": [
                    "email",
                    "email_verified"
                ]
            },
            {
                "Name": "address",
                "Enabled": true,
                "DisplayName": "Your address",
                "Emphasize": true,
                "UserClaims": [
                    "address"
                ]
            }
        ],
        "ApiScopes": [
          {
            "Name": "Idp_Admin_ClientId_api",
            "DisplayName": "Idp_Admin_ClientId_api",
            "Required": true,
            "UserClaims": [
              "role",
              "name"
            ]
          },
          {
            "Name": "WebApi.Read",
            "DisplayName": "WebApi Read",
            "Required": true,
            "UserClaims": [
              "role",
              "WebApi.Read"
            ]
          },
          {
            "Name": "WebApi.Write",
            "DisplayName": "WebApi Write",
            "Required": true,
            "UserClaims": [
              "role",
              "WebApi.Write"
            ]
          }
        ],
        "ApiResources": [
          {
            "Name": "Idp_Admin_ClientId_api",
            "Scopes": [
              "Idp_Admin_ClientId_api"
            ]
          },
          {
            "Name": "WebApi",
            "Scopes": [
              "WebApi.Read",
              "WebApi.Write"
            ]
          }
        ],
      "Clients": [
        {
          "ClientId": "Idp_Admin_ClientId",
          "ClientName": "Idp_Admin_ClientId",
          "ClientUri": "https://localhost:44303",
          "AllowedGrantTypes": [
            "authorization_code"
          ],
          "RequirePkce": true,
          "ClientSecrets": [
            {
              "Value": "Idp_Admin_ClientSecret"
            }
          ],
          "RedirectUris": [
            "https://localhost:44303/signin-oidc"
          ],
          "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
          "PostLogoutRedirectUris": [
            "https://localhost:44303/signout-callback-oidc"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44303"
          ],
          "AllowedScopes": [
            "openid",
            "email",
            "profile",
            "roles"
          ]
        },
        {
          "ClientId": "Idp_Admin_ClientId_api_swaggerui",
          "ClientName": "Idp_Admin_ClientId_api_swaggerui",
          "AllowedGrantTypes": [
            "authorization_code"
          ],
          "RequireClientSecret": false,
          "RequirePkce": true,
          "RedirectUris": [
            "https://localhost:44302/swagger/oauth2-redirect.html"
          ],
          "AllowedScopes": [
            "Idp_Admin_ClientId_api"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44302"
          ]
        },
        //WebApi
        {
          "ClientId": "WebApi_ClientId",
          "ClientName": "WebApi_ClientId",
          "ClientUri": "https://localhost:44365",
          "AllowedGrantTypes": [
            "authorization_code"
          ],
          "RequirePkce": true,
          "ClientSecrets": [
            {
              "Value": "WebApi"
            }
          ],
          "RedirectUris": [
            "https://localhost:44303/signin-oidc"
          ],
          "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
          "PostLogoutRedirectUris": [
            "https://localhost:44303/signout-callback-oidc"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44303",
            "https://localhost:44310"
          ],
          "AllowedScopes": [
            "openid",
            "email",
            "profile",
            "roles"
          ]
        },
        //Mvc
        {
          "ClientId": "Mvc_ClientId",
          "ClientName": "Mvc_ClientId",
          "ClientUri": "https://localhost:44332",
          "AllowedGrantTypes": [
            "hybrid"
          ],
          //"RequirePkce": true,
          "AllowPlainTextPkce": false,
          "ClientSecrets": [
            {
              "Value": "WebMvc"
            }
          ],

          "RedirectUris": [
            "https://localhost:44332/signin-oidc"
          ],
          "FrontChannelLogoutUri": "https://localhost:44332/signout-oidc",
          "PostLogoutRedirectUris": [
            "https://localhost:44332/signout-callback-oidc"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44332",
            "https://localhost:44310"
          ],
          "AllowedScopes": [
            "openid",
            "email",
            "profile",
            "roles",
            "address",
            "PS.webApi"
          ],
          "AllowAccessTokensViaBrowser": true,
          "RequireConsent": false,
          "AllowOfflineAccess": true
          //"UpdateAccessTokenClaimsOnRefresh": true
        }
      ]
    }
}

کنترلر Home در پروژه MVC:
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Default()
        {
            return View();
        }
        public IActionResult Index()
        {
            return View();
        }

        [Authorize]
        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }

اما در نهایت بعد از اجرا و مراجعه به آدرس https://localhost:44332/home/privacy که مزین به اتریبیوت
[Authorize]  
می‌باشد با خطای زیر مواجه می‌شوم:

لازم به توضیح هست که پروپرتی RequireHttpsMetadata = false می‌باشد.

مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهارم - نصب و راه اندازی IdentityServer
معرفی IdentityServer 4


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


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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

آزمایش IDP

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


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


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


افزودن UI به IdentityServer

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

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


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


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

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

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


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


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
آیا واکنش‌های اخیر در مورد AngularJS 2.0 زود است؟

AngularJS 2.0 با نگارش 1.0 آن سازگار نیست و نیاز به بازنویسی بسیاری از قسمت‌ها وجود خواهد داشت ... تا جائیکه نویسنده مطلب عنوان می‌کند که واقعا نمی‌داند آیا به صلاح است AngularJS را دیگر به کسی توصیه کرد یا خیر.

آیا واکنش‌های اخیر در مورد AngularJS 2.0 زود است؟
نظرات اشتراک‌ها
دوره آموزشی AngularJS فارسی
متاسفانه این دوره برای افرادی خوب هست که میخواهند بدانند که AngularJS چیست و چه کاربردهایی دارد.
در مقایسه با نمونه‌های مشابه خارجی بسیار ضعیف است !