مقابله با XSS ؛ یکبار برای همیشه!
بررسی تصویر امنیتی (Captcha) سایت - قسمت دوم
بانک اطلاعاتی
بانک اطلاعاتی را خودمون باید به پروژه اضافه کنیم؟
کتابخانه iTextSharp نمایش گرادیانی از رنگها را هم پشتیبانی میکند و بدیهی است این نمایش برداری است. روش استفاده از آن هم بسیار ساده است؛ مثلا:
PdfShading shading = PdfShading.SimpleAxial(pdfWriter, x0, y0, x1, y1, BaseColor.YELLOW, BaseColor.RED);
PdfShadingPattern pattern = new PdfShadingPattern(shading);
ShadingColor color = new ShadingColor(pattern);
متد PdfShading.SimpleAxial بر اساس شیء PdfWriter که توسط آن به Canvas صفحه دسترسی پیدا میکند، در مختصاتی مشخص، یک طیف رنگی را ایجاد میکند. بر این اساس میتوان به یک ShadingColor هم رسید که از آن مثلا به عنوان BackgroundColor یک PdfPCell قابل استفاده است.
تا اینجا ساده به نظر میرسد اما واقعیت این است که مختصات ذکر شده، مهم است و آنرا در مورد مثلا سلولهای یک جدول تنها در زمان تهیه نهایی یک جدول میتوان به دست آورد. البته اگر شیء سادهای را روی صفحه رسم کنیم، این مورد بر اساس مختصات ابتدایی شیء واضح به نظر میرسد.
برای حل این مشکل در مورد جداول و سلولهای آن خاصیتی به نام CellEvent وجود دارد که از نوع IPdfPCellEvent است. به عبارتی با ارسال یک وهله از کلاسی که اینترفیس IPdfPCellEvent را پیاده سازی میکند، میتوان در زمان نمایش نهایی یک سلول به مختصات دقیق آن دسترسی پیدا کرد.
یک مثال کامل را در مورد پیاده سازی IPdfPCellEvent و استفاده از آن جهت نمایش گرادیان در Header و footer یک جدول، در ادامه مشاهده خواهید نمود:
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
namespace iTextSharpGradientTest
{
public class GradientCellEvent : IPdfPCellEvent
{
public void CellLayout(PdfPCell cell, Rectangle position, PdfContentByte[] canvases)
{
var cb = canvases[PdfPTable.BACKGROUNDCANVAS];
cb.SaveState();
var shading = PdfShading.SimpleAxial(
cb.PdfWriter,
position.Left, position.Top, position.Left, position.Bottom,
BaseColor.YELLOW, BaseColor.ORANGE);
var shadingPattern = new PdfShadingPattern(shading);
cb.SetShadingFill(shadingPattern);
cb.Rectangle(position.Left, position.Bottom, position.Width, position.Height);
cb.Fill();
cb.RestoreState();
}
}
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();
var table1 = new PdfPTable(1);
table1.HeaderRows = 2;
table1.FooterRows = 1;
//header row
var headerCell = new PdfPCell(new Phrase("header"))
{
HorizontalAlignment = Element.ALIGN_CENTER,
Border = 0
};
headerCell.CellEvent = new GradientCellEvent();
table1.AddCell(headerCell);
//footer row
var footerCell = new PdfPCell(new Phrase("footer"))
{
HorizontalAlignment = Element.ALIGN_CENTER,
Border = 0
};
footerCell.CellEvent = new GradientCellEvent();
table1.AddCell(footerCell);
//adding some rows
for (int i = 0; i < 70; i++)
{
var rowCell = new PdfPCell(new Phrase("Row " + i)) { BorderColor = BaseColor.LIGHT_GRAY };
table1.AddCell(rowCell);
}
pdfDoc.Add(table1);
}
//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}
با خروجی:
نکته مهم این مثال نحوه مقدار دهی CellEvent است. به این ترتیب در زمان نمایش نهایی یک سلول میتوان در متد CellLayout، به خواص فقط خواندنی آن سلول دسترسی یافت. مثلا position، مختصات نهایی مستطیل مرتبط با سلول جاری را بر میگرداند؛ یا از طریق canvases میتوان برای آخرین بار فرصت یافت تا در یک سلول نقاشی کرد.
از آن میتوان جهت ثبت استثناهای مدیریت نشده در انواع و اقسام برنامههای وب دات نتی استفاده کرد. از برنامههای ASP.NET Webforms ، ASP.NET MVC تا سرویسهای WCF ، WCF RIA و غیره. سپس این Http module ویژه امکان مرور خطاهای ثبت شده را از راه دور میسر میسازد، به همراه فید RSS از خطاها، امکان مشاهدهی مقادیر فیلدها در لحظهی بروز خطا، ارسال ایمیل خطاها و امکان ذخیره سازی آنها در فایلهای XML تا انواع بانکهای اطلاعاتی.
برای نمونه، سایت Stackoverflow از یک نمونهی سفارشی شدهی آن استفاده میکند (+).
همانطور که در این معرفی آمده است : ثبت استثناهای "مدیریت نشده". اما آیا امکان ثبت استثناهای مدیریت شده هم توسط آن وجود دارد؟
پاسخ: بله. به صورت زیر:
... } catch (Exception ex) { Elmah.ErrorSignal.FromCurrentContext().Raise(ex); ...
در یک سری از متدهای فایل global.asax عموما Context ای وجود ندارد (دقیقا مانند برنامههای دسکتاپ). در این حالت از روش زیر
Elmah.ErrorLog.GetDefault(null).Log(new Error(ex))
using System; using System.Text; using Elmah; namespace Common.WebToolkit { public static class ElmahLogEx { public static void LogException(this string ex) { if (string.IsNullOrWhiteSpace(ex)) return; LogException(new Exception(ex)); } public static void LogException(this Exception ex) { if (ex == null) return; try { ErrorSignal.FromCurrentContext().Raise(ex); } catch { ErrorLog.GetDefault(null).Log(new Error(ex)); } } } }
تهیه یک بانک اطلاعاتی نمونه
برای نمایش امکانات کار با روش Database first، نیاز است یک بانک اطلاعاتی را به صورت مستقل و متداولی ایجاد کنیم. به همین جهت اسکریپت SQL ذیل را توسط Management studio اجرا کنید تا بانک اطلاعاتی BloggingCore2016، به همراه دو جدول به هم وابسته، در آن ایجاد شوند:
CREATE DATABASE [BloggingCore2016] GO USE [BloggingCore2016] GO CREATE TABLE [Blog] ( [BlogId] int NOT NULL IDENTITY, [Url] nvarchar(max) NOT NULL, CONSTRAINT [PK_Blog] PRIMARY KEY ([BlogId]) ); GO CREATE TABLE [Post] ( [PostId] int NOT NULL IDENTITY, [BlogId] int NOT NULL, [Content] nvarchar(max), [Title] nvarchar(max), CONSTRAINT [PK_Post] PRIMARY KEY ([PostId]), CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE ); GO INSERT INTO [Blog] (Url) VALUES ('https://www.dntips.ir/'), ('http://blogs.msdn.com/dotnet'), ('http://blogs.msdn.com/webdev'), ('http://blogs.msdn.com/visualstudio') GO
پیشنیازهای مهندسی معکوس ساختار بانک اطلاعاتی در EF Core
در قسمت اول در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است. همچنین وجود مدخل Microsoft.EntityFrameworkCore.SqlServer.Design برای تدارک امکانات مهندسی معکوس ساختار یک بانک اطلاعاتی SQL Server ضروری است.
تبدیل ساختار دیتابیس BloggingCore2016 به کدهای معادل EF Core آن
پس از فعال سازی ابزارهای خط فرمان EF Core، به پوشهی اصلی پروژه مراجعه کرده، کلید shift را نگه دارید. سپس کلیک راست کرده و گزینهی Open command window here را انتخاب کنید تا خط فرمان از این پوشه آغاز شود. در ادامه دستور ذیل را صادر کنید:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
اجرا این دستور سبب اتصال به رشتهی اتصالی ذکر شده که به بانک اطلاعاتی BloggingCore2016 اشاره میکند، میشود. سپس پروایدر مدنظر ذکر شدهاست. سوئیچ o محل درج فایلهای نهایی را مشخص میکند. برای مثال در اینجا فایلهای نهایی مهندسی معکوس شده در پوشهی Entities درج میشوند (تصویر فوق). همچنین در اینجا امکان ذکر فایل context تولیدی نیز وجود دارد. اگر علاقمند باشید تا تمام ریز جزئیات این عملیات را نیز مشاهده کنید، میتوانید پارامتر اختیاری verbose را نیز به انتهای دستور اضافه نمائید.
بقیه مراحل کار با این فایلهای تولید شده، با نکاتی که تاکنون عنوان شدهاند یکی است. برای مثال اگر میخواهید رشتهی اتصالی پیش فرض را از این Context تولید شده خارج کنید:
public partial class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true"); }
بررسی پارامترهای دیگر ابزار مهندسی معکوس به Code First
اگر دستور dotnet ef dbcontext scaffold --help را صادر کنیم، خروجی راهنمای ذیل را میتوان مشاهده کرد:
Usage: dotnet ef dbcontext scaffold [arguments] [options] Arguments: [connection] The connection string of the database [provider] The provider to use. For example, Microsoft.EntityFrameworkCore.SqlServer Options: -a|--data-annotations Use DataAnnotation attributes to configure the model where possible. If omitted, the output code will use only the fluent API. -c|--context <name> Name of the generated DbContext class. -f|--force Force scaffolding to overwrite existing files. Otherwise, the code will only proceed if no output files would be overwritten. -o|--output-dir <path> Directory of the project where the classes should be output. If omitted, the top-level project directory is used. --schema <schema> Selects a schema for which to generate classes. -t|--table <schema.table> Selects a table for which to generate classes. -e|--environment <environment> The environment to use. If omitted, "Development" is used. -h|--help Show help information -v|--verbose Enable verbose output
- حالت پیش فرض تنظیمات روابط مدلها در این روش، حالت استفاده از Fluent API است. اگر میخواهید آنرا به حالت استفادهی از Data Annotations تغییر دهید، پارامتر a- و یا data-annotations-- را در دستور نهایی ذکر کنید.
- حالت پیش فرض تولید فایلهای نهایی این روش، عدم بازنویسی فایلهای موجود است. اگر میخواهید پس از تغییر بانک اطلاعاتی، مجددا این فایلها را از صفر تولید کنید، پارامتر f- و یا force- را در دستور نهایی ذکر کنید.
بنابراین اگر میخواهید هربار فایلهای نهایی را بازنویسی کنید و همچنین روش کار با Data Annotations را ترجیح میدهید، دستور نهایی، شکل زیر را پیدا خواهد کرد:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose --force --data-annotations
کار با یک بانک اطلاعاتی موجود، با روش مهاجرتهای Code First
فرض کنید میخواهید از یک بانک اطلاعاتی از پیش موجود EF 6.x (یا هر بانک اطلاعاتی از پیش موجود دیگری)، به روش پیش فرض EF Core استفاده کنید. برای این منظور:
- ابتدا جدول migration history قدیمی آنرا حذف کنید؛ چون ساختار آن با EF Core یکی نیست.
- سپس با استفاده از دستور dotnet ef dbcontext scaffold فوق، معادل کلاسها، روابط و Context سازگار با EF Core آنرا تولید کنید.
- در ادامه رشتهی اتصالی پیش فرض آنرا از کلاس Context تولیدی خارج کرده و از یکی از روشهای مطرح شدهی در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» استفاده کنید.
- سپس نیاز است این Context جدید را توسط متد services.AddDbContext به لیست سرویسهای برنامه اضافه کنید. این مورد نیز در قسمت اول بررسی شدهاست.
- مرحلهی بعد، افزودن جدول __EFMigrationsHistory جدید EF Core، به این بانک اطلاعاتی است. برای این منظور به روش متداول فعال کردن مهاجرتها، دستور ذیل را صادر کنید:
dotnet ef migrations add InitialDatabase
using Microsoft.EntityFrameworkCore.Migrations; namespace Core1RtmEmptyTest.DataLayer.Migrations { public partial class InitialDatabase : Migration { protected override void Up(MigrationBuilder migrationBuilder) { } protected override void Down(MigrationBuilder migrationBuilder) { } } }
سپس دستور به روز رسانی بانک اطلاعاتی را صادر کنید:
dotnet ef database update
پس از این مرحله، روش کار، Code first خواهد بود. برای مثال خاصیتی را به کلاسی اضافه میکنید و سپس دو دستور ذیل را صادر خواهید کرد که در آن v2 یک نام دلخواه است:
dotnet ef migrations add v2 dotnet ef database update
در مثالهای زیر مجموعهای از Reflectionهای ساده و کاملا کاربردی است که من با آنها روبرو شده ام.
کوتاه سازی کدهای نمایش یک View در ASP.NET MVC با Reflection
یکی از قسمتهایی که مرتبا با آن سر و کار دارید، نمایش اطلاعات است. حتی یک جدول را هم که میخواهید بسازید، باید ستونهای آن جدول را یک به یک معرفی کنید. ولی در عمل، یک Reflection ساده این کار به یک تابع چند خطی و سپس برای ترسیم هر ستون جدول از دو خط استفاده خواهید کرد ولی مزیتی که دارد این است که این تابع برای تمامی جدولها کاربردی عمومی پیدا میکند. برای نمونه دوست داشتم برای بخش مدیر، قسمت پروفایلی را ایجاد کنم و در آن اطلاعاتی چون نام، نام خانوادگی، تاریخ تولد، تاریخ ایجاد و خیلی از اطلاعات دیگر را نمایش دهم. به جای اینکه بیایم برای هر قسمت یک خط partial ایجاد کنم، با استفاده از reflection و یک حلقه، تمامی اطلاعات را به آن پارشال پاس میکنم. مزیت این روش این است که اگر بخواهم در یک جای دیگر، اطلاعات یک محصول یا یک فاکتور را هم نمایش دهم، باز هم همین تابع برایم کاربرد خواهد داشت:
تصویر زیر را که برگرفته از یک قالب Bootstrap است، ملاحظه کنید. اصلا علاقه ندارم که برای یک به یک آنها، یک سطر جدید را تعریف کنم و به View بگویم این پراپرتی را نشان بده؛ دوباره مورد بعدی هم به همین صورت و دوباره و دوباره و ... . دوست دارم یک تابع عمومی، همهی این کارها را خودکار انجام دهد.
ساختار اطلاعاتی تصویر فوق به شرح زیر است:
<div> <div> <div> <p><span>First Name </span>: Jonathan</p> </div> </div> </div>
BioRow_
@model System.Web.UI.WebControls.ListItem <div> <p><span>@Model.Text </span>: @Model.Value</p> </div>
@using System.Web.UI.WebControls @using ZekrWepApp.Filters @model ZekrModel.Admin <div> <h1>Bio Graph</h1> <div> @{ ListItemCollection collection = GetCustomProperties.Get(Model,exclude:new string[]{"Poems","Id"}); foreach (var item in collection) { Html.RenderPartial(MVC.Shared.Views._BioRow, item); } } </div> </div>
کد درون این کلاس ایستا را بررسی میکنیم؛ این کلاس دو متد دارد یکی عمومی و دیگری خصوصی است:
public class GetCustomProperties { private static PropertyInfo[] getObjectsInfos(object obj,string[] inclue,string[] exclude ) { var list = obj.GetType().GetProperties(); PropertyInfo[] outputPropertyInfos = null; if (inclue != null) { return list.Where(propertyInfo => inclue.Contains(propertyInfo.Name)).ToArray(); } if (exclude != null) { return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray(); } return list; } }
متد عمومی که در این کلاس قرار دارد به شرح زیر است:
public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null) { var propertyInfos = getObjectsInfos(obj, inclue, exclude); if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null"); var collection = new ListItemCollection(); foreach (PropertyInfo propertyInfo in propertyInfos) { string name = propertyInfo.Name; foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true)) { DisplayAttribute displayAttribute = attribute as DisplayAttribute; if (displayAttribute != null) { name = displayAttribute.Name; break; } } string value = ""; object objvalue = propertyInfo.GetValue(obj); if (objvalue != null) value = objvalue.ToString(); collection.Add(new ListItem(name,value)); } return collection; }
کد بالا پراپرتیها را دریافت و یک به یک متادیتاهای آن را بررسی کرده و در صورتی که از متادیتای Display استفاده کرده باشند، مقدار آن را جایگزین نام پراپرتی خواهد کرد. در مورد مقدار هم از آنجا که اگر پراپرتی با Null پر شده باشد، تبدیل به رشتهای با پیام خطای روبرو خواهد شد. در نتیجه بهتر است یک شرط احتیاط هم روی آن پیاده شود. در آخر هم از متن و مقدار، یک آیتم ساخته و درون Collection اضافه میکنیم و بعد از اینکه همه پراپرتیها بررسی شدند، Collection را بر میگردانیم.
[Display(Name = "نام کاربری")] public string UserName { get; set; }
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Web; using System.Web.Mvc.Html; using System.Web.UI.WebControls; using Links; namespace ZekrWepApp.Filters { public class GetCustomProperties { public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null) { var propertyInfos = getObjectsInfos(obj, inclue, exclude); if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null"); var collection = new ListItemCollection(); foreach (PropertyInfo propertyInfo in propertyInfos) { string name = propertyInfo.Name; foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true)) { DisplayAttribute displayAttribute = attribute as DisplayAttribute; if (displayAttribute != null) { name = displayAttribute.Name; break; } } string value = ""; object objvalue = propertyInfo.GetValue(obj); if (objvalue != null) value = objvalue.ToString(); collection.Add(new ListItem(name,value)); } return collection; } private static PropertyInfo[] getObjectsInfos(object obj,string[] include,string[] exclude ) { var list = obj.GetType().GetProperties(); PropertyInfo[] outputPropertyInfos = null; if (include != null) { return list.Where(propertyInfo => include.Contains(propertyInfo.Name)).ToArray(); } if (exclude != null) { return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray(); } return list; } } }
لیستی از پارامترها با Reflection
مورد بعدی که سادهتر بوده و از کد بالا مختصرتر هم هست، این است که قرار بود برای یک درگاه، یک سری اطلاعات را با متد Post ارسال کنم که نحوهی ارسال اطلاعات به شکل زیر بود:
amount=1000&orderId=452&Pid=xxx&....
using System; using System.Collections.Generic; using System.Linq; namespace Utils { public class QueryStringParametersList { private string Symbol = "&"; private List<KeyValuePair<string, string>> list { get; set; } public QueryStringParametersList() { list = new List<KeyValuePair<string, string>>(); } public QueryStringParametersList(string symbol) { Symbol = symbol; list = new List<KeyValuePair<string, string>>(); } public int Size { get { return list.Count; } } public void Add(string key, string value) { list.Add(new KeyValuePair<string, string>(key, value)); } public string GetQueryStringPostfix() { return string.Join(Symbol, list.Select(p => Uri.EscapeDataString(p.Key) + "=" + Uri.EscapeDataString(p.Value))); } } }
یک متغیر به نام symbol دارد و در صورتی در شرایط متفاوت، قصد چسپاندن چیزی را به یکدیگر با علامتی خاص داشته باشید، این تابع میتواند کاربرد داشته باشد. این متد از یک لیست کلید و مقدار استفاده کرده و پارامترهایی را که به آن پاس میشود، نگهداری و سپس توسط متد GetQueryStringPostfix آنها را با یکدیگر الحاق کرده و در قالب یک رشته بر میگرداند.
کاربرد Reflection در اینجا این است که من باید دوبار به شکل زیر، دو نوع اطلاعات متفاوت را پست کنم. یکی موقع ارسال به درگاه و دیگری موقع بازگشت از درگاه.
QueryStringParametersList queryparamsList = new QueryStringParametersList(); ueryparamsList.Add("consumer_key", requestPayment.Consumer_Key); queryparamsList.Add("amount", requestPayment.Amount.ToString()); queryparamsList.Add("callback", requestPayment.Callback); queryparamsList.Add("description", requestPayment.Description); queryparamsList.Add("email", requestPayment.Email); queryparamsList.Add("mobile", requestPayment.Mobile); queryparamsList.Add("name", requestPayment.Name); queryparamsList.Add("irderid", requestPayment.OrderId.ToString());
ولی با استفاده از کد Reflection که در بالاتر عنوان شد، باید نام و مقدار پراپرتی را گرفته و در یک حلقه آنها را اضافه کنیم، بدین شکل:
private QueryStringParametersList ReadParams(object obj) { PropertyInfo[] propertyInfos = obj.GetType().GetProperties(); QueryStringParametersList queryparamsList = new QueryStringParametersList(); for (int i = 0; i < propertyInfos.Count(); i++) { queryparamsList.Add(propertyInfos[i].Name.ToLower(),propertyInfos[i].GetValue(obj).ToString() ); } return queryparamsList; }
جهت نگهداری بعضی از اطلاعات در صفحات کاربر، از فیلدهای مخفی ( 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