Full Stack ASP.NET Core 2.0 MVC Forum Build
Topics Covered:
- Setting up a new ASP .NET Core 2.0 MVC web application with Identity user authentication in Visual Studio
- Separating Web, Services, and Data Access Layers in our solution
- Setting up tests with NUnit and .NET Core virtual in-memory database
- Debugging / Fixing bugs
- Implementing the MVC (Model-View-Controller) pattern
- Dependency Injection of Services into our Controllers
- Using input forms to pass data from our Views to our Controllers
- Azure file storage for Profile Image uploads
- Azure SQL database hosting
- SQL Database seeding for starting the application with a super-user
- Code-first database migrations
- Writing SQL queries to inspect data in our database
- Deploying the application to Azure.
افزودن وابستگیهای MailKit به برنامه
برای شروع به استفادهی از MailKit، میتوان بستهی نیوگت آنرا به فایل project.json برنامه معرفی کرد:
{ "dependencies": { "MailKit": "1.10.0" } }
استفاده از MailKit جهت تکمیل وابستگیهای ASP.NET Core Identity
قسمتی از ASP.NET Core Identity، شامل ارسال ایمیلهای «ایمیل خود را تائید کنید» است که آنرا میتوان توسط MailKit به نحو ذیل تکمیل کرد:
using System.Threading.Tasks; using ASPNETCoreIdentitySample.Services.Contracts.Identity; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; namespace ASPNETCoreIdentitySample.Services.Identity { public class AuthMessageSender : IEmailSender, ISmsSender { public async Task SendEmailAsync(string email, string subject, string message) { var emailMessage = new MimeMessage(); emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info")); emailMessage.To.Add(new MailboxAddress("", email)); emailMessage.Subject = subject; emailMessage.Body = new TextPart(TextFormat.Html) { Text = message }; using (var client = new SmtpClient()) { client.LocalDomain = "dotnettips.info"; await client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false); await client.SendAsync(emailMessage).ConfigureAwait(false); await client.DisconnectAsync(true).ConfigureAwait(false); } } public Task SendSmsAsync(string number, string message) { // Plug in your SMS service here to send a text message. return Task.FromResult(0); } } }
در آخر، این پیام به SmtpClient جهت ارسال نهایی، فرستاده میشود. این SmtpClient هرچند هم نام مشابه آن در System.Net.Mail است اما با آن یکی نیست و متعلق است به MailKit. در اینجا ابتدا LocalDomain تنظیم شدهاست. تنظیم این مورد اختیاری بوده و صرفا به SMTP سرور دریافت کنندهی ایمیلها، مرتبط است که آیا قید آنرا اجباری کردهاست یا خیر. تنظیمات اصلی SMTP Server در متد ConnectAsync ذکر میشوند که شامل مقادیر host ،port و پروتکل ارسالی هستند.
ارسال ایمیل به SMTP pickup folder
روشی که تا به اینجا بررسی شد، جهت ارسال ایمیلها به یک SMTP Server واقعی کاربرد دارد. اما در حین توسعهی محلی برنامه میتوان ایمیلها را در داخل یک پوشهی موقتی ذخیره و آنها را توسط برنامهی Outlook (و یا حتی مرورگر Firefox) بررسی و بازبینی کامل کرد.
در این حالت تنها کاری را که باید انجام داد، جایگزین کردن قسمت ارسال ایمیل واقعی توسط SmtpClient در کدهای فوق، با قطعه کد ذیل است:
using (var stream = new FileStream($@"c:\smtppickup\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew)) { emailMessage.WriteTo(stream); }
FAQ و منبع تکمیلی
فعالسازی تولید خودکار بستههای نیوگت در پروژههای NET Core.
پس از تهیهی یک کتابخانهی مبتنی بر NET Core.، تنها کاری که در جهت تولید خودکار بستههای نیوگت باید انجام شود، افزودن مدخل postcompile ذیل به فایل project.json است:
"scripts": { "postcompile": [ "dotnet pack --no-build --configuration %compile:Configuration%" ] }
در اینحالت اگر فایل nupkg تولیدی را توسط برنامههای zip باز کنید، مشاهده خواهید کرد که فایل nuspec خودکاری نیز در آن درج شدهاست؛ اما ... مشخصات ثبت شدهی در آن ناقص هستند و شامل مواردی مانند نام پروژه، نام نویسنده، مجوز استفادهی از پروژه، آدرس پروژه و امثال آنها نیستند. در نگارشهای دیگر دات نت، این مشخصات از فایل nuspec تهیه شدهی توسط ما جمع آوری و درج میشود. اما در اینجا خیر.
تکمیل فایل project.json برای درج مشخصات پروژه و تکمیل اطلاعات فایل nuspec
هرچند به ظاهر دیگر فایل nuspec دستی تهیه شده در اینجا پردازش نمیشود، اما تمام اطلاعات آنرا در فایل project.json نیز میتوان درج کرد:
{ "version": "1.1.1.0", "authors": [ "Vahid Nasiri" ], "packOptions": { "owners": [ "Vahid Nasiri" ], "tags": [ "PdfReport", "Excel", "Export", "iTextSharp", "PDF", "Report", "Reporting", "Persian", ".NET Core" ], "licenseUrl": "http://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html", "projectUrl": "https://github.com/VahidN/iTextSharp.LGPLv2.Core" }, "description": " iTextSharp.LGPLv2.Core is an unofficial port of the last LGPL version of the iTextSharp (V4.1.6) to .NET Core.", "scripts": { "postcompile": [ "dotnet pack --no-build --configuration %compile:Configuration%" ] } }
تکمیل تنظیمات Build پروژه
بهتر است کتابخانههای خود را در حالت release و همچنین بهینه سازی شده، توزیع کنید. به همین منظور نیاز است مدخل ذیل را نیز به فایل project.json اضافه کرد:
"configurations": { "Release": { "buildOptions": { "optimize": true, "platform": "anycpu" } } },
افزودن مستندات XML ایی کتابخانه
به احتمال زیاد XML-Docهای هر متد (کامنتهای مخصوص دات نتی هر متد یا خاصیت عمومی) را نیز به کدهای خود افزودهاید. برای اینکه فایل XML نهایی آن به صورت خودکار تولید شده و همچنین در بستهی نیوگت نهایی درج شود، نیاز است مدخل xmlDoc را به buildOptions اضافه کنید:
"buildOptions": { "xmlDoc": true },
"buildOptions": { "xmlDoc": true, "nowarn": [ "1591" ] // 1591: missing xml comment for publicly visible type or member },
برای مطالعهی بیشتر
project.json reference
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه میتوانیم یک چنین حملاتی را مشکلتر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکنها و یا کوکیهای سرقت شده، در برنامههای مبتنی بر ASP.NET Core بررسی خواهیم کرد.
توسعهی یک سرویس تشخیص مرورگر و سیستم عامل شخص وارد شدهی به سیستم
یکی از روشهای غیرممکن کردن یک چنین حملاتی، درج مشخصات سیستم عامل و مرورگر شخص وارد شدهی به سیستم، در کوکی و همچنین توکن صادر شدهی حاصل از اعتبارسنجی موفق است. سپس زمانیکه قرار است از اطلاعات این کوکی و یا توکن در برنامه استفاده شود، این اطلاعات را با اطلاعات درخواست جاری کاربر مقایسه کرده و در صورت عدم تطابق، درخواست او را برگشت میزنیم. برای مثال اگر عملیات لاگین، در ویندوز انجام شده و اکنون توکن و یا کوکی حاصل، در سیستم عامل اندروید در حاصل استفادهاست، یعنی ... این عملیات مشکوک است و باید خاتمه یابد و کاربر باید مجبور به لاگین مجدد شود و نه اعتبارسنجی خودکار بدون زحمت!
برای این منظور میتوان از کتابخانهی UA-Parser استفاده کرد و توسط آن سرویس زیر را توسعه داد:
using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; using UAParser; namespace ASPNETCore2JwtAuthentication.Services; /// <summary> /// To invalidate an old user's token from a new device /// </summary> public class DeviceDetectionService : IDeviceDetectionService { private readonly IHttpContextAccessor _httpContextAccessor; private readonly ISecurityService _securityService; public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor) { _securityService = securityService ?? throw new ArgumentNullException(nameof(securityService)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext); public string GetDeviceDetails(HttpContext context) { var ua = GetUserAgent(context); if (ua is null) { return "unknown"; } var client = Parser.GetDefault().Parse(ua); var deviceInfo = client.Device.Family; var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}"; var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}"; //TODO: Add the user's IP address here, if it's a banking system. return $"{deviceInfo}, {browserInfo}, {osInfo}"; } public string GetDeviceDetailsHash(HttpContext context) => _securityService.GetSha256Hash(GetDeviceDetails(context)); public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext); public string GetCurrentUserTokenDeviceDetailsHash() => GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity); public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity) { if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any()) { return null; } return claimsIdentity.FindFirst(ClaimTypes.System)?.Value; } public bool HasCurrentUserTokenValidDeviceDetails() => HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity); public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) => string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity), StringComparison.Ordinal); private static string GetUserAgent(HttpContext context) { if (context is null) { return null; } return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent) ? userAgent.ToString() : null; } }
اصل کار این سرویس در متد زیر رخ میدهد:
public string GetDeviceDetails(HttpContext context) { var ua = GetUserAgent(context); if (ua is null) { return "unknown"; } var client = Parser.GetDefault().Parse(ua); var deviceInfo = client.Device.Family; var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}"; var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}"; //TODO: Add the user's IP address here, if it's a banking system. return $"{deviceInfo}, {browserInfo}, {osInfo}"; }
اضافه کردن اطلاعات مشخصات دستگاه کاربر به کوکی و یا توکن او
همانطور که عنوان شد، در متد HasUserTokenValidDeviceDetails، ابتدا مشخصات دستگاه موجود در کوکی و یا توکن دریافتی، استخراج میشود. به همین جهت نیاز است این مشخصات را دقیقا در حین لاگین موفق، به صورت یک Claim جدید، برای مثال از نوع ClaimTypes.System به مجموعهی Claims کاربر اضافه کرد:
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(), ClaimValueTypes.String, _configuration.Value.Issuer),
- نمونهی انجام اینکار در یک برنامهی تولید کنندهی JWT
- نمونهی انجام اینکار در یک برنامهی تولید کنندهی کوکی
یکپارچه کردن DeviceDetectionService با اعتبارسنجهای کوکیها و توکنها
پس از افزودن مشخصات سیستم کاربر وارد شدهی به سیستم، به صورت یک Claim جدید به توکنها، روش اعتبارسنجی اطلاعات موجود در توکن رسیده، در رخداد گردان OnTokenValidated است که امکان دسترسی به HttpContext و محتوای توکن را میسر میکند:
.AddJwtBearer(cfg => { cfg.Events = new JwtBearerEvents { OnTokenValidated = context => { var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>(); return tokenValidatorService.ValidateAsync(context); }, }; });
.AddCookie(options => { options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = context => { var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>(); return cookieValidatorService.ValidateAsync(context); } }; });
- نمونهی کامل انجام اینکار در یک برنامهی تولید کنندهی JWT
- نمونهی کامل انجام اینکار در یک برنامهی تولید کنندهی کوکی
در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامهی تولید کنندهی JWT در اینجا و برای یک برنامهی مبتنی بر کوکیها در اینجا میتوانید مشاهده کنید.