مطالب
پیاده سازی Structured Logging در ASP.NET Core با استفاده از Serilog
زمانیکه یک رشته را با استفاده از ILogger ثبت می‌کنید، در پنل مربوط به نمایش لاگ‌ها گاهی اوقات نیازمند آن هستیم که یکسری فیلتر یا مرتب سازی را بر روی لاگ‌های ثبت شده انجام دهیم. (در این مثال از Kibana برای نمایش لاگها استفاده شده است) برای مثال کد زیر را در نظر بگیرید که یک رشته را ثبت می‌کند:
public IActionResult Get()
{
    var userData = new User
    {
        Name = "Farhad",
        Family = "Zamani"
    };

    _logger.LogInformation($"ValuesController called. Name:{userData.Name}, Family:{userData.Family}");

    return Ok();
}
اکنون اگر در پنل کیبانا لاگ ثبت شده را تماشا کنیم چنین لاگی را مشاهده می‌کنیم:

اگر بخواهیم مرتب سازی و یا فیلتری را بر روی Name و یا Family انجام دهیم، کار آسانی نخواهد بود و وقت‌گیر خواهد بود؛ زیرا باید با یک فیلد رشته ( message )، که تمامی لاگ درون آن قرار دارد، کار کنیم. اما با استفاده از قابلیت Structured Logging مربوط به Serilog، می‌توانیم آبجکت userData را به ILogger پاس دهیم و پراپرتی‌های آن را به صورت فیلدهای جدا نمایش دهیم. 

برای این کار باید آبجکت موردنظر خود را درون {} قرار دهیم و قبل از نام متغییر آن یک @ قرار دهیم. بدین صورت: {userData@}  و سپس دیتای موردنظر را در پارامتر دوم logger قرار دهیم.

_logger.LogInformation("ValuesController called. {@userData}", userData);

 اگر کد نوشته شده مربوط به ثبت لاگ را به صورت زیر اصلاح کنیم:

public IActionResult Get()
{
    var userData = new User
    {
        Name = "Farhad",
        Family = "Zamani"
    };

    _logger.LogInformation("ValuesController called. {@userData}", userData);

    return Ok();
}

در پنل کیبانا به راحتی می‌توان عملیات مرتب سازی و یا فیلتر را بر روی پراپرتی‌های Name و Family انجام دهیم. اکنون اگر پنل کیبانا را تماشا کنید چنین لاگی را مشاهده می‌کنیم:

هرکدام از پراپرتی‌های userData به صورت یک فیلد جدا ارسال شده‌است که به راحتی می‌توانید مرتب سازی، فیلتر و... را بر روی هرکدام از فیلدها انجام دهید.

تنظیمات مربوط به Serilog:

public static void Main(string[] args)
{
    var configuration = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .Build();

    Log.Logger = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Filter.ByExcluding(Matching.FromSource("Serilog"))
        .Filter.ByExcluding(Matching.FromSource("System.Net.Http"))
        .Filter.ByExcluding(Matching.FromSource("Microsoft.AspNetCore"))
        .WriteTo.Console()
        .ReadFrom.Configuration(configuration)
        .CreateLogger();

    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSerilog()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

فایل appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Serilog": {
    "WriteTo": [
      {
        "Name": "Elasticsearch",
        "Args": {
          "nodeUris": "http://127.0.0.1:9200;",
          "indexFormat": "structuredlogging-{0:yyyy.MM}",
          "templateName": "structuredlogging"
        }
      }
    ]
  }
}

فایل docker-compose برای اجرای Elasticsearch  و Kibana:

version: '3'
services:
  elasticsearch:
    container_name: elasticsearch
    image: elasticsearch:7.14.2
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
      - 9300:9300
      - 8200:8200

  kibana:
    container_name: kibana
    image: kibana:7.14.2
    ports:
      - 5601:5601


منابع استفاده شده:

مطالب
طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید

هدف از این مطلب، ارائه راه حلی برای تولید خودکار کد یا شماره یکتا و ترتیبی در زمان ثبت رکورد جدید به صورت یکپارچه با EF Core، می‌باشد. به عنوان مثال فرض کنید در زمان ثبت سفارش، نیاز است بر اساس یکسری تنظیمات، یک شماره منحصر به فرد برای آن سفارش، تولید شده و در فیلدی تحت عنوان Number قرار گیرد؛ یا به صورت کلی برای موجودیت‌هایی که نیاز به یک نوع شماره گذاری منحصر به فرد دارند، مانند: سفارش، طرف حساب و ... 


یک مثال واقعی

در زمان ثبت یک Task، کاربر می‌تواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شماره‌ای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین می‌کند که شماره یکتایی را ایجاد کند.

ایجاد یک قرارداد برای موجودیت‌های دارای شماره منحصر به فرد
public interface INumberedEntity
{
    string Number { get; set; }
}
با استفاده از این واسط می‌توان از تکرار یکسری از تنظیمات مانند تنظیم طول فیلد Number و همچنین ایجاد ایندکس منحصر به فرد برروی آن، به شکل زیر جلوگیری کرد.
foreach (var entityType in builder.Model.GetEntityTypes()
    .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType)))
{
    builder.Entity(entityType.ClrType)
        .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50);

    if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType))
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId))
            .HasName(
                $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
    else
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number))
            .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
}

ایجاد یک Entity برای نگهداری شماره قابل استفاده بعدی مرتبط با موجودیت‌ها
public class NumberedEntity : Entity, IMultiTenantEntity
{
    public string EntityName { get; set; }
    public long NextNumber { get; set; }
    
    public long TenantId { get; set; }
}

با تنظیمات زیر:
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity>
{
    public void Configure(EntityTypeBuilder<NumberedEntity> builder)
    {
        builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false);
        builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique();
        builder.ToTable(nameof(NumberedEntity));
    }
}

شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت می‌توان آخرین شماره ثبت شده‌ی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:

  • ایجاد Gap مابین شماره‌های تولید شده، که مدنظر ما نمی‌باشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شماره‌ای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)


پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number

internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity>
{
    private readonly IUnitOfWork _uow;
    private readonly IOptions<NumberingConfiguration> _configuration;

    public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));
        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
    }

    protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata)
    {
        if (!entity.Number.IsNullOrEmpty()) return;

        bool retry;
        string nextNumber;
        
        do
        {
            nextNumber = GenerateNumber(entity);
            var exists = CheckDuplicateNumber(entity, nextNumber);
            retry = exists;
            
        } while (retry);
        
        entity.Number = nextNumber;
    }

    private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber)
    {
       //...
    }

    private string GenerateNumber(INumberedEntity entity)
    {
       //...
    }
}

