نظرات مطالب
استفاده از قابلیت پارتیشن بندی در آرشیو جداول بانک‌های اطلاعاتی SQL Server
در مثال مذکور از Partition Key در زمان تعریف یک Primary Key Constraint روی جدول به منظور داشتن ساختار Aligned Index ، استفاده نموده ایم.
در هنگام ایجاد یک Primary Key Constraint بطور خودکار یک Unique Clustered Index  نیز روی ستون (های) شرکت یافته در تعریف  Primary Key ایجاد می‌شود و بدین ترتیب Table براساس این فیلد (ها) به شکل Sort شده نگهداری می‌شود، ضمن اینکه هر Table می‌تواند، شامل 1 عدد Clustered Index و 249 عدد Nonclustered Index باشد.
نظرات مطالب
EF Code First #2
نه. همانطوری که کوئری را صادر کردید اجرا می‌شود. یعنی فقط بر اساس primary key کار آپدیت یا delete را انجام می‌دهد.
مطالب
رمزنگاری خودکار فیلدهای مخفی در ASP.NET MVC

جهت نگهداری بعضی از اطلاعات در صفحات کاربر، از فیلد‌های مخفی ( 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

    }
در این کلاس تنظیمات مربوط به Encryption را بازیابی مینماییم.

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
    }
در این پروژه ، جهت رمزنگاری، از کلاس  RijndaelManaged استفاده میکنیم.
RijndaelManaged :Accesses the managed version of the Rijndael algorithm
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)
این متد در DefaultControllerFactory  جهت بدست آوردن کلید  View در زمانیکه میخواهیم اطلاعات را بازیابی کنیم استفاده میشود.

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);
        }

    }

دو helper برای ایجاد فیلد مخفی رمزنگاری شده ایجاد شده است . در ادامه نحوه استفاده از این دو متد الحاقی را در View‌های برنامه، مشاهده مینمایید. 
   @Html.EncryptedHiddenFor(model => model.Id)
   @Html.EncryptedHidden("Id2","2")
کلاس DecryptingControllerFactory :
    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;
        }

    }
از این DefaultControllerFactory جهت رمزگشایی داده‌هایی رمز نگاری شده و بازگرداندن آنها به مقادیر اولیه، در هنگام عملیات PostBack استفاده میشود. 
  این قسمت از کد
  if (requestContext.RouteData.Values[oldKey] != null)
                {
                    if (requestContext.RouteData.Values[oldKey].ToString() != oldValue)
                        throw new ApplicationException("Form values is modified!");
                }
زمانی استفاده میشود که کلید مد نظر ما در UrlParameter‌ها یافت شود و درصورت مغایرت این پارامتر و فیلد مخفی، یک Exception تولید میشود.
همچنین بایستی این 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
نظرات مطالب
بررسی فرمت کوکی‌های ASP.NET Identity
با سلام
آقای نصیری در آخرین سوال گفتید که باید از UpdateSecurityStampAsync استفاده بشه. من تو برنامم ، بر اساس هر Application_AuthenticateRequest میام چک میکنم که اگر کاربر حذف یا قفل یا ... بود به صورت خودکار Signout بشه. با توجه به نکته امنیتی که که شما گفتید باید اینجا هم از  UpdateSecurityStampAsync  استفاده بشه ، من به این صورت استفاده کردم :


   public class MvcApplication : HttpApplication
    {
        public MvcApplication()
        {
            var wrapper = new EventHandlerTaskAsyncHelper(AuthenticateRequest);
            AddOnAuthenticateRequestAsync(wrapper.BeginEventHandler, wrapper.EndEventHandler);
        }
        private async Task AuthenticateRequest(object sender, EventArgs e)
        {
            if (Context.User == null)
                return;

            //bla bla

            if (هرچیزی که مجاز نباشد)
            {
                var authenticationManager = ProjectObjectFactory.Container.GetInstance<IAuthenticationManager>();
                authenticationManager.SignOut
                (
                    DefaultAuthenticationTypes.ExternalCookie,
                    DefaultAuthenticationTypes.ApplicationCookie
                );
                FormsAuthentication.SignOut();
                await userService.UpdateSecurityStampAsync(userId);
            }
        }
    }

