معرفی ویژگی های ویژال استدیو
در هنگام ایجاد یک Primary Key Constraint بطور خودکار یک Unique Clustered Index نیز روی ستون (های) شرکت یافته در تعریف Primary Key ایجاد میشود و بدین ترتیب Table براساس این فیلد (ها) به شکل Sort شده نگهداری میشود، ضمن اینکه هر Table میتواند، شامل 1 عدد Clustered Index و 249 عدد Nonclustered Index باشد.
EF Code First #2
جهت نگهداری بعضی از اطلاعات در صفحات کاربر، از فیلدهای مخفی ( 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
بررسی فرمت کوکیهای 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
[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; }
قبلا در مورد تبدیل 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 به یک خروجی ساده و بدون وجود هیچ شرطی رسیدهایم.
همانطور که در مطلب "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
}
}
سورس کامل این کتابخانهی کوچک را از اینجا میتوانید دریافت کنید. جهت استفاده از آن تنها کافی است چند سطر زیر به فایل 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
ایجاد یک کوکی جدید
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!"); } } }
همانطور که در تصویر نیز مشخص است، طول عمر این کوکی، به سشن تنظیم شدهاست و با پایان سشن جاری مرورگر (بسته شدن کل مرورگر)، این کوکی نیز غیرمعتبر شده و به صورت خودکار حذف خواهد شد. برای تعیین عمر دقیق یک کوکی میتوان از خاصیت 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، درصورت یافت نشدن کلید، 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، این کوکی جدید اضافه شده، دارای تاریخ انقضای 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>();