ابتدا بررسی می‌شود اگر پراپرتی Number مقداردهی شده‌است، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شماره‌ای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت می‌گیرد.


بررسی متد GenerateNumber

private string GenerateNumber(INumberedEntity entity)
{
    var option = _configuration.Value.NumberedEntityOptions[entity.GetType()];

    var entityName = $"{entity.GetType().FullName}";

    var lockKey = $"Tenant_{_uow.TenantId}_" + entityName;

    _uow.ObtainApplicationLevelDatabaseLock(lockKey);

    var nextNumber = option.Start.ToString();

    var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName);
    if (numberedEntity == null)
    {
        _uow.ExecuteSqlCommand(
            "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName,
            option.Start + option.IncrementBy, _uow.TenantId);
    }
    else
    {
        nextNumber = numberedEntity.NextNumber.ToString();
        _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ",
            numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id);
    }

    if (!string.IsNullOrEmpty(option.Prefix))
        nextNumber = option.Prefix + nextNumber;
    
    return nextNumber;
}

ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد می‌کنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنش‌های همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید می‌باشد، با توجه به اینکه INumberedEntity می‌باشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.

پیاده سازی متد مذکور به شکل زیر می‌باشد:

public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource)
{
    uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, 
                @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000);
}

با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیت‌های INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونه‌ها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی می‌شود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود. 

گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری می‌باشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام می‌گیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.

ساختار NumberingConfiguration

public class NumberingConfiguration
{
    public bool Enabled { get; set; }

    public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } =
        new Dictionary<Type, NumberedEntityOption>();
}
public class NumberedEntityOption
{
    public string Prefix { get; set; }
    public int Start { get; set; } = 1;
    public int IncrementBy { get; set; } = 1;
}

با استفاده از دوکلاس بالا، امکان تنظیم الگوی تولید برای موجودیت‌ها را خواهیم داشت.

گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیت‌ها

public static void AddNumbering(this IServiceCollection services,
    IDictionary<Type, NumberedEntityOption> options)
{
    services.Configure<NumberingConfiguration>(configuration =>
    {
        configuration.Enabled = true;
        configuration.NumberedEntityOptions.AddRange(options);
    });
    
    services.AddTransient<IPreActionHook, NumberingPreInsertHook>();
}

و استفاده از این متد الحاقی در Startup پروژه

services.AddNumbering(new Dictionary<Type, NumberedEntityOption>
{
    [typeof(Task)] = new NumberedEntityOption
    {
        Prefix = "T_",
        Start = 1000,
        IncrementBy = 5
    }
});

و موجودیت Task

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024; 

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo; 
    public byte[] RowVersion { get; set; }
    public string Number { get; set; }
}

با خروجی‌های زیر

پ.ن ۱: در برخی از Domain‌ها نیاز به ریست کردن این شماره‌ها براساس یکسری فیلد موجود در موجودیت مورد نظر نیز مطرح می‌باشد. به عنوان مثال در یک سیستم انبارداری شاید براساس FiscalYear و در یک سیستم فروش با توجه به نحوه فروش (SaleType)، لازم باشد این ریست برای شماره‌های موجودیت «سفارش»، انجام پذیرد. در کل با کمی تغییرات می‌توان از این روش مطرح شده در چنین حالاتی نیز به عنوان یک ابزار شماره گذاری خودکار کمک گرفت.
پ.ن ۲: استفاده از امکانات  Sequence در Sql Server هم شاید اولین راه حلی باشد که به ذهن می‌رسد؛ ولی از آنجایی که از تراکنش‌ها پشتیبانی ندارد، مسئله Gap بین شماره‌ها پابرجاست و همچنین آزادی عملی را به این شکل که در مطلب مطرح شد، نداریم.
مطالب
رمزنگاری فایل‌های PDF با استفاده از کلید عمومی توسط iTextSharp

دو نوع رمزنگاری را می‌توان توسط iTextSharp به PDF تولیدی و یا موجود، اعمال کرد:
الف) رمزنگاری با استفاده از کلمه عبور
ب) رمزنگاری توسط کلید عمومی

الف) رمزنگاری با استفاده از کلمه عبور
در اینجا امکان تنظیم read password و edit password به کمک متد SetEncryption شیء pdfWrite وجود دارد. همچنین می‌توان مشخص کرد که مثلا آیا کاربر می‌تواند فایل PDF را چاپ کند یا خیر (PdfWriter.ALLOW_PRINTING).
ذکر read password اختیاری است؛ اما جهت اعمال permissions حتما نیاز است تا edit password ذکر گردد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.Text;

namespace EncryptPublicKey
{
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));

var readPassword = Encoding.UTF8.GetBytes("123");//it can be null.
var editPassword = Encoding.UTF8.GetBytes("456");
int permissions = PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_COPY;
pdfWriter.SetEncryption(readPassword, editPassword, permissions, PdfWriter.STRENGTH128BITS);

pdfDoc.Open();

pdfDoc.Add(new Phrase("tst 0"));
pdfDoc.NewPage();
pdfDoc.Add(new Phrase("tst 1"));
}

Process.Start("TestEnc.pdf");
}
}
}


اگر read password ذکر شود، کاربران برای مشاهده محتویات فایل نیاز خواهند داشت تا کلمه‌ی عبور مرتبط را وارد نمایند:


این روش آنچنان امنیتی ندارد. هستند برنامه‌هایی که این نوع فایل‌ها را «آنی» به نمونه‌ی غیر رمزنگاری شده تبدیل می‌کنند (حتی نیازی هم ندارند که از شما کلمه‌ی عبوری را سؤال کنند). بنابراین اگر کاربران شما آنچنان حرفه‌ای نیستند، این روش خوب است؛ در غیراینصورت از آن صرفنظر کنید.


ب) رمزنگاری توسط کلید عمومی
این روش نسبت به حالت الف بسیار پیشرفته‌تر بوده و امنیت قابل توجهی هم دارد و «نیستند» برنامه‌هایی که بتوانند این فایل‌ها را بدون داشتن اطلاعات کافی، به سادگی رمزگشایی کنند.

برای شروع به کار با public key encryption نیاز است یک فایل PFX یا Personal Information Exchange داشته باشیم. یا می‌توان این نوع فایل‌ها را از CA's یا Certificate Authorities خرید، که بسیار هم نیکو یا اینکه می‌توان فعلا برای آزمایش، نمونه‌ی self signed این‌ها را هم تهیه کرد. مثلا با استفاده از این برنامه.