در عمل چک کردم جواب داد ، البته مجبور بودم از یک EventHandlerTaskAsyncHelper استفاده کنم چون نمیشد تو خود متد Application_AuthenticateRequest متدهای Async تعریف کرد.

سوال من اینه ، آیا اصلا نیاز هست در اینجا هم security stamp رو آپدیت کنیم ؟ یا اصلا این روشی که من رفتم از نظر امنیتی درست هست یا خیر.

با تشکر
نظرات اشتراک‌ها
کتابخانه tinymce
Required کردن TinyMCE با اتریبیوت اعتبارسنجی سفارشی
 سمت سرور :
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class MakeTinyMceRequiredAttribute : ValidationAttribute, IClientModelValidator
{
    public MakeTinyMceRequiredAttribute()
    {
        ErrorMessage = "لطفا {0} را وارد نمایید";
    }

    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        var displayName = validationContext.DisplayName;
        ErrorMessage = ErrorMessage.Replace("{0}", displayName);

        if (string.IsNullOrWhiteSpace(value?.ToString()))
        {
            return new ValidationResult(ErrorMessage);
        }
        return ValidationResult.Success;
    }

    public void AddValidation(ClientModelValidationContext context)
    {
        var displayName = context.ModelMetadata.ContainerMetadata
            .ModelType.GetProperty(context.ModelMetadata.PropertyName)
            .GetCustomAttributes(typeof(DisplayAttribute), false)
            .Cast<DisplayAttribute>()
            .FirstOrDefault()?.Name;
        ErrorMessage = ErrorMessage.Replace("{0}", displayName);

        MergeAttribute(context.Attributes, "data-val", "true");
        MergeAttribute(context.Attributes, "data-val-makeTinyMceRequired", ErrorMessage);
    }
    public bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
    {
        if (attributes.ContainsKey(key))
        {
            return false;
        }
        attributes.Add(key, value);
        return true;
    }
}
سمت کلاینت :
if (jQuery.validator) {
    // For hidden inputs
    $.validator.setDefaults({
        ignore: []
    });

    // makeTinyMceRequired
    jQuery.validator.addMethod('makeTinyMceRequired', function (value, element, param) {
        var editorId = $(element).attr('id');
        var editorContent = tinyMCE.get(editorId).getContent();
        $('body').append(`<div id="test-makeTinyMceRequired">${editorContent}</div>`);
        var result = isNullOrWhitespace($('#test-makeTinyMceRequired').text());
        $('#test-makeTinyMceRequired').remove();
        return !result;
    });
    jQuery.validator.unobtrusive.adapters.addBool('makeTinyMceRequired');
}
function isNullOrWhitespace(input) {
    if (typeof input === 'undefined' || input == null)
        return true;
    return input.replace(/\s/g, '').length < 1;
}
نحوه انجام کار :
متنِ ادیتور TinyMCE داخل یک div موقت ریخته میشه بعد text اون div با isNullOrWhitespace بررسی میشه، اگه کاربر مقداری رو وارد کرده باشه اعتبارسنجی پاس میشه در غیر اینصورت متن خطارو نمایش میده.
مطالب
آشنایی با Refactoring - قسمت 12


قبلا در مورد تبدیل switch statement به الگوی استراتژی مطلبی را در این سایت مطالعه کرده‌اید (^) و بیشتر مربوط است به حالتی که داخل هر یک از case های یک switch statement چندین و چند سطر کد و یا فراخوانی یک تابع وجود دارد. حالت ساده‌تری هم برای refactoring یک عبارت switch وجود دارد و آن هم زمانی است که هر case، تنها از یک سطر تشکیل می‌شود؛ مانند:

