۶ سال و ۵ ماه قبل، چهارشنبه ۲۲ فروردین ۱۳۹۷، ساعت ۱۸:۵۶
۶ سال و ۵ ماه قبل، سهشنبه ۲۱ فروردین ۱۳۹۷، ساعت ۲۳:۴۹
پاسخ دادن به این سؤال بدون مطالعهی سورس https://github.com/aspnet/Antiforgery غیرممکن است. علت اینکه روش توضیح داده شدهی در این مطلب، برای حالت برنامههای دارای اعتبارسنجی کار نمیکند و خطای «The provided antiforgery token was meant for a different claims-based user than the current user» را دریافت میکنید، این صورت است:
میانافزار طراحی شدهی در این مطلب، پیش از لاگین و با اولین درخواست تک صفحهی برنامه، کوکیهای antiforgery را دریافت میکند. اما ... سیستم antiforgery طوری طراحی شدهاست که پیش از تولید کوکی، مشخصات دقیق this.HttpContext.User را دریافت و هش میکند (لیست Claims آنرا به صورت رشته در میآورد و هش SHA256 آن را محاسبه میکند). از این هش هم جهت تولید محتوای کوکی نهایی خود استفاده میکند. بنابراین در بار اولی که صفحه درخواست شدهاست، یک کوکی antiforgery مخصوص کاربر null و اعتبارسنجی نشده، تولید خواهد شد. بعد از آن پس از لاگین، اگر میانافزار یاد شده مجددا نیز فراخوانی شود، هیچ اتفاق خاصی رخ نخواهد داد. از این جهت که در طراحی متد GetAndStoreTokens آن، به ازای یک صفحه، فقط یکبار این کوکی تولید میشود و اگر هزار بار دیگر هم این متد را جهت برنامهی تک صفحهای خود فراخوانی کنیم، به این معنا نخواهد بود که مشخصات this.HttpContext.User را به کوکی جدیدی اضافه میکند؛ چون اصلا کوکی جدیدی را تولید نمیکند!
بنابراین راه حل نهایی به این صورت است:
الف) میان افزار AngularAntiforgeryTokenMiddleware فوق را حذف کنید. این میانافزار عملا کاربردی برای برنامههای SPA دارای اعتبارسنجی ندارد.
ب) امضای متد Login را به این صورت تغییر دهید که شامل IgnoreAntiforgeryToken باشد:
از این جهت که نمیتوانیم پیش از لاگین، درخواست تولید کوکی antiforgery را برای کاربر null صادر کنیم؛ چون این کوکی تا پایان عمر مرورگر تغییری نخواهد کرد.
ج) در متد لاگین، پس از تولید توکنها، اکنون کار تولید کوکی را به صورت زیر انجام میدهیم:
در اینجا لیست claims ایی را که در حین تولید توکنها ایجاد کردیم، تبدیل به یک ClaimsPrincipal میکنیم تا بتوانیم this.HttpContext.User را مقدار دهی کنیم (دقیقا با مشخصات اصلی کاربر لاگین شده) و سپس متد GetAndStoreTokens را فراخوانی میکنیم تا این User غیرنال را خوانده و کوکی صحیحی را تولید کند. به این صورت است که دیگر پیام خطای «این antiforgery توکن، متعلق به کاربر دیگری است» را دریافت نخواهیم کرد. این «کاربر دیگر» منظورش همان کاربر null ابتدای کار است که پیش از لاگین تنظیم میشد و با کاربر جدیدی که دارای claims است یکی نبود.
نکتهی مهم! جائیکه Claimهای برنامه را تولید میکنید، باید حتما Issuer را هم ذکر کنید:
از این جهت که هش SHA256 ایی که توضیح داده شده، بر اساس این Issuer، مقدار و نوع Claim محاسبه میشود. اگر Issuer را ذکر نکنید، به local authority تنظیم میشود که با Issuer نهایی توکن تولید شده یکی نیست. به همین جهت حتی اگر this.HttpContext.User را هم مقدار دهی کنید، کار نمیکند؛ چون هش claimهای تولیدی، یکی نخواهند بود و باز هم پیام «این antiforgery توکن متعلق به کاربر دیگری است» را دریافت خواهید کرد.
خلاصه این تغییرات به پروژهی ASPNETCore2JwtAuthentication اعمال شدهاند.
میانافزار طراحی شدهی در این مطلب، پیش از لاگین و با اولین درخواست تک صفحهی برنامه، کوکیهای antiforgery را دریافت میکند. اما ... سیستم antiforgery طوری طراحی شدهاست که پیش از تولید کوکی، مشخصات دقیق this.HttpContext.User را دریافت و هش میکند (لیست Claims آنرا به صورت رشته در میآورد و هش SHA256 آن را محاسبه میکند). از این هش هم جهت تولید محتوای کوکی نهایی خود استفاده میکند. بنابراین در بار اولی که صفحه درخواست شدهاست، یک کوکی antiforgery مخصوص کاربر null و اعتبارسنجی نشده، تولید خواهد شد. بعد از آن پس از لاگین، اگر میانافزار یاد شده مجددا نیز فراخوانی شود، هیچ اتفاق خاصی رخ نخواهد داد. از این جهت که در طراحی متد GetAndStoreTokens آن، به ازای یک صفحه، فقط یکبار این کوکی تولید میشود و اگر هزار بار دیگر هم این متد را جهت برنامهی تک صفحهای خود فراخوانی کنیم، به این معنا نخواهد بود که مشخصات this.HttpContext.User را به کوکی جدیدی اضافه میکند؛ چون اصلا کوکی جدیدی را تولید نمیکند!
بنابراین راه حل نهایی به این صورت است:
الف) میان افزار AngularAntiforgeryTokenMiddleware فوق را حذف کنید. این میانافزار عملا کاربردی برای برنامههای SPA دارای اعتبارسنجی ندارد.
ب) امضای متد Login را به این صورت تغییر دهید که شامل IgnoreAntiforgeryToken باشد:
[AllowAnonymous] [IgnoreAntiforgeryToken] [HttpPost("[action]")] public async Task<IActionResult> Login([FromBody] User loginUser)
ج) در متد لاگین، پس از تولید توکنها، اکنون کار تولید کوکی را به صورت زیر انجام میدهیم:
private void regenerateAntiForgeryCookie(IEnumerable<Claim> claims) { this.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme)); var tokens = _antiforgery.GetAndStoreTokens(this.HttpContext); this.HttpContext.Response.Cookies.Append( key: "XSRF-TOKEN", value: tokens.RequestToken, options: new CookieOptions { HttpOnly = false // Now JavaScript is able to read the cookie }); }
نکتهی مهم! جائیکه Claimهای برنامه را تولید میکنید، باید حتما Issuer را هم ذکر کنید:
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString(), ClaimValueTypes.String, _configuration.Value.Issuer),
خلاصه این تغییرات به پروژهی ASPNETCore2JwtAuthentication اعمال شدهاند.
۶ سال و ۵ ماه قبل، یکشنبه ۱۹ فروردین ۱۳۹۷، ساعت ۱۵:۳۵
دلیل دیگری برای مشاهدهی خطای 502.5
فایل web.config یک برنامهی publish نشدهی ASP.NET Core چنین شکلی را دارد:
اگر این فایل بدون publish بر روی سرور قرار گیرد، خطای 502.5 را مشاهده خواهید کرد که بیانگر عدم یافت شدن فایل dll اصلی برنامه جهت اجرای آن است. برای رفع آن، برنامه را یکبار publish کنید تا مسیرهایی که با % مشخص شدهاند، با نمونههای واقعی جایگزین شوند و به این صورت، امکان آغاز پروسهی برنامه وجود داشته باشد.
فایل web.config یک برنامهی publish نشدهی ASP.NET Core چنین شکلی را دارد:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" /> </handlers> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/> </system.webServer> </configuration>
۶ سال و ۵ ماه قبل، شنبه ۱۸ فروردین ۱۳۹۷، ساعت ۱۵:۱۸
return Ok نوشته شده محدودیتی ندارد؛ چون در پشت صحنه از کتابخانهی Json.NET استفاده میکند و قادر هست با انواع و اقسام اشیاء پیچیده کار کند و آنها را serialize کند. جهت سادگی کار در اینجا از یک anonymous object استفاده شدهاست. آنرا با هر شیء دیگری و با هر ساختاری که علاقمندید تعویض کنید. هر نوع شیءایی را در اینجا میتوان ذکر و یا اضافه کرد:
public IHttpActionResult Get() { // Anonymous and Weakly-Typed Objects return Ok(new { Id = 1, Title = "Hello from My Protected Controller!", Username = this.User.Identity.Name, Property2 = new Object1 { id = 1, name = "V" } }); // OR ... Strongly-Typed Objects return Ok(model); }
۶ سال و ۵ ماه قبل، جمعه ۱۷ فروردین ۱۳۹۷، ساعت ۱۵:۵۲
یک مثال تکمیلی: چگونه توکنها و همچنین کوکیهای Anti-forgery را توسط یک کلاینت یک غیر وبی به سمت سرور ارسال کنیم.
۶ سال و ۵ ماه قبل، سهشنبه ۱۴ فروردین ۱۳۹۷، ساعت ۱۸:۳۹
حالت پیشفرض اعتبارسنجی آن OnBlur هست. یکبار هم که انجام شد، تا زمانیکه cache آن تغییری نکند، تکرار نمیشود. اگر میخواهید این وضعیت را تغییر دهید، میتوان آنرا دستی هم فعال کرد:
$("#id1").change(function () { // trigger RemoteValidation $('#id1').removeData('previousValue'); //clear cache $('form').validate().element('#id1'); //retrigger remote call // $('#id1').blur(); });
۶ سال و ۵ ماه قبل، یکشنبه ۱۲ فروردین ۱۳۹۷، ساعت ۱۵:۴۲
یک نکتهی تکمیلی: غیر سراسری تعریف کردن یک Model Binder سفارشی
روش استفادهی از options.ModelBinderProviders.Insert، یک Model Binder را به صورت سراسری به کل برنامه اعمال میکند. اگر میخواهید این Binder فقط به یک ViewModel خاص اعمال شود، میتوان به صورت زیر عمل کرد (بدون نیازی به Insert آن در options.ModelBinderProviders):
البته در این حالت Binder تعریف شده نباید دارای پارامتری در سازندهی آن باشد.
روش استفادهی از options.ModelBinderProviders.Insert، یک Model Binder را به صورت سراسری به کل برنامه اعمال میکند. اگر میخواهید این Binder فقط به یک ViewModel خاص اعمال شود، میتوان به صورت زیر عمل کرد (بدون نیازی به Insert آن در options.ModelBinderProviders):
[ModelBinder(BinderType = typeof(PersianDateModelBinder))] public class MyViewModel {
۶ سال و ۵ ماه قبل، یکشنبه ۱۲ فروردین ۱۳۹۷، ساعت ۰۱:۲۳
یک نکتهی تکمیلی در مورد فونتها
عموما فونتها در بستههای اصلی یک چنین مسیرهایی را دارند:
و در حالت پیشفرض این ابزار آنها را به صورت زیر در فایل نهایی تولیدی بازنویسی میکند (بر اساس مسیر نسبی قرارگیری آن در پروژه):
به همین جهت در حین اجرای برنامه پیام یافت نشدن آنها را مشاهده میکنید.
برای غیرفعال کردن این بازنویسی مسیر (بدون نیاز به عمومی کردن مسیر node_modules در کلاس آغازین برنامه)، باید در اینجا adjustRelativePaths را به false تنظیم کنید:
عموما فونتها در بستههای اصلی یک چنین مسیرهایی را دارند:
src: url("../webfonts/fa-brands-400.eot");
src: url("../node_modules/components-font-awesome/webfonts/fa-brands-400.eot");
برای غیرفعال کردن این بازنویسی مسیر (بدون نیاز به عمومی کردن مسیر node_modules در کلاس آغازین برنامه)، باید در اینجا adjustRelativePaths را به false تنظیم کنید:
{ "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "node_modules/bootstrap-rtl/dist/css/bootstrap-rtl.min.css", "node_modules/components-font-awesome/css/fa-solid.min.css", "node_modules/components-font-awesome/css/fontawesome.min.css", "content/custom.css" ], "minify": { "enabled": true, "renameLocals": false, "adjustRelativePaths": false }, "sourceMap": false },
۶ سال و ۵ ماه قبل، شنبه ۱۱ فروردین ۱۳۹۷، ساعت ۲۲:۱۷
یا از ویژگی Route استفاده کنید (در متن بحث شده) یا از ویژگی ActionName:
[ActionName("another_name")] //or [Route("Home/Contact")]
۶ سال و ۵ ماه قبل، شنبه ۱۱ فروردین ۱۳۹۷، ساعت ۱۸:۲۶
یک نکتهی تکمیلی در مورد context.Metadata.IsComplexType
اگر اکشن متد شما یک چنین امضایی را دارد:
هیچگاه model binder تعریف شده سفارشی، بر روی خواص MyViewModel اعمال نخواهد شد؛ از این جهت که ویژگی FromBody، کار پردازش Request Body را مستقلا انجام داده و پس از یکبار پردازش، پردازش مجددی بر روی آن صورت نخواهد گرفت (بدنهی درخواست، یک non-rewindable stream است). به همین جهت دیگر کار به فراخوانی یک Model Binder سفارشی نمیرسد. بنابراین اگر میخواهید Model Binder سفارشی شما بر روی خواص یک شیء نیز اعمال شود، باید ویژگی FromBody را حذف کنید.
البته با حذف FromBody، اطلاعات از یکی از سه منبع زیر خوانده خواهند شد:
- form-URL-encoded body
- route values
- query string
اگر اکشن متد شما یک چنین امضایی را دارد:
public IActionResult Index([FromBody] MyViewModel model) {
البته با حذف FromBody، اطلاعات از یکی از سه منبع زیر خوانده خواهند شد:
- form-URL-encoded body
contentType: 'application/x-www-form-urlencoded; charset=utf-8'
- query string