در ادامه نیاز خواهیم داشت تا اطلاعات این فایل PFX را جهت استفاده توسط iTextSharp استخراج کنیم. کلاس‌های زیر اینکار را انجام می‌دهند و نهایتا کلیدهای عمومی و خصوصی ذخیره شده در فایل PFX را بازگشت خواهند داد:

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.X509;

namespace EncryptPublicKey
{
/// <summary>
/// A Personal Information Exchange File Info
/// </summary>
public class PfxData
{
/// <summary>
/// Represents an X509 certificate
/// </summary>
public X509Certificate[] X509PrivateKeys { set; get; }

/// <summary>
/// Certificate's public key
/// </summary>
public ICipherParameters PublicKey { set; get; }
}
}

using System;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;

namespace EncryptPublicKey
{
/// <summary>
/// A Personal Information Exchange File Reader
/// </summary>
public class PfxReader
{
X509Certificate[] _chain;
AsymmetricKeyParameter _asymmetricKeyParameter;

/// <summary>
/// Reads A Personal Information Exchange File.
/// </summary>
/// <param name="pfxPath">Certificate file's path</param>
/// <param name="pfxPassword">Certificate file's password</param>
public PfxData ReadCertificate(string pfxPath, string pfxPassword)
{
using (var stream = new FileStream(pfxPath, FileMode.Open, FileAccess.Read))
{
var pkcs12Store = new Pkcs12Store(stream, pfxPassword.ToCharArray());
var alias = findThePublicKey(pkcs12Store);
_asymmetricKeyParameter = pkcs12Store.GetKey(alias).Key;
constructChain(pkcs12Store, alias);
return new PfxData { X509PrivateKeys = _chain, PublicKey = _asymmetricKeyParameter };
}
}

private void constructChain(Pkcs12Store pkcs12Store, string alias)
{
var certificateChains = pkcs12Store.GetCertificateChain(alias);
_chain = new X509Certificate[certificateChains.Length];

for (int k = 0; k < certificateChains.Length; ++k)
_chain[k] = certificateChains[k].Certificate;
}

private static string findThePublicKey(Pkcs12Store pkcs12Store)
{
string alias = string.Empty;
foreach (string entry in pkcs12Store.Aliases)
{
if (pkcs12Store.IsKeyEntry(entry) && pkcs12Store.GetKey(entry).Key.IsPrivate)
{
alias = entry;
break;
}
}

if (string.IsNullOrEmpty(alias))
throw new NullReferenceException("Provided certificate is invalid.");

return alias;
}
}
}


اکنون رمزنگاری فایل PDF تولیدی توسط کلید عمومی، به سادگی چند سطر کد زیر خواهد بود:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace EncryptPublicKey
{
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));

var certs = new PfxReader().ReadCertificate(@"D:\path\cert.pfx", "123");
pdfWriter.SetEncryption(
certs: certs.X509PrivateKeys,
permissions: new int[] { PdfWriter.ALLOW_PRINTING, PdfWriter.ALLOW_COPY },
encryptionType: PdfWriter.ENCRYPTION_AES_128);

pdfDoc.Open();

pdfDoc.Add(new Phrase("tst 0"));
pdfDoc.NewPage();
pdfDoc.Add(new Phrase("tst 1"));
}

Process.Start("Test.pdf");
}
}
}

پیش از فراخوانی متد Open باید تنظیمات رمزنگاری مشخص شوند. در اینجا ابتدا فایل PFX خوانده شده و کلیدهای عمومی و خصوصی آن استخراج می‌شوند. سپس به متد SetEncryption جهت استفاده نهایی ارسال خواهند شد.

نحوه استفاده از این نوع فایل‌های رمزنگاری شده:
اگر سعی در گشودن این فایل رمزنگاری شده نمائیم با خطای زیر مواجه خواهیم شد:


کاربران برای اینکه بتوانند این فایل‌های PDF را بار کنند نیاز است تا فایل PFX شما را در سیستم خود نصب کنند. ویندوز فایل‌های PFX را می‌شناسد و نصب آن‌ها با دوبار کلیک بر روی فایل و چندبار کلیک بر روی دکمه‌ی Next و وارد کردن کلمه عبور آن، به پایان می‌رسد.

سؤال: آیا می‌توان فایل‌های PDF موجود را هم به همین روش رمزنگاری کرد؟
بله. iTextSharp علاوه بر PdfWriter دارای PdfReader نیز می‌باشد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace EncryptPublicKey
{
class Program
{
static void Main(string[] args)
{
PdfReader reader = new PdfReader("TestDec.pdf");
using (var stamper = new PdfStamper(reader, new FileStream("TestEnc.pdf", FileMode.Create)))
{
var certs = new PfxReader().ReadCertificate(@"D:\path\cert.pfx", "123");
stamper.SetEncryption(
certs: certs.X509PrivateKeys,
permissions: new int[] { PdfWriter.ALLOW_PRINTING, PdfWriter.ALLOW_COPY },
encryptionType: PdfWriter.ENCRYPTION_AES_128);
stamper.Close();
}

Process.Start("TestEnc.pdf");
}
}
}


سؤال: آیا می‌توان نصب کلید عمومی را خودکار کرد؟
سورس برنامه SelfCert که معرفی شد، در دسترس است. این برنامه قابلیت انجام نصب خودکار مجوزها را دارد.

مطالب
اضافه کردن سعی مجدد به اجرای عملیات Migration در EF Core
در حین کار با بانک‌های اطلاعاتی، ممکن است Timeout رخ دهد، یا اتصال برای لحظه‌ای قطع شود و یا خطای قفل بودن جدولی مشاهده شود و امثال این‌ها. در این حالات، اعمال نتیجه‌ی عملیات Migration با شکست مواجه خواهد شد. بنابراین اضافه کردن امکان سعی مجدد عملیات شکست خورده به آن، ضروری است.


پشتیبانی توکار EF Core از سعی مجدد یک عملیات شکست خورده

EF Core بدون نیاز به کتابخانه و یا راه حل ثالثی می‌تواند یک عملیات شکست خورده را مجددا اجرا کند. برای اینکار تنها کافی است تنظیمات زیر را به پروایدر مورد استفاده اضافه کنید؛ برای مثال در مورد SQL Server به صورت زیر است:
public class Startup
{
    // Other code ...
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddDbContext<CatalogContext>(options =>
        {
            options.UseSqlServer(Configuration["ConnectionString"],
            sqlServerOptionsAction: sqlOptions =>
            {
                sqlOptions.EnableRetryOnFailure(
                maxRetryCount: 10,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null);
            });
        });
    }