namespace Refactoring.Day12.RefactoringSwitchStatement.Before
{
public class Translator
{
public string ToPersian(string englishWord)
{
switch (englishWord)
{
case "zero":
return "صفر";
case "one":
return "یک";
default:
return string.Empty;
}
}
}
}

در اینجا می‌توان از امکانات ساختار داده‌های توکار دات نت استفاده کرد و این switch statement را به یک dictionary تبدیل نمود:

using System.Collections.Generic;

namespace Refactoring.Day12.RefactoringSwitchStatement.After
{
public class Translator
{
IDictionary<string, string> Words = new Dictionary<string, string>
{
{ "zero", "صفر" },
{ "one", "یک" }
};

public string ToPersian(string englishWord)
{
string persianWord;
if (Words.TryGetValue(englishWord, out persianWord))
{
return persianWord;
}

return string.Empty;
}
}
}

همانطور که ملاحظه می‌کنید هر case به یک key و هر return به یک value در Dictionary تعریف شده، تبدیل گشته‌اند. در اینجا هم بهتر است از متد TryGetValue جهت دریافت مقدار کلیدها استفاده شود؛ زیرا در صورت فراخوانی یک Dictionary با کلیدی که در آن موجود نباشد یک استثناء بروز خواهد کرد.
برای حذف این متد TryGetValue، می‌توان یک enum را بجای کلیدهای تعریف شده، معرفی کرد. به صورت زیر:

using System.Collections.Generic;

namespace Refactoring.Day12.RefactoringSwitchStatement.After
{
public enum EnglishWord
{
Zero,
One
}

public class Translator2
{
IDictionary<EnglishWord, string> Words = new Dictionary<EnglishWord, string>
{
{ EnglishWord.Zero, "صفر" },
{ EnglishWord.One, "یک" }
};

public string ToPersian(EnglishWord englishWord)
{
return Words[englishWord];
}
}
}


به این ترتیب از یک خروجی پر از if و else و switch به یک خروجی ساده و بدون وجود هیچ شرطی رسیده‌ایم.
مطالب
ذخیره سازی SQL تولیدی در NH3

همانطور که در مطلب "NHibernate 3.0 و عدم وابستگی مستقیم به Log4Net" عنوان شد، از اینترفیس جدید IInternalLogger آن می‌توان جهت ثبت وقایع داخلی NHibernate استفاده کرد. اگر در این بین صرفا بخواهیم SQL های تولیدی را لاگ کنیم، خلاصه‌ی آن به صورت زیر خواهد بود:
public class LoggerFactory : ILoggerFactory
{
public IInternalLogger LoggerFor(System.Type type)
{
if (type == typeof(NHibernate.Tool.hbm2ddl.SchemaExport))
//log it
}

public IInternalLogger LoggerFor(string keyName)
{
if (keyName == "NHibernate.SQL")
//log it
}
}
یا کلید NHibernate.SQL باید پردازش شود (جهت ثبت SQL های کوئری‌ها) یا نوع NHibernate.Tool.hbm2ddl.SchemaExport جهت ثبت SQL ساخت ساختار جداول بانک اطلاعاتی باید بررسی گردد.
سورس کامل این کتابخانه‌ی کوچک را از اینجا می‌توانید دریافت کنید. جهت استفاده از آن تنها کافی است چند سطر زیر به فایل app.config یا web.config برنامه‌ی شما اضافه شوند:

<appSettings>
<add key="nhibernate-logger" value="NH3SQLLogger.LoggerFactory, NH3SQLLogger" />
</appSettings>

