ASP.NET MVC #5
Source could be also accessed from GitHub -> https://github.com/fpetru/WebApiMongoDB.
Problem / solution format brings an easier understanding on how to build things, giving an immediate feedback. Starting from this idea, the blog post will present step by step how to build
a web application to store your ideas in an easy way, adding text notes, either from desktop or mobile, with few characteristics: run fast, save on the fly whatever you write, and be reasonably reliable and secure.
This blog post will implement just the backend, WebApi and the database access, in the most simple way. Next blog post will cover the front end, using Angular. Then, there will be an additional article on how to increase the performance and the security.
چرا باید اسکالا یاد بگیریم ؟
بانک مبتنی بر گراف
نگاشت JSON به کلاسهای معادل آن
CoffeeScript #12
بخشهای بد
جاوااسکریپت یک زبان پیچیده است که شما برای کار با آن، نیاز است قسمتهایی را که باید از آنها دوری کنید و قسمتهای مهمی را که باید استفاده کنید، بشناسید. همانطور که Sun Tzu گفته "دشمن خود را بشناس"، ما نیز در این قسمت میخواهیم برای شناخت بیشتر قسمتهای تاریک و روشن جاوااسکریپت به آن بپردازیم.
همانطور که در قسمتهای قبل گفته شد، CoffeeScript تنها به یک syntax محدود نمیشود و توانایی برطرف کردن برخی از مشکلات جاوااسکریپت را نیز دارد. با این حال، با توجه به این واقعیت که کدهای CoffeeScript به صورت مستقیم به جاوااسکریپت تبدیل میشوند و نمیتوانند تمامی مشکلاتی را که در جاوااسکریپت وجود دارند، حل کنند، پس برخی از مسائل وجود دارند که شما باید از آنها آگاهی داشته باشید.
اول از قسمتهایی که توسط CoffeeScript حل شدهاند شروع میکنیم.
A JavaScript Subset
with یک دستور بسیار زمانبر است و مضر شناخته شده است و نباید از آن استفاده کنید. with با ایجاد یک ساختار خلاصه نویسی، برای جستجو بر روی خصوصیات اشیاء در نظر گرفته شده بود. برای نمونه به جای نوشتن:
dataObj.users.vahid.email = "info@vmt.ir";
with(dataObj.users.vahid) { email = "info@vmt.ir"; }
همه چیز برای عدم استفاده از with در نظر گرفته شده است. CoffeeScript یک قدم جلوتر از همه برداشته و with را از syntax خود حذف کرده است. به عبارت دیگر در صورتیکه شما از آن استفاده کنید، کامپایلر CoffeeScript خطا صادر میکند.
Global variables
به طور پیش فرض تمامی برنامههای جاوااسکریپت در دامنه global اجرا میشوند و تمامی متغیرهایی که ساخته میشوند به طور پیش فرض در ناحیهی global قرار میگیرند. اگر شما بخواهید متغیری را در ناحیهی local ایجاد کنید، باید از کلمه کلیدی var استفاده کنید.
usersCount = 1; // Global var groupsCount = 2; // Global (function(){ pagesCount = 3; // Global var postsCount = 4; // Local })()
خوشبختانه CoffeeScript به کمک شما میآید و به طور کامل انتساب متغیرهای global را به طور ضمنی از بین میبرد. به عبارت دیگر کلمه کلیدی var در CoffeeScript رزرو شده است و در صورت استفاده خطا صادر میشود.
به صورت پیش فرض به طور ضمنی متغیرها local ایجاد میشوند و خیلی سخت میشود متغیر global ایی را بدون انتساب آن به عنوان خصوصیتی از شیء window ایجاد کرد.
outerScope = true do -> innerScope = true
var outerScope; outerScope = true; (function() { var innerScope; return innerScope = true; })();
package = require('./package') class Test build: -> # Overwrites outer variable! package = @testPackage.compile() testPackage: -> package.create()
class window.Asset constructor: ->
Semicolons
جاوااسکریپت اجباری برای نوشتن ";" ندارد، بنابراین ممکن است یک سری از دستورات از قلم بیافتند. با این حال در پشت صحنهی کامپایلر جاوااسکریپت به ";" احتیاج دارد. به طوری که parser جاوااسکریپت به صورت خودکار هر زمانی که نتواند ارزیابی از دستورات داشته باشد، یک بار دیگر با ";" این کار را انجام میدهد و درصورت موفقیت، پیام خطایی مبنی بر نبود ";" را صادر میکند.متاسفانه این یک ایده بد است. چرا که ممکن است تغییر رفتاری در کد نوشته شده به وجود آید. به مثال زیر توجه کنید. به نظر کد نوشته شده صحیح است؛ درسته؟
function() {} (window.options || {}).property
function() {}(window.options || {}).property
فعالسازی Windows Authentication در IIS
پس از publish برنامه و رعایت مواردی که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS» بحث شد، باید به قسمت Authentication برنامهی مدنظر، در کنسول مدیریتی IIS رجوع کرد:
و سپس Windows Authentication را با کلیک راست بر روی آن و انتخاب گزینهی Enable، فعال نمود:
این تنظیم دقیقا معادل افزودن تنظیمات ذیل به فایل web.config برنامه است:
<system.webServer> <security> <authentication> <anonymousAuthentication enabled="true" /> <windowsAuthentication enabled="true" /> </authentication> </security> </system.webServer>
فعالسازی Windows Authentication در IIS Express
اگر برای آزمایش میخواهید از IIS Express به همراه ویژوال استودیو استفاده کنید، نیاز است فایلی را به نام Properties\launchSettings.json با محتوای ذیل در ریشهی پروژهی خود ایجاد کنید (و یا تغییر دهید):
{ "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:3381/", "sslPort": 0 } } }
تغییر مهم فایل web.config برنامه جهت هدایت اطلاعات ویندوز به آن
اگر پروژهی شما فایل web.config ندارد، باید آنرا اضافه کنید؛ با حداقل محتوای ذیل:
<?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="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/> </system.webServer> </configuration>
تنظیمات برنامهی ASP.NET Core جهت فعالسازی Windows Authentication
پس از فعالسازی windowsAuthentication در IIS و همچنین تنظیم forwardWindowsAuthToken به true در فایل web.config برنامه، اکنون جهت استفادهی از windowsAuthentication دو روش وجود دارد:
الف) تنظیمات مخصوص برنامههای Self host
اگر برنامهی وب شما قرار است به صورت self host ارائه شود (بدون استفاده از IIS)، تنها کافی است در تنظیمات ابتدای برنامه در فایل Program.cs، استفادهی از میانافزار HttpSys را ذکر کنید:
namespace ASPNETCore2WindowsAuthentication { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup<Startup>() .UseHttpSys(options => // Just for local tests without IIS, Or self-hosted scenarios on Windows ... { options.Authentication.Schemes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM; options.Authentication.AllowAnonymous = true; //options.UrlPrefixes.Add("http://+:80/"); }) .Build(); host.Run(); } } }
ب) تنظیمات مخصوص برنامههایی که قرار است در IIS هاست شوند
در اینحالت تنها کافی است UseIISIntegration در تنظیمات ابتدایی برنامه ذکر شود و همانطور که عنوان شد، نیازی به UseHttpSys در این حالت نیست:
namespace ASPNETCore2WindowsAuthentication { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) .UseStartup<Startup>() .Build(); host.Run(); } } }
فعالسازی میانافزار اعتبارسنجی ASP.NET Core جهت یکپارچه شدن با Windows Authentication
در پایان تنظیمات فعالسازی Windows Authentication نیاز است به فایل Startup.cs برنامه مراجعه کرد و یکبار AddAuthentication را به همراه تنظیم ChallengeScheme آن به IISDefaults افزود:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.Configure<IISOptions>(options => { // Sets the HttpContext.User // Note: Windows Authentication must also be enabled in IIS for this to work. options.AutomaticAuthentication = true; options.ForwardClientCertificate = true; }); services.AddAuthentication(options => { // for both windows and anonymous authentication options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme; }); }
آزمایش برنامه با تدارک یک کنترلر محافظت شده
در اینجا قصد داریم اطلاعات ذیل را توسط تعدادی اکشن متد، نمایش دهیم:
private string authInfo() { var claims = new StringBuilder(); if (User.Identity is ClaimsIdentity claimsIdentity) { claims.Append("Your claims: \n"); foreach (var claim in claimsIdentity.Claims) { claims.Append(claim.Type + ", "); claims.Append(claim.Value + "\n"); } } return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}"; }
namespace ASPNETCore2WindowsAuthentication.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } [Authorize] public IActionResult Windows() { return Content(authInfo()); } private string authInfo() { var claims = new StringBuilder(); if (User.Identity is ClaimsIdentity claimsIdentity) { claims.Append("Your claims: \n"); foreach (var claim in claimsIdentity.Claims) { claims.Append(claim.Type + ", "); claims.Append(claim.Value + "\n"); } } return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}"; } [AllowAnonymous] public IActionResult Anonymous() { return Content(authInfo()); } [Authorize(Roles = "Domain Admins")] public IActionResult ForAdmins() { return Content(authInfo()); } [Authorize(Roles = "Domain Users")] public IActionResult ForUsers() { return Content(authInfo()); } } }
dotnet publish
اکنون اگر برنامه را در مرورگر مشاهده کنیم، یک چنین خروجی قابل دریافت است:
در اینجا نام کاربر وارد شدهی به ویندوز و همچنین لیست تمام Claims او مشاهده میشوند. مسیر Home/Windows نیز توسط ویژگی Authorize محافظت شدهاست.
برای محدود کردن دسترسی کاربران به اکشن متدها، توسط گروههای دومین و اکتیودایرکتوری، میتوان به نحو ذیل عمل کرد:
[Authorize(Roles = @"<domain>\<group>")] //or [Authorize(Roles = @"<domain>\<group1>,<domain>\<group2>")]
services.AddAuthorization(options => { options.AddPolicy("RequireWindowsGroupMembership", policy => { policy.RequireAuthenticatedUser(); policy.RequireRole(@"<domain>\<group>")); } });
[Authorize(Policy = "RequireWindowsGroupMembership")]
[HttpGet("[action]")] public IActionResult SomeValue() { if (!User.IsInRole(@"Domain\Group")) return StatusCode(403); return Ok("Some Value"); }
افزودن Claims سفارشی به Claims پیشفرض کاربر سیستم
همانطور که در شکل فوق ملاحظه میکنید، یک سری Claims حاصل از Windows Authentication در اینجا به شیء User اضافه شدهاند؛ بدون اینکه برنامه، صفحهی لاگینی داشته باشد و همینقدر که کاربر به ویندوز وارد شدهاست، میتواند از برنامه استفاده کند.
اگر نیاز باشد تا Claims خاصی به لیست Claims کاربر جاری اضافه شود، میتوان از پیاده سازی یک IClaimsTransformation سفارشی استفاده کرد:
public class ApplicationClaimsTransformation : IClaimsTransformation { private readonly ILogger<ApplicationClaimsTransformation> _logger; public ApplicationClaimsTransformation(ILogger<ApplicationClaimsTransformation> logger) { _logger = logger; } public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { if (!(principal.Identity is ClaimsIdentity identity)) { return Task.FromResult(principal); } var claims = addExistingUserClaims(identity); identity.AddClaims(claims); return Task.FromResult(principal); } private IEnumerable<Claim> addExistingUserClaims(IIdentity identity) { var claims = new List<Claim>(); var user = @"VahidPC\Vahid"; if (identity.Name != user) { _logger.LogError($"Couldn't find {identity.Name}."); return claims; } claims.Add(new Claim(ClaimTypes.GivenName, user)); return claims; } }
services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>(); services.AddAuthentication(options => { // for both windows and anonymous authentication options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme; });
به این ترتیب میتوان لیست Claims ثبت شدهی یک کاربر را در یک بانک اطلاعاتی استخراج و به لیست Claims فعلی آن افزود و دسترسیهای بیشتری را به او اعطاء کرد (فراتر از دسترسیهای پیشفرض سیستم عامل).
برای دسترسی به مقادیر این Claims نیز میتوان به صورت ذیل عمل کرد:
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var userName = User.FindFirstValue(ClaimTypes.Name); var userName = User.FindFirstValue(ClaimTypes.GivenName);
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید: ASPNETCore2WindowsAuthentication.zip
همان طور که ممکن است بدانید، اکشن متدها در کنترلرهای MVC میتوانند انواع مختلفی را برگشت دهند که شرح آن در مطالب این سایت به مفصل گذشته است. یکی از این انواع، نوع ActionResult میباشد. این یک کلاس پایه برای انواع برگشتی توسط اکشن متدها مثل JsonResult، FileResult میباشد. (اطلاعات بیشتر را اینجا بخوانید) اما ممکن است مواقعی پیش بیاید که بخواهید نوعی را توسط یک اکشن متد برگشت دهید که به صورت توکار تعریف نشده باشد. مثلا زمانی را در نظر بگیرید که بخواهید یک تصویر امنیتی را برگشت دهید. یکی از راه حلهای ممکن به این صورت است که کلاسی ایجاد شود که از کلاس پایه ActionResult ارث بری کرده باشد. بدین صورت:
using System; using System.Web.Mvc; namespace MVCPersianCaptcha.Models { public class CaptchaImageResult : ActionResult { public override void ExecuteResult(ControllerContext context) { throw new NotImplementedException(); } } }
کدهای اولیه برای ایجاد یک تصویر امنیتی به صورت خیلی ساده از کلاسهای فراهم شده توسط +GDI ، که در دات نت فریمورک وجود دارند استفاده خواهند کرد. برای این کار ابتدا یک شیء از کلاس Bitmap با دستور زیر ایجاد خواهیم کرد:
// Create a new 32-bit bitmap image. Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
سپس شیئی از نوع Graphics برای انجام عملیات ترسیم نوشتههای فارسی روی شیء bitmap ساخته میشود:
// Create a graphics object for drawing. Graphics gfxCaptchaImage = Graphics.FromImage(bitmap);
gfxCaptchaImage.PageUnit = GraphicsUnit.Pixel; gfxCaptchaImage.SmoothingMode = SmoothingMode.HighQuality; gfxCaptchaImage.Clear(Color.White);
سپس یک عدد اتفاقی بین 1000 و 9999 با دستور زیر تولید میشود:
// Create a Random Number from 1000 to 9999 int salt = CaptchaHelpers.CreateSalt();
public int CreateSalt() { Random random = new Random(); return random.Next(1000, 9999); }
HttpContext.Current.Session["captchastring"] = salt;
string randomString = (salt).NumberToText(Language.Persian);
به صورت پیش فرض نوشتههای تصویر امنیتی به صورت چپ چین نوشته خواهند شد، و با توجه به این که نوشته ای که باید در تصویر امنیتی قرار بگیرد فارسی است، پس بهتر است آنرا به صورت راست به چپ در تصویر بنویسیم، بدین صورت:
// Set up the text format. var format = new StringFormat(); int faLCID = new System.Globalization.CultureInfo("fa-IR").LCID; format.SetDigitSubstitution(faLCID, StringDigitSubstitute.National); format.Alignment = StringAlignment.Near; format.LineAlignment = StringAlignment.Near; format.FormatFlags = StringFormatFlags.DirectionRightToLeft;
// Font of Captcha and its size Font font = new Font("Tahoma", 10);
// Create a path for text GraphicsPath path = new GraphicsPath();
path.AddString(randomString, font.FontFamily, (int)font.Style, (gfxCaptchaImage.DpiY * font.SizeInPoints / 72), new Rectangle(0, 0, width, height), format);
gfxCaptchaImage.DrawPath(Pens.Navy, path);
//-- using a sin ware distort the image int distortion = random.Next(-10, 10); using (Bitmap copy = (Bitmap)bitmap.Clone()) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int newX = (int)(x + (distortion * Math.Sin(Math.PI * y / 64.0))); int newY = (int)(y + (distortion * Math.Cos(Math.PI * x / 64.0))); if (newX < 0 || newX >= width) newX = 0; if (newY < 0 || newY >= height) newY = 0; bitmap.SetPixel(x, y, copy.GetPixel(newX, newY)); } } }
//-- Draw the graphic to the bitmap gfxCaptchaImage.DrawImage(bitmap, new Point(0, 0)); gfxCaptchaImage.Flush();
HttpResponseBase response = context.HttpContext.Response; response.ContentType = "image/jpeg"; bitmap.Save(response.OutputStream, ImageFormat.Jpeg);
// Clean up. font.Dispose(); gfxCaptchaImage.Dispose(); bitmap.Dispose();
public CaptchaImageResult CaptchaImage() { return new CaptchaImageResult(); }
<img src="@Url.Action("CaptchaImage")"/>
[HttpPost] public ActionResult Index(LogOnModel model) { if (!ModelState.IsValid) return View(model); if (model.CaptchaInputText == Session["captchastring"].ToString()) TempData["message"] = "تصویر امنتی را صحیح وارد کرده اید"; else TempData["message"] = "تصویر امنیتی را اشتباه وارد کرده اید"; return View(); }
کدهای کامل مربوط به این مطلب را به همراه یک مثال از لینک زیر دریافت نمائید:
MVC-Persian-Captcha
در این مقاله به بررسی دو الگوریتم عمده GC در JavaScript میپردازیم.
1. مدل Reference Counting Garbage Collectorدر این مدل از جمع آوری زباله، به ازای ایجاد هر آبجکت در حافظه و یا هر تخصیصی در حافظه، شمارشگری با عنوان reference counter در نظر گرفته میشود. هر زمان که به این آبجکت یا حافظه تخصیصی دسترسی ایجاد شود و یا reference داده شود، یک واحد به شمارشگر آن اضافه و هر وقت که رفرنس به حافظه یا آبجکت دیگر مورد استفاده نداشت یا از دسترس خارج شد، یک واحد از شمارشگر آن کاسته میشود. این مدل که سریعترین، سادهترین و کم سربارترین مدل GC میباشد، وقتی شمارشگر رفرنس حافظه به صفر رسید، حافظه ومنابع سیستم تخصیصی به آن آبجکت آزاد شده و آماده استفاده مجدد میشودبه عنوان نمونه به کد زیر دقت کنید:
var object1='GC test object 1'; function Test1(){ var object2='GC test object 2'; alert (object1+'-' + object2); } alert (object1);
پس از اجرای این کد، جدولی مانند زیر در GC ایجاد میشود که به صورت زیر مقدار طی اجرای برنامه مقدار دهی میشود:
Object | Reference Counter Line 1 | Reference Counter Line 3 | Reference Counter Line 5 | Reference Counter Line 6 | Reference Counter End Program |
object1 | 1 | 2 | 1 | 1 | 0 |
object2 | - | 1 | 0 | 0 | 0 |
این مدل که در مرورگرهای قدیمی مورد استفاده قرار گرفته است، در صورتی که دو آبجکت به یکدیگر ارجاع داشته باشند، reference counter آن صفر نشده، حافظه و منابع تخصیصی آنها آزاد نمیشود و احتمال ایجاد نشت حافظه زیاد میشود.
2. مدل Mark-and-Sweepدر این مدل از مدیریت حافظه، برای آبجکتهای ایجادی در حافظه،GC درخت ارجاعات ایجاد کرده و دقیقا مشخص میکند زمانی که یک آبجکت در دسترس نباشد و یا دیگر نیازی به آن نباشد، آن را از حافظه حذف میکند. مانند شکل زیر:
در این صورت هنگامی که آبجکت دیگر واقعا مورد نیاز نباشد از حافظه حذف میشود. یعنی اگر دو آبجکت به یکدیگر نیز ارجاع داشته باشند هنگامی که دیگر مورد استفاده قرار نگیرند حذف شده و امکان ایجاد نشت حافظه به حداقل میرسد.
تفاوت عمده بین GC در Javascript و GC در CLR این است که در زبانهای مبتنی بر .NET شما میتوانید به صورت مستقیم GC را صدا زده تا عمل جمع آوری زباله انجام پذیرد ولی در JavaScript هر زمان که نیاز به حافظه بیشتر باشد (و یا در یک زمانبندی مشخص) عمل جمع آوری زباله انجام شده و از طریق کد قابل فراخوانی نمیباشد.