//...
}
با تنظیم پارامترهای متد EnableRetryOnFailure در اینجا، یک عملیات شکست خورده، حداکثر 10 بار با یک مکث 30 ثانیه‌ای در بین آن‌ها، تکرار می‌شود.


مشکل! EnableRetryOnFailure به عملیات Migration اعمال نمی‌شود!

بر اساس نظر تیم EF Core، تنظیمات تکرار یک عملیات شکست خورده، در زمان اعمال نتیجه‌ی عملیات Migrations ندید گرفته می‌شوند. بنابراین اگر در ابتدای اجرای برنامه قصد اجرای متد ()context.Database.Migrate را دارید تا از اعمال آخرین Migrations موجود اطمینان حاصل کنید و در این بین Timeout ای رخ دهد، این عملیات مجددا تکرار نخواهد شد و این مساله کار کردن با نگارش‌های جدید یک برنامه را غیرممکن می‌کند؛ چون برنامه‌ی جدید، بر اساس ساختار جدید بانک اطلاعاتی طراحی شده قرار است کار کند و به روز رسانی آن با شکست مواجه شده‌است.


اضافه کردن سعی مجدد در اجرای یک عملیات Migration شکست خورده با استفاده از Polly

در مورد کتابخانه‌ی Polly پیشتر در مطلب «پیاده سازی مکانیسم سعی مجدد (Retry)» بحث شده‌است. در اینجا از امکانات این کتابخانه برای سعی مجدد در اجرای عملیات شکست خورده استفاده می‌کنیم:
public static IHost MigrateDbContext<TContext>(this IHost host)  where TContext : DbContext
{
    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        var logger = services.GetRequiredService<ILogger<TContext>>();
        var context = services.GetService<TContext>();

        logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}");

        var retry = Policy.Handle<Exception>().WaitAndRetry(new[]
            {
                TimeSpan.FromSeconds(5),
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(15),
            });

        retry.Execute(() =>
            {
                context.Database.Migrate();
            });

        logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}");
    }
    return host;
}
- هدف از این متد الحاقی فراهم آوردن روشی برای اجرای به روز رسانی ساختار بانک اطلاعاتی، در زمان آغاز برنامه‌است. به همین جهت به صورت افزونه‌ای برای IHost تعریف شده‌است.
- همچنین چون DbContext با طول عمر Scoped تعریف می‌شود، نیاز است در اینجا و خارج از طول عمر یک درخواست، این Scope را به صورت دستی ایجاد و Dispose کرد.
- در این متد با استفاده از امکانات کتابخانه‌ی Polly، در صورتیکه هر گونه خطایی رخ دهد، عملیات Migration سه بار با فواصل زمانی 5، 10 و 15 ثانیه، تکرار خواهد شد.

روش اعمال آن نیز به متد Main برنامه به صورت زیر است:
public static void Main(string[] args)
{
    CreateHostBuilder(args)
    .Build()
    .MigrateDbContext<AppDbContext>()
    .Run();
}
مطالب
Identity 2.0 : تایید حساب های کاربری و احراز هویت دو مرحله ای
در پست قبلی نگاهی اجمالی به انتشار نسخه جدید Identity Framework داشتیم. نسخه جدید تغییرات چشمگیری را در فریم ورک بوجود آورده و قابلیت‌های جدیدی نیز عرضه شده‌اند. دو مورد از این قابلیت‌ها که پیشتر بسیار درخواست شده بود، تایید حساب‌های کاربری (Account Validation) و احراز هویت دو مرحله ای (Two-Factor Authorization) بود. در این پست راه اندازی این دو قابلیت را بررسی می‌کنیم.

تیم ASP.NET Identity پروژه نمونه ای را فراهم کرده است که می‌تواند بعنوان نقطه شروعی برای اپلیکیشن‌های MVC استفاده شود. پیکربندی‌های لازم در این پروژه انجام شده‌اند و برای استفاده از فریم ورک جدید آماده است.


شروع به کار : پروژه نمونه را توسط NuGet ایجاد کنید

برای شروع یک پروژه ASP.NET خالی ایجاد کنید (در دیالوگ قالب‌ها گزینه Empty را انتخاب کنید). سپس کنسول Package Manager را باز کرده و دستور زیر را اجرا کنید.

PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre

پس از اینکه NuGet کارش را به اتمام رساند باید پروژه ای با ساختار متداول پروژه‌های ASP.NET MVC داشته باشید. به تصویر زیر دقت کنید.


همانطور که می‌بینید ساختار پروژه بسیار مشابه پروژه‌های معمول MVC است، اما آیتم‌های جدیدی نیز وجود دارند. فعلا تمرکز اصلی ما روی فایل IdentityConfig.cs است که در پوشه App_Start قرار دارد.

اگر فایل مذکور را باز کنید و کمی اسکرول کنید تعاریف دو کلاس سرویس را مشاهده می‌کنید: EmailService و SmsService.

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your email service here to send an email.
        return Task.FromResult(0);
    }
}

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your sms service here to send a text message.
        return Task.FromResult(0);
    }
}

اگر دقت کنید هر دو کلاس قرارداد IIdentityMessageService را پیاده سازی می‌کنند. می‌توانید از این قرارداد برای پیاده سازی سرویس‌های اطلاع رسانی ایمیلی، پیامکی و غیره استفاده کنید. در ادامه خواهیم دید چگونه این دو سرویس را بسط دهیم.


یک حساب کاربری مدیریتی پیش فرض ایجاد کنید

پیش از آنکه بیشتر جلو رویم نیاز به یک حساب کاربری در نقش مدیریتی داریم تا با اجرای اولیه اپلیکیشن در دسترس باشد. کلاسی بنام ApplicationDbInitializer در همین فایل وجود دارد که هنگام اجرای اولیه و یا تشخیص تغییرات در مدل دیتابیس، اطلاعاتی را Seed می‌کند.

public class ApplicationDbInitializer 
    : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }

    //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role        
    public static void InitializeIdentityForEF(ApplicationDbContext db) 
    {
        var userManager = 
           HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
        
        var roleManager = 
            HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();

        const string name = "admin@admin.com";
        const string password = "Admin@123456";
        const string roleName = "Admin";

        //Create Role Admin if it does not exist
        var role = roleManager.FindByName(roleName);

        if (role == null) {
            role = new IdentityRole(roleName);
            var roleresult = roleManager.Create(role);
        }

        var user = userManager.FindByName(name);

        if (user == null) {
            user = new ApplicationUser { UserName = name, Email = name };
            var result = userManager.Create(user, password);
            result = userManager.SetLockoutEnabled(user.Id, false);
        }

        // Add user admin to Role Admin if not already added
        var rolesForUser = userManager.GetRoles(user.Id);

        if (!rolesForUser.Contains(role.Name)) {
            var result = userManager.AddToRole(user.Id, role.Name);
        }
    }
}
همانطور که می‌بینید این قطعه کد ابتدا نقشی بنام Admin می‌سازد. سپس حساب کاربری ای، با اطلاعاتی پیش فرض ایجاد شده و بدین نقش منتسب می‌گردد. اطلاعات کاربر را به دلخواه تغییر دهید و ترجیحا از یک آدرس ایمیل زنده برای آن استفاده کنید.


