Datepicker برای Bootstrap
فشرده سازی HTML
جهت نگهداری بعضی از اطلاعات در صفحات کاربر، از فیلدهای مخفی ( Hidden Inputs ) استفاده میکنیم. مشکلی که در این روش وجود دارد این است که اگر این اطلاعات مهم باشند (مانند کلیدها) کاربر میتواند توسط ابزارهایی این اطلاعات را تغییر دهد و این مورد مسئلهای خطرناک میباشد.
راه حل رفع این مسئلهی امنیتی، استفاده از یک Html Helper جهت رمزنگاری این فیلد مخفی در مرورگر کاربر و رمز گشایی آن هنگام Post شدن سمت سرور میباشد.
برای رسیدن به این هدف یک Controller Factory ( Understanding and Extending Controller Factory in MVC ) سفارشی را جهت دستیابی به مقادیر فرم ارسالی، قبل از استفاده در Actionها و به همراه کلاسهای زیر ایجاد کردیم.
کلاس EncryptSettingsProvider :public interface IEncryptSettingsProvider { byte[] EncryptionKey { get; } string EncryptionPrefix { get; } } public class EncryptSettingsProvider : IEncryptSettingsProvider { private readonly string _encryptionPrefix; private readonly byte[] _encryptionKey; public EncryptSettingsProvider() { //read settings from configuration var useHashingString = ConfigurationManager.AppSettings["UseHashingForEncryption"]; var useHashing = System.String.Compare(useHashingString, "false", System.StringComparison.OrdinalIgnoreCase) != 0; _encryptionPrefix = ConfigurationManager.AppSettings["EncryptionPrefix"]; if (string.IsNullOrWhiteSpace(_encryptionPrefix)) { _encryptionPrefix = "encryptedHidden_"; } var key = ConfigurationManager.AppSettings["EncryptionKey"]; if (useHashing) { var hash = new SHA256Managed(); _encryptionKey = hash.ComputeHash(Encoding.UTF8.GetBytes(key)); hash.Clear(); hash.Dispose(); } else { _encryptionKey = Encoding.UTF8.GetBytes(key); } } #region ISettingsProvider Members public byte[] EncryptionKey { get { return _encryptionKey; } } public string EncryptionPrefix { get { return _encryptionPrefix; } } #endregion }
EncryptionKey : کلید رمز نگاری میباشد و در فایل Config برنامه ذخیره میباشد.
EncryptionPrefix : پیشوند نام Hidden فیلدها میباشد، این پیشوند برای یافتن Hidden فیلد هایی که رمزنگاری شده اند استفاده میشود. میتوان این فیلد را در فایل Config برنامه ذخیره کرد.
<appSettings> <add key="EncryptionKey" value="asdjahsdkhaksj dkashdkhak sdhkahsdkha kjsdhkasd"/> </appSettings>
کلاس RijndaelStringEncrypter :
public interface IRijndaelStringEncrypter : IDisposable { string Encrypt(string value); string Decrypt(string value); } public class RijndaelStringEncrypter : IRijndaelStringEncrypter { private RijndaelManaged _encryptionProvider; private ICryptoTransform _cryptoTransform; private readonly byte[] _key; private readonly byte[] _iv; public RijndaelStringEncrypter(IEncryptSettingsProvider settings, string key) { _encryptionProvider = new RijndaelManaged(); var keyBytes = Encoding.UTF8.GetBytes(key); var derivedbytes = new Rfc2898DeriveBytes(settings.EncryptionKey, keyBytes, 3); _key = derivedbytes.GetBytes(_encryptionProvider.KeySize / 8); _iv = derivedbytes.GetBytes(_encryptionProvider.BlockSize / 8); } #region IEncryptString Members public string Encrypt(string value) { var valueBytes = Encoding.UTF8.GetBytes(value); if (_cryptoTransform == null) { _cryptoTransform = _encryptionProvider.CreateEncryptor(_key, _iv); } var encryptedBytes = _cryptoTransform.TransformFinalBlock(valueBytes, 0, valueBytes.Length); var encrypted = Convert.ToBase64String(encryptedBytes); return encrypted; } public string Decrypt(string value) { var valueBytes = Convert.FromBase64String(value); if (_cryptoTransform == null) { _cryptoTransform = _encryptionProvider.CreateDecryptor(_key, _iv); } var decryptedBytes = _cryptoTransform.TransformFinalBlock(valueBytes, 0, valueBytes.Length); var decrypted = Encoding.UTF8.GetString(decryptedBytes); return decrypted; } #endregion #region IDisposable Members public void Dispose() { if (_cryptoTransform != null) { _cryptoTransform.Dispose(); _cryptoTransform = null; } if (_encryptionProvider != null) { _encryptionProvider.Clear(); _encryptionProvider.Dispose(); _encryptionProvider = null; } } #endregion }
Rijndael :Represents the base class from which all implementations of the Rijndael symmetric encryption algorithm must inherit
متغیر key در سازنده کلاس کلیدی جهت رمزنگاری و رمزگشایی میباشد. این کلید میتواند AntiForgeryToken تولیدی در View ها و یا کلیدی باشد که در سیستم خودمان ذخیره سازی میکنیم.
در این پروژه از کلید سیستم خودمان استفاده میکنیم.
کلاس ActionKey :
public class ActionKey { public string Area { get; set; } public string Controller { get; set; } public string Action { get; set; } public string ActionKeyValue { get; set; } }
در اینجا هر View که بخواهد از این فیلد رمزنگاری شده استفاده کند بایستی دارای کلیدی در سیستم باشد.مدل متناظر مورد استفاده را مشاهده مینمایید. در این مدل، ActionKeyValue کلیدی جهت رمزنگاری این فیلد مخفی میباشد.
کلاس ActionKeyService :
/// <summary> /// پیدا کردن کلید متناظر هر ویو.ایجاد کلید جدید در صورت عدم وجود کلید در سیستم /// </summary> /// <param name="action"></param> /// <param name="controller"></param> /// <param name="area"></param> /// <returns></returns> string GetActionKey(string action, string controller, string area = ""); } public class ActionKeyService : IActionKeyService { private static readonly IList<ActionKey> ActionKeys; static ActionKeyService() { ActionKeys = new List<ActionKey> { new ActionKey { Area = "", Controller = "Product", Action = "dit", ActionKeyValue = "E702E4C2-A3B9-446A-912F-8DAC6B0444BC", } }; } /// <summary> /// پیدا کردن کلید متناظر هر ویو.ایجاد کلید جدید در صورت عدم وجود کلید در سیستم /// </summary> /// <param name="action"></param> /// <param name="controller"></param> /// <param name="area"></param> /// <returns></returns> public string GetActionKey(string action, string controller, string area = "") { area = area ?? ""; var actionKey= ActionKeys.FirstOrDefault(a => a.Action.ToLower() == action.ToLower() && a.Controller.ToLower() == controller.ToLower() && a.Area.ToLower() == area.ToLower()); return actionKey != null ? actionKey.ActionKeyValue : AddActionKey(action, controller, area); } /// <summary> /// اضافه کردن کلید جدید به سیستم /// </summary> /// <param name="action"></param> /// <param name="controller"></param> /// <param name="area"></param> /// <returns></returns> private string AddActionKey(string action, string controller, string area = "") { var actionKey = new ActionKey { Action = action, Controller = controller, Area = area, ActionKeyValue = Guid.NewGuid().ToString() }; ActionKeys.Add(actionKey); return actionKey.ActionKeyValue; } }
جهت بازیابی کلید هر View میباشد. در متد GetActionKey ابتدا بدنبال کلید View درخواستی در منبعی از ActionKeyها میگردیم. اگر این کلید یافت نشد کلیدی برای آن ایجاد میکنیم و نیازی به مقدار دهی آن نمیباشد.
کلاس MvcHtmlHelperExtentions :
public static class MvcHtmlHelperExtentions { public static string GetActionKey(this System.Web.Routing.RequestContext requestContext) { IActionKeyService actionKeyService = new ActionKeyService(); var action = requestContext.RouteData.Values["Action"].ToString(); var controller = requestContext.RouteData.Values["Controller"].ToString(); var area = requestContext.RouteData.Values["Area"]; var actionKeyValue = actionKeyService.GetActionKey( action, controller, area != null ? area.ToString() : null); return actionKeyValue; } public static string GetActionKey(this HtmlHelper helper) { IActionKeyService actionKeyService = new ActionKeyService(); var action = helper.ViewContext.RouteData.Values["Action"].ToString(); var controller = helper.ViewContext.RouteData.Values["Controller"].ToString(); var area = helper.ViewContext.RouteData.Values["Area"]; var actionKeyValue = actionKeyService.GetActionKey( action, controller, area != null ? area.ToString() : null); return actionKeyValue; } }
public static string GetActionKey(this System.Web.Routing.RequestContext requestContext)
public static string GetActionKey(this HtmlHelper helper)
کلاس InputExtensions :
public static class InputExtensions { public static MvcHtmlString EncryptedHidden(this HtmlHelper helper, string name, object value) { if (value == null) { value = string.Empty; } var strValue = value.ToString(); IEncryptSettingsProvider settings = new EncryptSettingsProvider(); var encrypter = new RijndaelStringEncrypter(settings, helper.GetActionKey()); var encryptedValue = encrypter.Encrypt(strValue); encrypter.Dispose(); var encodedValue = helper.Encode(encryptedValue); var newName = string.Concat(settings.EncryptionPrefix, name); return helper.Hidden(newName, encodedValue); } public static MvcHtmlString EncryptedHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) { var name = ExpressionHelper.GetExpressionText(expression); var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); return EncryptedHidden(htmlHelper, name, metadata.Model); } }
@Html.EncryptedHiddenFor(model => model.Id) @Html.EncryptedHidden("Id2","2")
public class DecryptingControllerFactory : DefaultControllerFactory { private readonly IEncryptSettingsProvider _settings; public DecryptingControllerFactory() { _settings = new EncryptSettingsProvider(); } public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) { var parameters = requestContext.HttpContext.Request.Params; var encryptedParamKeys = parameters.AllKeys.Where(x => x.StartsWith(_settings.EncryptionPrefix)).ToList(); IRijndaelStringEncrypter decrypter = null; foreach (var key in encryptedParamKeys) { if (decrypter == null) { decrypter = GetDecrypter(requestContext); } var oldKey = key.Replace(_settings.EncryptionPrefix, string.Empty); var oldValue = decrypter.Decrypt(parameters[key]); if (requestContext.RouteData.Values[oldKey] != null) { if (requestContext.RouteData.Values[oldKey].ToString() != oldValue) throw new ApplicationException("Form values is modified!"); } requestContext.RouteData.Values[oldKey] = oldValue; } if (decrypter != null) { decrypter.Dispose(); } return base.CreateController(requestContext, controllerName); } private IRijndaelStringEncrypter GetDecrypter(System.Web.Routing.RequestContext requestContext) { var decrypter = new RijndaelStringEncrypter(_settings, requestContext.GetActionKey()); return decrypter; } }
این قسمت از کد
if (requestContext.RouteData.Values[oldKey] != null) { if (requestContext.RouteData.Values[oldKey].ToString() != oldValue) throw new ApplicationException("Form values is modified!"); }
همچنین بایستی این Controller Factory را در Application_Start فایل global.asax.cs برنامه اضافه نماییم.
protected void Application_Start() { .... ControllerBuilder.Current.SetControllerFactory(typeof(DecryptingControllerFactory)); }
کدهای پروژهی جاری
TestHiddenEncrypt.7z
*در تکمیل این مقاله میتوان SessionId کاربر یا AntyForgeryToken تولیدی در View را نیز در کلید دخالت داد و در هربار Post شدن اطلاعات این ActionKeyValue مربوط به کاربر جاری را تغییر داد و کلیدها را در بانکهای اطلاعاتی ذخیره نمود.
مراجع:
Automatic Encryption of Secure Form Field Data
Encrypted Hidden Redux : Let's Get Salty
خواندنیهای 24 خرداد
Silverlight و WPF
دات نت فریم ورک
اس کیوال سرور
متفرقه
مدلهای برنامه
using System.Collections.Generic; namespace jQueryMvcSample05.Models { public class Survey { public int Id { set; get; } public string Title { set; get; } public virtual ICollection<SurveyItem> SurveyItems { set; get; } } }
namespace jQueryMvcSample05.Models { public class SurveyItem { public int Id { set; get; } public string Title { set; get; } public int Order { set; get; } //[ForeignKey("SurveyId")] public virtual Survey Survey { set; get; } public int SurveyId { set; get; } } }
تعدادی نظر سنجی به همراه گزینههای آنها تعریف خواهند شد (یک رابطه one-to-many است). سپس توسط افزونه sortable میخواهیم ترتیب قرارگیری گزینههای آنرا مشخص کنیم یا تغییر دهیم.
منبع داده فرضی برنامه
using System.Collections.Generic; using jQueryMvcSample05.Models; namespace jQueryMvcSample05.DataSource { /// <summary> /// یک منبع داده فرضی جهت دموی سادهتر برنامه /// </summary> public static class SurveysDataSource { private static IList<Survey> _surveysCache; static SurveysDataSource() { _surveysCache = createSurveys(); } public static IList<Survey> SystemSurveys { get { return _surveysCache; } } private static IList<Survey> createSurveys() { var results = new List<Survey>(); for (int i = 1; i < 6; i++) { results.Add(new Survey { Id = i, Title = "نظر سنجی " + i, SurveyItems = new List<SurveyItem> { new SurveyItem{ Id = 1, SurveyId = i, Title = "گزینه 1", Order = 1 }, new SurveyItem{ Id = 2, SurveyId = i, Title = "گزینه 2", Order = 2 }, new SurveyItem{ Id = 3, SurveyId = i, Title = "گزینه 3", Order = 3 }, new SurveyItem{ Id = 4, SurveyId = i, Title = "گزینه 4", Order = 4 } } }); } return results; } } }
کدهای کنترلر برنامه
using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using System.Web.UI; using jQueryMvcSample05.DataSource; using jQueryMvcSample05.Security; namespace jQueryMvcSample05.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { var surveysList = SurveysDataSource.SystemSurveys; return View(surveysList); } [HttpPost] [AjaxOnly] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public ActionResult SortItems(int? surveyId, string[] items) { if (items == null || items.Length == 0 || surveyId == null) return Content("nok"); updateSurvey(surveyId, items); return Content("ok"); } /// <summary> /// این متد جهت آشنایی با پروسه به روز رسانی ترتیب گزینهها در اینجا قرار گرفته است /// بدیهی است محل قرارگیری آن باید در لایه سرویس برنامه اصلی باشد /// </summary> private static void updateSurvey(int? surveyId, string[] items) { var itemIds = new List<int>(); foreach (var item in items) { itemIds.Add(int.Parse(item.Replace("item-row-", string.Empty))); } var survey = SurveysDataSource.SystemSurveys.FirstOrDefault(x => x.Id == surveyId.Value); if (survey == null) return; int order = 0; foreach (var itemId in itemIds) { order++; var surveyItem = survey.SurveyItems.FirstOrDefault(x => x.Id == itemId); if (surveyItem == null) continue; surveyItem.Order = order; } //todo: call save changes .... } } }
و کدهای View متناظر
@model IList<jQueryMvcSample05.Models.Survey> @{ ViewBag.Title = "Index"; var sortUrl = Url.Action(actionName: "SortItems", controllerName: "Home"); } <h2> نظر سنجیها</h2> @foreach (var survey in Model) { <fieldset> <legend>@survey.Title</legend> <div id="sortable-@survey.Id"> @foreach (var surveyItem in survey.SurveyItems.OrderBy(x => x.Order)) { <div id="item-row-@surveyItem.Id"> <span class="handles">::</span> @surveyItem.Title </div> } </div> </fieldset> } <div> لطفا برای تغییر ترتیب آیتمهای تعریف شده، از امکان کشیدن و رها کردن تعریف شده بر روی آیکونهای :: در کنار هر آیتم استفاده نمائید. </div> @section JavaScript { <script type="text/javascript"> $(document).ready(function () { $('div[id^="sortable"]').sortable({ handle: 'span' }).bind('sortupdate', function (e, ui) { var sortableItemId = $(ui.item).parent().attr('id'); var surveyId = sortableItemId.replace('sortable-', ''); var items = []; $('#' + sortableItemId + ' div').each(function () { items.push($(this).attr('id')); }); //alert(items.join('&')); $.ajax({ type: "POST", url: "@sortUrl", data: JSON.stringify({ items: items, surveyId: surveyId }), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = "/login"; } else if (status === 'error' || !data || data == "nok") { alert('خطایی رخ داده است'); } else { alert('انجام شد'); } } }); }); }); </script> }
در اینجا نیاز بود تا ابتدا کدهای کنترلر و View ارائه شوند، تا بتوان در مورد ارتباطات بین آنها بهتر بحث کرد.
در ابتدای نمایش صفحه Home، رکوردهای نظرسنجیها از منبع داده دریافت شده و به View ارسال میشوند. در View برنامه یک حلقه تشکیل گردیده و این موارد رندر خواهند شد.
هر نظر سنجی با یک div بیرونی که با id مساوی sortable شروع میشود، آغاز گردیده و گزینههای آن نظر سنجی نیز توسط divهایی با id مساوی item-row شروع خواهند گردید. هر کدام از این idها حاوی id رکوردهای متناظر هستند. از این idها در کدهای برنامه جهت یافتن یک نظر سنجی یا یک ردیف مشخص برای به روز رسانی ترتیب آنها استفاده خواهیم کرد.
ادامه کار، به تنظیمات و اعمال افزونه sortable مرتبط میشود. توسط تنظیم ذیل به jQuery اعلام خواهیم کرد، هرجایی یک div با id شروع شده با sortable یافتی، افزونه sortable را به آن متصل کن:
$('div[id^="sortable"]').sortable
var sortableItemId = $(ui.item).parent().attr('id'); var surveyId = sortableItemId.replace('sortable-', ''); var items = []; $('#' + sortableItemId + ' div').each(function () { items.push($(this).attr('id')); });
data: JSON.stringify({ items: items, surveyId: surveyId })
public ActionResult SortItems(int? surveyId, string[] items)
اطلاعاتی که در اینجا دریافت میشوند در متد updateSurvey مورد استفاده قرار خواهند گرفت. بر اساس surveyId دریافتی، نظرسنجی مرتبط را یافته و سپس به گزینههای آن دست خواهیم یافت. اکنون نوبت به پردازش آرایه items دریافت شده است. این آرایه بر اساس انتخاب کاربر مرتب شده است.
دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample05.zip
ایجاد یک ثبت کنندهی وقایع EF Core
مرحلهی اول مشاهدهی خروجیهای نهایی EF Core، پیاده سازی اینترفیس ILoggerProvider است که در آن قرار است وهلهی از نوع ILogger بازگشت داده شود. به همین جهت یک کلاس تو در توی خصوصی را در اینجا مشاهده میکنید که اینترفیس ILogger را نیز پیاده سازی کردهاست:
using System; using Microsoft.Extensions.Logging; namespace Tests { public class MyLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string categoryName) { return new MyLogger(); } public void Dispose() { } private class MyLogger : ILogger { public bool IsEnabled(LogLevel logLevel) { return true; } public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { //File.AppendAllText(@"C:\temp\log.txt", formatter(state, exception)); Console.WriteLine(""); Console.WriteLine(formatter(state, exception)); } public IDisposable BeginScope<TState>(TState state) { return null; } } } }
در متد Log:
- پارامتر logLevel، سطح اهمیت اطلاعات رسیده را به همراه دارد. برای مثال اطلاعات است یا خطا؟
برای مثال شاید نیاز به ذخیره سازی اطلاعاتی با سطحهای بحرانی، خطا و یا اخطار در یک بانک اطلاعاتی وجود داشته باشد:
if (logLevel == LogLevel.Critical || logLevel == LogLevel.Error || logLevel == LogLevel.Warning)
- state: میتواند هر نوع شیءایی، حاوی اطلاعات وضعیت رخداد رسیده باشد.
- exception: بیانگر استثنای احتمالی رخ داده است.
- formatter: کار آن تولید یک رشتهی قابل خواندن، توسط اطلاعات حالت و استثناء است.
معرفی Logger تهیه شده به برنامه
پس از تهیهی Logger فوق، جهت معرفی آن به یک برنامهی کنسول، میتوان به صورت ذیل عمل کرد:
using (var db = new MyContext()) { var loggerFactory = (ILoggerFactory)db.GetInfrastructure().GetService(typeof(ILoggerFactory)); loggerFactory.AddProvider(new MyLoggerProvider()); }
در برنامههای ASP.NET Core، کار معرفی MyLoggerProvider در متد Configure کلاس آغازین برنامه انجام میشود:
public void Configure(ILoggerFactory loggerFactory) { loggerFactory.AddProvider(new MyLoggerProvider());
اختصاصی سازی ثبت وقایع رسیده
کلاس MyLoggerProvider، هر نوع اطلاعات داخلی EF Core را نیز لاگ میکند. اگر هدف صرفا بررسی خروجی SQL نهایی تولیدی است، میتوان در متد ذیل:
public ILogger CreateLogger(string categoryName)
برای این منظور، ابتدای Logger تهیه شده چنین شکلی را پیدا میکند:
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging; using System.Linq; using System; namespace Tests { public class MyLoggerProvider : ILoggerProvider { private static readonly string[] _categories = { typeof(RelationalCommandBuilderFactory).FullName, typeof(SqlServerConnection).FullName }; public ILogger CreateLogger(string categoryName) { if (_categories.Contains(categoryName)) { return new MyLogger(); } return NullLogger.Instance; }