کلید nhibernate-logger ، به صورت مستقیم توسط NHibernate بررسی می‌شود و صرف نظر از اینکه از کدامیک از مشتقات NHibernate استفاده می‌کنید، با تمام آن‌ها کار خواهد کرد.
لازم به ذکر است که اگر برنامه‌ی شما از نوع ASP.NET است، این کتابخانه اطلاعات را در پوشه‌ی استاندارد App_Data ثبت خواهد کرد؛ در غیراینصورت فایل‌ها در کنار فایل اجرایی برنامه تشکیل خواهند شد.

مطالب
کار با کوکی‌ها در ASP.NET Core
API کار با کوکی‌ها نیز در ASP.NET Core نسبت به نگارش‌های دیگر تغییریافته‌است که در ادامه این موارد را بررسی خواهیم کرد. همچنین با کمک مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» یک تامین کنند‌ه‌ی سفارشی کوکی‌های رمزنگاری شده را نیز ایجاد می‌کنیم.


خلاصه‌ای از روش‌های کار با کوکی‌ها در ASP.NET Core

ایجاد یک کوکی جدید
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestCookiesController : Controller
    {
        public IActionResult Index()
        {
            this.Response.Cookies.Append("key", "value", new CookieOptions
            {
                HttpOnly = true,
                Path = this.Request.PathBase.HasValue ? this.Request.PathBase.ToString() : "/",
                Secure = this.Request.IsHttps
            });
 
            return Content("OK!");
        }
    }
}
کوکی جدید را می‌توان توسط متد Append مجموعه‌ی کوکی‌ها، به Response اضافه کرد:


همانطور که در تصویر نیز مشخص است، طول عمر این کوکی، به سشن تنظیم شده‌است و با پایان سشن جاری مرورگر (بسته شدن کل مرورگر)، این کوکی نیز غیرمعتبر شده و به صورت خودکار حذف خواهد شد. برای تعیین عمر دقیق یک کوکی می‌توان از خاصیت Expires شیء CookieOptions که در مثال فوق مقدار دهی نشده‌است، استفاده کرد؛ مانند:
 Expires = DateTimeOffset.UtcNow.AddDays(2)

خواندن محتویات کوکی ذخیره شده

پس از ثبت کوکی در Response، خواندن آن در Request بعدی به شکل زیر است:
 var value = this.Request.Cookies["key"];
در این حالت اگر کلید درخواستی در مجموعه‌ی کوکی‌ها یافت نشد، نال بازگشت داده می‌شود.
شیء this.Request.Cookies از نوع IRequestCookieCollection است:
    public interface IRequestCookieCollection : IEnumerable<KeyValuePair<string, string>>, IEnumerable
    {
        string this[string key] { get; }
        ICollection<string> Keys { get; }
        bool ContainsKey(string key);
        bool TryGetValue(string key, out string value);
    }
و همانطور که ملاحظه می‌کنید، برای دریافت مقدار یک کوکی یا می‌توان از indexer آن مانند مثال فوق و یا از متد TryGetValue استفاده کرد.
در مستندات آن عنوان شده‌است که در حالت استفاده‌ی از indexer، درصورت یافت نشدن کلید، string.Empty بازگشت داده می‌شود (که آزمایشات null را نمایش می‌دهند). اما در حالت استفاده‌ی از TryGetValue بر اساس خروجی bool آن دقیقا می‌توان مشخص کرد که آیا این کوکی وجود داشته‌است یا خیر.
در اینجا همچنین متد ContainsKey نیز جهت بررسی وجود یک کلید، در مجموعه‌ی کلیدها نیز پیش بینی شد‌ه‌است.
بنابراین بهتر است جهت یافتن مقادیر کوکی‌ها از روش ذیل استفاده کرد:
string cookieValue;
if (this.Request.Cookies.TryGetValue(key, out cookieValue))
{
   // TODO: use the cookieValue
}
else
{
  // this cookie doesn't exist.
}

حذف کوکی‌های موجود

در اینجا متد Delete نیز پیش بینی شده‌است که باید بر روی کوکی‌های Response فراخوانی شود:
 this.Response.Cookies.Delete("key");