تایید حساب‌های کاربری : چگونه کار می‌کند

بدون شک با تایید حساب‌های کاربری توسط ایمیل آشنا هستید. حساب کاربری ای ایجاد می‌کنید و ایمیلی به آدرس شما ارسال می‌شود که حاوی لینک فعالسازی است. با کلیک کردن این لینک حساب کاربری شما تایید شده و می‌توانید به سایت وارد شوید.

اگر به کنترلر AccountController در این پروژه نمونه مراجعه کنید متد Register را مانند لیست زیر می‌یابید.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);

        if (result.Succeeded)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);

            var callbackUrl = Url.Action(
                "ConfirmEmail", 
                "Account", 
                new { userId = user.Id, code = code }, 
                protocol: Request.Url.Scheme);

            await UserManager.SendEmailAsync(
                user.Id, 
                "Confirm your account", 
                "Please confirm your account by clicking this link: <a href=\"" 
                + callbackUrl + "\">link</a>");

            ViewBag.Link = callbackUrl;

            return View("DisplayEmail");
        }
        AddErrors(result);
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}
اگر به قطعه کد بالا دقت کنید فراخوانی متد UserManager.SendEmailAsync را می‌یابید که آرگومانهایی را به آن پاس می‌دهیم. در کنترلر جاری، آبجکت UserManager یک خاصیت (Property) است که وهله ای از نوع ApplicationUserManager را باز می‌گرداند. اگر به فایل IdentityConfig.cs مراجعه کنید تعاریف این کلاس را خواهید یافت. در این کلاس، متد استاتیک ()Create وهله ای از ApplicationUserManager را می‌سازد که در همین مرحله سرویس‌های پیام رسانی پیکربندی می‌شوند.

public static ApplicationUserManager Create(
    IdentityFactoryOptions<ApplicationUserManager> options, 
    IOwinContext context)
{
    var manager = new ApplicationUserManager(
        new UserStore<ApplicationUser>(
            context.Get<ApplicationDbContext>()));

    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };

    // Configure validation logic for passwords
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6, 
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };

    // Configure user lockout defaults
    manager.UserLockoutEnabledByDefault = true;
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;

    // Register two factor authentication providers. This application 
    // uses Phone and Emails as a step of receiving a code for verifying the user
    // You can write your own provider and plug in here.
    manager.RegisterTwoFactorProvider(
        "PhoneCode", 
        new PhoneNumberTokenProvider<ApplicationUser>
    {
        MessageFormat = "Your security code is: {0}"
    });

    manager.RegisterTwoFactorProvider(
        "EmailCode", 
        new EmailTokenProvider<ApplicationUser>
    {
        Subject = "SecurityCode",
        BodyFormat = "Your security code is {0}"
    });

    manager.EmailService = new EmailService();
    manager.SmsService = new SmsService();

    var dataProtectionProvider = options.DataProtectionProvider;

    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider = 
            new DataProtectorTokenProvider<ApplicationUser>(
                dataProtectionProvider.Create("ASP.NET Identity"));
    }

    return manager;
}

در قطعه کد بالا کلاس‌های EmailService و SmsService روی وهله ApplicationUserManager تنظیم می‌شوند.

manager.EmailService = new EmailService();
manager.SmsService = new SmsService();

درست در بالای این کد‌ها می‌بینید که چگونه تامین کنندگان احراز هویت دو مرحله ای (مبتنی بر ایمیل و پیامک) رجیستر می‌شوند.

// Register two factor authentication providers. This application 
// uses Phone and Emails as a step of receiving a code for verifying the user
// You can write your own provider and plug in here.
manager.RegisterTwoFactorProvider(
    "PhoneCode", 
    new PhoneNumberTokenProvider<ApplicationUser>
{
    MessageFormat = "Your security code is: {0}"
});

manager.RegisterTwoFactorProvider(
    "EmailCode", 
    new EmailTokenProvider<ApplicationUser>
    {
        Subject = "SecurityCode",
        BodyFormat = "Your security code is {0}"
    });

تایید حساب‌های کاربری توسط ایمیل و احراز هویت دو مرحله ای توسط ایمیل و/یا پیامک نیاز به پیاده سازی هایی معتبر از قراردارد IIdentityMessageService دارند.

پیاده سازی سرویس ایمیل توسط ایمیل خودتان

پیاده سازی سرویس ایمیل نسبتا کار ساده ای است. برای ارسال ایمیل‌ها می‌توانید از اکانت ایمیل خود و یا سرویس هایی مانند SendGrid استفاده کنید. بعنوان مثال اگر بخواهیم سرویس ایمیل را طوری پیکربندی کنیم که از یک حساب کاربری Outlook استفاده کند، مانند زیر عمل خواهیم کرد.

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var credentialUserName = "yourAccount@outlook.com";
        var sentFrom = "yourAccount@outlook.com";
        var pwd = "yourApssword";

        // Configure the client:
        System.Net.Mail.SmtpClient client = 
            new System.Net.Mail.SmtpClient("smtp-mail.outlook.com");

        client.Port = 587;
        client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
        client.UseDefaultCredentials = false;

        // Creatte the credentials:
        System.Net.NetworkCredential credentials = 
            new System.Net.NetworkCredential(credentialUserName, pwd);

        client.EnableSsl = true;
        client.Credentials = credentials;

        // Create the message:
        var mail = 
            new System.Net.Mail.MailMessage(sentFrom, message.Destination);

        mail.Subject = message.Subject;
        mail.Body = message.Body;

        // Send:
        return client.SendMailAsync(mail);
    }
}
تنظیمات SMTP میزبان شما ممکن است متفاوت باشد اما مطمئنا می‌توانید مستندات لازم را پیدا کنید. اما در کل رویکرد مشابهی خواهید داشت.


پیاده سازی سرویس ایمیل با استفاده از SendGrid

سرویس‌های ایمیل متعددی وجود دارند اما یکی از گزینه‌های محبوب در جامعه دات نت SendGrid است. این سرویس API قدرتمندی برای زبان‌های برنامه نویسی مختلف فراهم کرده است. همچنین یک Web API مبتنی بر HTTP نیز در دسترس است. قابلیت دیگر اینکه این سرویس مستقیما با Windows Azure یکپارچه می‌شود.

می توانید در سایت SendGrid یک حساب کاربری رایگان بعنوان توسعه دهنده بسازید. پس از آن پیکربندی سرویس ایمیل با مرحله قبل تفاوت چندانی نخواهد داشت. پس از ایجاد حساب کاربری توسط تیم پشتیبانی SendGrid با شما تماس گرفته خواهد شد تا از صحت اطلاعات شما اطمینان حاصل شود. برای اینکار چند گزینه در اختیار دارید که بهترین آنها ایجاد یک اکانت ایمیل در دامنه وب سایتتان است. مثلا اگر هنگام ثبت نام آدرس وب سایت خود را www.yourwebsite.com وارد کرده باشید، باید ایمیلی مانند info@yourwebsite.com ایجاد کنید و توسط ایمیل فعالسازی آن را تایید کند تا تیم پشتیبانی مطمئن شود صاحب امتیاز این دامنه خودتان هستید.

تنها چیزی که در قطعه کد بالا باید تغییر کند اطلاعات حساب کاربری و تنظیمات SMTP است. توجه داشته باشید که نام کاربری و آدرس فرستنده در اینجا متفاوت هستند. در واقع می‌توانید از هر آدرسی بعنوان آدرس فرستنده استفاده کنید.

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var sendGridUserName = "yourSendGridUserName";
        var sentFrom = "whateverEmailAdressYouWant";
        var sendGridPassword = "YourSendGridPassword";

        // Configure the client:
        var client = 
            new System.Net.Mail.SmtpClient("smtp.sendgrid.net", Convert.ToInt32(587));

        client.Port = 587;
        client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
        client.UseDefaultCredentials = false;

        // Creatte the credentials:
        System.Net.NetworkCredential credentials = 
            new System.Net.NetworkCredential(credentialUserName, pwd);

        client.EnableSsl = true;
        client.Credentials = credentials;

        // Create the message:
        var mail = 
            new System.Net.Mail.MailMessage(sentFrom, message.Destination);

        mail.Subject = message.Subject;
        mail.Body = message.Body;

        // Send:
        return client.SendMailAsync(mail);
    }
}
حال می‌توانیم سرویس ایمیل را تست کنیم.


آزمایش تایید حساب‌های کاربری توسط سرویس ایمیل

ابتدا اپلیکیشن را اجرا کنید و سعی کنید یک حساب کاربری جدید ثبت کنید. دقت کنید که از آدرس ایمیلی زنده که به آن دسترسی دارید استفاده کنید. اگر همه چیز بدرستی کار کند باید به صفحه ای مانند تصویر زیر هدایت شوید.

همانطور که مشاهده می‌کنید پاراگرافی در این صفحه وجود دارد که شامل لینک فعالسازی است. این لینک صرفا جهت تسهیل کار توسعه دهندگان درج می‌شود و هنگام توزیع اپلیکیشن باید آن را حذف کنید. در ادامه به این قسمت باز می‌گردیم. در این مرحله ایمیلی حاوی لینک فعالسازی باید برای شما ارسال شده باشد.

پیاده سازی سرویس SMS

برای استفاده از احراز هویت دو مرحله ای پیامکی نیاز به یک فراهم کننده SMS دارید، مانند Twilio . مانند SendGrid این سرویس نیز در جامعه دات نت بسیار محبوب است و یک C# API قدرتمند ارائه می‌کند. می‌توانید حساب کاربری رایگانی بسازید و شروع به کار کنید.

پس از ایجاد حساب کاربری یک شماره SMS، یک شناسه SID و یک شناسه Auth Token به شما داده می‌شود. شماره پیامکی خود را می‌توانید پس از ورود به سایت و پیمایش به صفحه Numbers مشاهده کنید.

شناسه‌های SID و Auth Token نیز در صفحه Dashboard قابل مشاهده هستند.

اگر دقت کنید کنار شناسه Auth Token یک آیکون قفل وجود دارد که با کلیک کردن روی آن شناسه مورد نظر نمایان می‌شود.

حال می‌توانید از سرویس Twilio در اپلیکیشن خود استفاده کنید. ابتدا بسته NuGet مورد نیاز را نصب کنید.

PM> Install-Package Twilio
پس از آن فضای نام Twilio را به بالای فایل IdentityConfig.cs اضافه کنید و سرویس پیامک را پیاده سازی کنید.

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        string AccountSid = "YourTwilioAccountSID";
        string AuthToken = "YourTwilioAuthToken";
        string twilioPhoneNumber = "YourTwilioPhoneNumber";

        var twilio = new TwilioRestClient(AccountSid, AuthToken);

        twilio.SendSmsMessage(twilioPhoneNumber, message.Destination, message.Body); 

        // Twilio does not return an async Task, so we need this:
        return Task.FromResult(0);
    }
}

حال که سرویس‌های ایمیل و پیامک را در اختیار داریم می‌توانیم احراز هویت دو مرحله ای را تست کنیم.


آزمایش احراز هویت دو مرحله ای

پروژه نمونه جاری طوری پیکربندی شده است که احراز هویت دو مرحله ای اختیاری است و در صورت لزوم می‌تواند برای هر کاربر بصورت جداگانه فعال شود. ابتدا توسط حساب کاربری مدیر، یا حساب کاربری ای که در قسمت تست تایید حساب کاربری ایجاد کرده اید وارد سایت شوید. سپس در سمت راست بالای صفحه روی نام کاربری خود کلیک کنید. باید صفحه ای مانند تصویر زیر را مشاهده کنید.

در این قسمت باید احراز هویت دو مرحله ای را فعال کنید و شماره تلفن خود را ثبت نمایید. پس از آن یک پیام SMS برای شما ارسال خواهد شد که توسط آن می‌توانید پروسه را تایید کنید. اگر همه چیز بدرستی کار کند این مراحل چند ثانیه بیشتر نباید زمان بگیرد، اما اگر مثلا بیش از 30 ثانیه زمان برد احتمالا اشکالی در کار است.

حال که احراز هویت دو مرحله ای فعال شده از سایت خارج شوید و مجددا سعی کنید به سایت وارد شوید. در این مرحله یک انتخاب به شما داده می‌شود. می‌توانید کد احراز هویت دو مرحله ای خود را توسط ایمیل یا پیامک دریافت کنید.

پس از اینکه گزینه خود را انتخاب کردید، کد احراز هویت دو مرحله ای برای شما ارسال می‌شود که توسط آن می‌توانید پروسه ورود به سایت را تکمیل کنید.