کار آن افزودن یک کوکی دیگر با همین کلید، اما منقضی شده‌است؛ تا مرورگر را مجبور به حذف آن کند. در اینجا در صورت نیاز می‌توان به عنوان پارامتر دوم، CookieOptions این کوکی جدید را نیز تنظیم کرد.


همانطور که در تصویر نیز مشخص است، در صورت عدم تنظیم CookieOptions، این کوکی جدید اضافه شده، دارای تاریخ انقضای 1970 است که سبب خواهد شد تا توسط مرورگر، غیرمعتبر درنظر گرفته شده و حذف شود.


طراحی یک تامین کننده‌ی کوکی‌های امن

پس از آشنایی با مقدمات کوکی‌ها و همچنین «بررسی تغییرات رمزنگاری اطلاعات در NET Core.»، اکنون می‌توان یک تامین کننده‌ی کوکی‌های رمزنگاری شده را برای ASP.NET Core به نحو ذیل طراحی کرد:
public interface ISecureCookiesProvider
{
    void Add(HttpContext context, string token, string value);
    bool Contains(HttpContext context, string token);
    string GetValue(HttpContext context, string token);
    void Remove(HttpContext context, string token);
}
 
public class SecureCookiesProvider : ISecureCookiesProvider
{
    private readonly IProtectionProvider _protectionProvider;
 
    public SecureCookiesProvider(IProtectionProvider protectionProvider)
    {
 
        _protectionProvider = protectionProvider;
    }
 
    public void Add(HttpContext context, string token, string value)
    {
        value = _protectionProvider.Encrypt(value);
        context.Response.Cookies.Append(token, value, getCookieOptions(context));
    }
 
    public bool Contains(HttpContext context, string token)
    {
        return context.Request.Cookies.ContainsKey(token);
    }
 
    public string GetValue(HttpContext context, string token)
    {
        string cookieValue;
        if (!context.Request.Cookies.TryGetValue(token, out cookieValue))
        {
            return null;
        }
        return _protectionProvider.Decrypt(cookieValue);
    }
 
    public void Remove(HttpContext context, string token)
    {
        if (context.Request.Cookies.ContainsKey(token))
        {
            context.Response.Cookies.Delete(token);
        }
    }
 
    /// <summary>
    /// Expires at the end of the browser's session.
    /// </summary>
    private CookieOptions getCookieOptions(HttpContext context)
    {
        return new CookieOptions
        {
            HttpOnly = true,
            Path = context.Request.PathBase.HasValue ? context.Request.PathBase.ToString() : "/",
            Secure = context.Request.IsHttps
        };
    }
}
- نکاتی را که در ابتدای مطلب در مورد ثبت و خواندن و حذف کوکی‌ها مطالعه کردید، به این کلاس اضافه شده‌اند. با این تغییر که پیش از ذخیر‌ه‌ی مقدار کوکی، این مقدار رمزنگاری می‌شود و همچنین پس از خواندن آن، رمزگشایی خواهد شد.
- در این تامین کننده‌ی کوکی‌های امن، IProtectionProvider تزریقی به سازنده‌ی کلاس را در مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» پیشتر ملاحظه کرده‌اید.
- در اینجا برای ثبت سرویس جدید، تنظیمات ابتدایی برنامه چنین شکلی را پیدا می‌کنند و پس از آن می‌توان سرویس ISecureCookiesProvider را به کنترلرهای برنامه تزریق و استفاده کرد:
public class Startup
{ 
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
        services.TryAddSingleton<ISecureCookiesProvider, SecureCookiesProvider>();
- چون در کلاس SecureCookiesProvider، خاصیت Expires تنظیم نشده‌است، طول عمر این کوکی‌ها محدود است به مدت زمان باز بودن مرورگر. بنابراین در صورت نیاز این مورد را تغییر دهید.