حذف میانبرهای آزمایشی

همانطور که گفته شد پروژه نمونه شامل میانبرهایی برای تسهیل کار توسعه دهندگان است. در واقع اصلا نیازی به پیاده سازی سرویس‌های ایمیل و پیامک ندارید و می‌توانید با استفاده از این میانبرها حساب‌های کاربری را تایید کنید و کدهای احراز هویت دو مرحله ای را نیز مشاهده کنید. اما قطعا این میانبرها پیش از توزیع اپلیکیشن باید حذف شوند.

بدین منظور باید نماها و کدهای مربوطه را ویرایش کنیم تا اینگونه اطلاعات به کلاینت ارسال نشوند. اگر کنترلر AccountController را باز کنید و به متد ()Register بروید با کد زیر مواجه خواهید شد.

if (result.Succeeded)
{
    var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
    var callbackUrl = 
        Url.Action("ConfirmEmail", "Account", 
            new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);

    await UserManager.SendEmailAsync(user.Id, "Confirm your account", 
        "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");

    // This should not be deployed in production:
    ViewBag.Link = callbackUrl;

    return View("DisplayEmail");
}

AddErrors(result);
همانطور که می‌بینید پیش از بازگشت از این متد، متغیر callbackUrl به ViewBag اضافه می‌شود. این خط را Comment کنید یا به کلی حذف نمایید.

نمایی که این متد باز می‌گرداند یعنی DisplayEmail.cshtml نیز باید ویرایش شود.

@{
    ViewBag.Title = "DEMO purpose Email Link";
}

<h2>@ViewBag.Title.</h2>

<p class="text-info">
    Please check your email and confirm your email address.
</p>

<p class="text-danger">
    For DEMO only: You can click this link to confirm the email: <a href="@ViewBag.Link">link</a>
    Please change this code to register an email service in IdentityConfig to send an email.
</p>

متد دیگری که در این کنترلر باید ویرایش شود ()VerifyCode است که کد احراز هویت دو مرحله ای را به صفحه مربوطه پاس می‌دهد.

[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, string returnUrl)
{
    // Require that the user has already logged in via username/password or external login
    if (!await SignInHelper.HasBeenVerified())
    {
        return View("Error");
    }

    var user = 
        await UserManager.FindByIdAsync(await SignInHelper.GetVerifiedUserIdAsync());

    if (user != null)
    {
        ViewBag.Status = 
            "For DEMO purposes the current " 
            + provider 
            + " code is: " 
            + await UserManager.GenerateTwoFactorTokenAsync(user.Id, provider);
    }

    return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl });
}

همانطور که می‌بینید متغیری بنام Status به ViewBag اضافه می‌شود که باید حذف شود.

نمای این متد یعنی VerifyCode.cshtml نیز باید ویرایش شود.

@model IdentitySample.Models.VerifyCodeViewModel

@{
    ViewBag.Title = "Enter Verification Code";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.Hidden("provider", @Model.Provider)
    <h4>@ViewBag.Status</h4>
    <hr />

    <div class="form-group">
        @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Code, new { @class = "form-control" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <div class="checkbox">
                @Html.CheckBoxFor(m => m.RememberBrowser)
                @Html.LabelFor(m => m.RememberBrowser)
            </div>
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Submit" />
        </div>
    </div>
}

در این فایل کافی است ViewBag.Status را حذف کنید.


از تنظیمات ایمیل و SMS محافظت کنید

در مثال جاری اطلاعاتی مانند نام کاربری و کلمه عبور، شناسه‌های SID و Auth Token همگی در کد برنامه نوشته شده اند. بهتر است چنین مقادیری را بیرون از کد اپلیکیشن نگاه دارید، مخصوصا هنگامی که پروژه را به سرویس کنترل ارسال می‌کند (مثلا مخازن عمومی مثل GitHub). بدین منظور می‌توانید یکی از پست‌های اخیر را مطالعه کنید.

بازخوردهای دوره
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
البته راه دیگری هم پیدا کردم.
به نظرم جالب اومد.
ابتدا یک اکشن فیلتر تعریف شده و در ان هدر درخواست خوانده شده و با محتوی کوکی چک می‌شود در صورتی که برابر باشند عمل انجام خواهد شد با این تفاوت باید توکن در زمان ارسال در هدر درخواست قرار گیرد مانند زیر:
کد اکشن فیلتر:
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
    {
#region Methods (1) 

// Public Methods (1) 

        /// <summary>
        /// Called when [action executing].
        /// </summary>
        /// <param name="actionContext">The action context.</param>
        public void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var cookieName = AntiForgeryConfig.CookieName;
                var headers = actionContext.Request.Headers;
                var cookie = headers
                    .GetCookies()
                    .Select(c => c[AntiForgeryConfig.CookieName])
                    .FirstOrDefault();
                var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
                AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
            }
            catch
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
            }
        }

#endregion Methods 
    }
سپس در اکشن دلخواه باید این اکشن فیلتر را بکار برد (در درخواست‌های json)
[HttpPost]
        //[ValidateAntiForgeryToken]
        [ValidateJsonAntiForgeryToken]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult DeletePhoto(int? id)
        {
            string ret="error";
            if (id != null && id > 0)
            {
                bool result = _imageGalleryService.Value.Photos.Value.Delete(id.ToInt32());
                ret = result ? "ok" : "nok";
            }
            return Content(ret);
            //return Json(new { ret });
        }
سپس در طرف ویو به صورت زیر عمل شود
$.ajax({
                       ....
                    type: "POST",
                    url: '@Url',
                    data: JSON.stringify({ id: Id }),
                    contentType: "application/json; charset=utf-8",
                    // این قسمت اضافه شود
                    headers: { __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() },
                    dataType: "json",
                     ..........
امیدوارم برای دوستان هم کاربرد داشته باشد




نظرات مطالب
معرفی OLTP درون حافظه‌ای در SQL Server 2014

جناب نصیری با تشکر از مقاله مفیدتون لازم می‌دونم در جهت تکمیل مباحث به چند نکته اشاره کنم

1- « اگر حالت SCHEMA_AND_DATA انتخاب شود، اطلاعات شما پس از ری‌استارت سرور نیز در دسترس خواهد بود. این اطلاعات به صورت خودکار از لاگ تراکنش‌ها بازیابی شده و مجددا در حافظه قرار می‌گیرند   «.

بازیابی اطلاعات مربوط به تراکنش‌هایی که به ازای In Memory OLTP است بوسیله Data File + Delta File و Log File می‌باشد. در صورتیکه Schema_AND_Data را به ازای این نوع جداول فعال کنید داده‌های شما در Data File و داده‌های حذف شده در Delta File ثبت می‌گردد. مکانیزم Log File برای In Memory OLTP همچنان مانند جداول Disk base وجود دارد اما با بهینه سازی مناسب مانند ثبت Log Record کمتر به ازای عملیات کاربران و... 

2- در جایی دیگر در متن اشاره شده که In Memory OLTP اجازه استفاده از Identity را به کاربر نمی‌دهد باید اشاره کنم که این موضوع برای نسخه CTP بوده است در نسخه RTM این قابلیت وجود دارد . لازم می‌دانم اشاره کنم که  در Books Online جایی گفته شده که امکان استفاده وجود ندارد و در جایی هم گفته شده وجود دارد .

به مثال زیر دقت کنید

CREATE TABLE test(
[ID] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT=10000),
N1 NVARCHAR(100),
N2 NVARCHAR(100),
N3 NVARCHAR(100)
) WITH (MEMORY_OPTIMIZED=ON,DURABILITY = SCHEMA_AND_DATA)
GO
این مثال در SQL Server 2014 RTM Edition قابل اجرا است اما یکسری محدودیت داریم . مثلاً مقدار شروع و گام افزایش باید 1 باشد .  

باز هم از مطالب خوب شما متشکرم مقاله مفیدی بود .

بازخوردهای پروژه‌ها
اضافه کردن چند متد الحاقی
با سلام من توی پروژه هام چند تا متد استفاده می‌کنم. خوشحال می‌شم نظر شمارو هم بدونم و به پروژه اتون اضافه اش کنید.

این متد برای محاسبه اختلاف دو تاریخ استفاده میشه، البته منبعش یادم نیست این متد از کجا گرفتم:
/// <summary>
        /// DateDiff in SQL style.
        /// Datepart implemented:
        ///     "year" (abbr. "yy", "yyyy"),
        ///     "quarter" (abbr. "qq", "q"),
        ///     "month" (abbr. "mm", "m"),
        ///     "day" (abbr. "dd", "d"),
        ///     "week" (abbr. "wk", "ww"),
        ///     "hour" (abbr. "hh"),
        ///     "minute" (abbr. "mi", "n"),
        ///     "second" (abbr. "ss", "s"),
        ///     "millisecond" (abbr. "ms").
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="datePart"></param>
        /// <param name="endDate"></param>
        /// <returns></returns>
        public static Int64 DateDiff(this DateTime startDate, String datePart, DateTime endDate)
        {
            Int64 dateDiffVal;
            System.Globalization.Calendar cal = System.Threading.Thread.CurrentThread.CurrentCulture.Calendar;
            TimeSpan ts = new TimeSpan(endDate.Ticks - startDate.Ticks);
            switch (datePart.ToLower().Trim())
            {
                #region year

                case "year":
                case "yy":
                case "yyyy":
                    dateDiffVal = cal.GetYear(endDate) - cal.GetYear(startDate);
                    break;

                #endregion

                #region quarter

                case "quarter":
                case "qq":
                case "q":
                    dateDiffVal = (((cal.GetYear(endDate) - cal.GetYear(startDate)) * 4) + ((cal.GetMonth(endDate) - 1) / 3)) - ((cal.GetMonth(startDate) - 1) / 3);
                    break;

                #endregion

                #region month

                case "month":
                case "mm":
                case "m":
                    dateDiffVal = ((cal.GetYear(endDate) - cal.GetYear(startDate)) * 12 + cal.GetMonth(endDate)) - cal.GetMonth(startDate);
                    break;

                #endregion

                #region day

                case "day":
                case "d":
                case "dd":
                    dateDiffVal = (Int64)ts.TotalDays;
                    break;

                #endregion

                #region week

                case "week":
                case "wk":
                case "ww":
                    dateDiffVal = (Int64)(ts.TotalDays / 7);
                    break;

                #endregion

                #region hour

                case "hour":
                case "hh":
                    dateDiffVal = (Int64)ts.TotalHours;
                    break;

                #endregion

                #region minute

                case "minute":
                case "mi":
                case "n":
                    dateDiffVal = (Int64)ts.TotalMinutes;
                    break;

                #endregion

                #region second

                case "second":
                case "ss":
                case "s":
                    dateDiffVal = (Int64)ts.TotalSeconds;
                    break;

                #endregion

                #region millisecond

                case "millisecond":
                case "ms":
                    dateDiffVal = (Int64)ts.TotalMilliseconds;
                    break;

                #endregion

                default:
                    throw new Exception(String.Format("DatePart \"{0}\" is unknown", datePart));
            }
            return dateDiffVal;
        }

بررسی اینکه آیا رشته ورودی به الگوی مورد نظر مطابقت دارد یا خیر؟
/// <summary>
        /// بررسی اینکه رشته وارد شده با قالب مورد نظر مطابقت دارد یا خیر
        /// </summary>
        /// <param name="source">متن وارد شده</param>
        /// <param name="regexTemplate">قالب مورد نظر</param>
        /// <returns></returns>
        public static Boolean IsMatchTemplate(this String source, String regexTemplate)
        {
            var regex = new Regex(regexTemplate);
            return regex.IsMatch(source);
        }
البته متدهای زیادی دارم می‌ترسم وجهه خوبی نداشته باشه همش اینجا بذارم، با اجازه مدیریت سایت در صورت تمایل ایمیلتون بهم پیام خصوصی بدین، که متد هارو براتون ارسال کنم.

نظرات مطالب
روش‌هایی برای بهبود سرعت برنامه‌های مبتنی بر Entity framework
احتمالا برنامه بزرگ شما RAM زیاد مصرف می‌کند و هاست هم تنظیم کرده که اگر مصرف RAM به یک حد خاصی رسید، IIS برنامه را ری استارت کند. به همین جهت هست که تاخیر 20 ثانیه‌ای را مدام مشاهده می‌کنید (چون برنامه مدام ری استارت می‌شود). با استفاده از ELMAH، متدهای application_start و application_end فایل global.asax را لاگ کنید و بررسی کنید چندبار اجرا شده‌اند.
نظرات مطالب
نحوه ایجاد یک تصویر امنیتی (Captcha) با حروف فارسی در ASP.Net MVC
با سلام.
سپاس از مقاله خوبتون. 
در کلاینت همه چی درست کار میکند ولی وقتی آنرا درون هاست واقعی publish میکنم، تصویر نمایش داده نمی‌شود و خطایی که توسط elmah لاگ می‌شود به صورت زیر است:
The system cannot find the file specified " source="mscorlib" detail="System.Security.Cryptography.CryptographicException: The system cannot find the file specified.

با تشکر.