مطالب
MongoDB #4
مدل کردن داده در MongoDB
داده در MongoDB شمای منعطفی دارد. سندها در یک مجموعه به تعدادی از فیلدها با ساختاری شبیه به هم نیازی ندارند و فیلدهای مشترک در یک سند مجموعه ممکن است نوع‌های داده‌ی متفاوتی را نگهداری کنند.

برخی ملاحظات هنگام طراحی شمای در MongoDB
  • شمای خود را بر اساس نیازمندی‌های کاربر طراحی کنید.
  • آبجکت هایی را که از آنها باهم استفاده می‌کنید، داخل یک سند ترکیب کنید؛ درغیر اینصورت آنها را جدا کنید (اما مطمئن شوید که نباید نیازی به استفاده از Join باشد).
  • داده را بصورت محدود تکثیر کنید؛ چون فضای دیسک ارزانتر است از محاسبه زمان.
  • عمل Join را هنگام ذخیره کردن انجام دهید، نه موقع خواندن.
  • شمای خود را برای بیشترین موارد استفاده بهینه کنید.
  • تجمع‌های پیچیده (Complex Aggregation) را در شمای انجام دهید.

مثال
فرض کنید یک کاربر نیاز به طراحی یک پایگاه داده برای وب سایت خود دارد. تفاوت‌های طراحی شمای بین RDBMS و MongoDB را در ادامه ملاحظه خواهید کرد. وب سایت نیازمندهای زیر را دارد:
  • هر پست یک عنوان یکتا، توضیحات و آدرس اینترنتی دارد.
  • هر پست می‌تواند یک یا چند برچسب داشته باشد.
  • هر پست نام نویسنده و تعداد Like‌ها را دارد.
  • هر پست تعدادی نظر معین را توسط کاربران همراه با نامشان، پیام، تاریخ ثبت و تعداد Likeها، دارد.
  • در هر پست صفر یا چند نظر وجود دارد.
در طراحی شمای توسط RDMBS برای نیازمندی‌های فوق، حداقل سه جدول نیاز است. 

درحالیکه در طراحی شمای توسط MongoDB یک مجموعه از پست را با ساختار زیر خواهیم داشت:
{
   _id: POST_ID
   title: TITLE_OF_POST, 
   description: POST_DESCRIPTION,
   by: POST_BY,
   url: URL_OF_POST,
   tags: [TAG1, TAG2, TAG3],
   likes: TOTAL_LIKES, 
   comments: [
      {
         user:'COMMENT_BY',
         message: TEXT,
         dateCreated: DATE_TIME,
         like: LIKES 
      },
      {
         user:'COMMENT_BY',
         message: TEXT,
         dateCreated: DATE_TIME,
         like: LIKES
      }
   ]
}
بنابراین هنگام نمایش داده در RDBMS نیاز به تهیه Join بین سه جدول دارید؛ اما در MongoDB داده‌ها از یک مجموعه نمایش داده خواهند شد. 
مطالب
پیاده سازی UnitOfWork به وسیله MEF
در این پست قصد دارم یک UnitOfWork به روش MEF پیاده سازی کنم. ORM مورد نظر EntityFramework CodeFirst است. در صورتی که با UnitOfWork , MEF آشنایی ندارید از لینک‌های زیر استفاده کنید:
 برای شروع ابتدا مدل برنامه رو به صورت زیر تعریف کنید.
 public class Category
    {
        public int Id { get; set; }

        public string Title { get; set; }
    }
سپس فایل Map  رو برای مدل بالا به صورت زیر تعریف کنید.
 public class CategoryMap : EntityTypeConfiguration<Entity.Category>
    {
        public CategoryMap()
        {
            ToTable( "Category" );

            HasKey( _field => _field.Id );

            Property( _field => _field.Title )
            .IsRequired();            
        }
    }
برای پیاده سازی الگوی واحد کار ابتدا باید یک اینترفیس به صورت زیر تعریف کنید.
using System.Data.Entity;
using System.Data.Entity.Infrastructure;

namespace DataAccess
{
    public interface IUnitOfWork
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;
        DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class;
        void SaveChanges();     
        void Dispose();
    }
}
DbContext مورد نظر باید اینترفیس مورد نظر را پیاده سازی کند و برای اینکه بتونیم اونو در CompositionContainer اضافه کنیم باید از Export Attribute استفاده کنیم.
چون کلاس DatabaseContext از اینترفیس IUnitOfWork ارث برده است برای همین از InheritedExport استفاده می‌کنیم.
[InheritedExport( typeof( IUnitOfWork ) )]
    public class DatabaseContext : DbContext, IUnitOfWork
    {
        private DbTransaction transaction = null;

        public DatabaseContext()           
        {
            this.Configuration.AutoDetectChangesEnabled = false;
            this.Configuration.LazyLoadingEnabled = true;
        }

        protected override void OnModelCreating( DbModelBuilder modelBuilder )
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

            modelBuilder.AddFormAssembly( Assembly.GetAssembly( typeof( Entity.Map.CategoryMap ) ) );
        }

        public DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class
        {
            return this.Entry<TEntity>();
        }      
    }
نکته قابل ذکر در قسمت OnModelCreating این است که یک Extension Methodبه نام AddFromAssembly (همانند NHibernate) اضافه شده است که از Assembly  مورد نظر تمام کلاس‌های Map رو پیدا می‌کنه و اونو به ModelBuilder اضافه می‌کنه. کد متد به صورت زیر است:
 public static class ModelBuilderExtension
    {
        public static void AddFormAssembly( this DbModelBuilder modelBuilder, Assembly assembly )
        {
            Array.ForEach<Type>( assembly.GetTypes().Where( type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof( EntityTypeConfiguration<> ) ).ToArray(), delegate( Type type )
            {
                dynamic instance = Activator.CreateInstance( type );
                modelBuilder.Configurations.Add( instance );
            } );
        }
    }

برای پیاده سازی قسمت BusinessLogic ابتدا کلاس BusiessBase را در آن قرار دهید:
public class BusinessBase<TEntity> where TEntity : class
    {        
        public BusinessBase( IUnitOfWork unitOfWork )
        {
            this.UnitOfWork = unitOfWork;
        }

        [Import]
        public IUnitOfWork UnitOfWork
        {
            get;
            private set;
        }

        public virtual IEnumerable<TEntity> GetAll()
        {
            return UnitOfWork.Set<TEntity>().AsNoTracking();
        }

        public virtual void Add( TEntity entity )
        {
            try
            {             
                UnitOfWork.Set<TEntity>().Add( entity );
                UnitOfWork.SaveChanges();
            }
            catch
            {              
                throw;
            }
            finally
            {
                UnitOfWork.Dispose();
            }
        }
    }

تمام متد‌های پایه مورد نظر را باید در این کلاس قرار داد که برای مثال من متد Add , GetAll را براتون پیاده سازی کردم. UnitOfWork توسط ImportAttribute مقدار دهی می‌شود و نیاز به وهله سازی از آن نیست 
کلاس Category رو هم باید به صورت زیر اضافه کنید.
 public class Category : BusinessBase<Entity.Category>
    {      
        [ImportingConstructor]
        public Category( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork )
            : base( unitOfWork )
        {
        }
    }
.در انتها باید UI مورد نظر طراحی شود که من در اینجا از Console Application استفاده کردم. یک کلاس به نام Plugin ایجاد کنید  و کد‌های زیر را در آن قرار دهید.
public class Plugin
    {        
        public void Run()
        {
            AggregateCatalog catalog = new AggregateCatalog();

            Container = new CompositionContainer( catalog );

            CompositionBatch batch = new CompositionBatch();

            catalog.Catalogs.Add( new AssemblyCatalog( Assembly.GetExecutingAssembly() ) );

            batch.AddPart( this );

            Container.Compose( batch );
        }

        public CompositionContainer Container 
        {
            get; 
            private set;
        }
    }
در کلاس Plugin  توسط AssemblyCatalog تمام Export Attribute‌های موجود جستجو می‌شود و بعد به عنوان کاتالوگ مورد نظر به Container اضافه می‌شود. انواع Catalog در MEF به شرح زیر است:
  • AssemblyCatalog : در اسمبلی مورد نظر به دنبال تمام Export Attribute‌ها می‌گردد و آن‌ها را به عنوان ExportedValue در Container اضافه می‌کند.
  • TypeCatalog: فقط یک نوع مشخص را به عنوان ExportAttribute در نظر می‌گیرد.
  • DirectoryCatalog :  در یک مسیر مشخص تمام Assembly مورد نظر را از نظر Export Attribute جستجو می‌کند و آن‌ها را به عنوان ExportedValue در Container اضافه می‌کند. 
  • ApplicationCatalog :  در اسمبلی  و فایل‌های (EXE) مورد نظر به دنبال تمام Export Attribute‌ها می‌گردد و آن‌ها را به عنوان ExportedValue در Container اضافه می‌کند. 
  • AggregateCatalog : تمام موارد فوق را Support می‌کند.
کلاس Program  رو به صورت زیر بازنویسی کنید.
  class Program
    {
        static void Main( string[] args )
        {
            Plugin plugin = new Plugin();
            plugin.Run();

            Category category = new Category(plugin.Container.GetExportedValue<IUnitOfWork>());
            category.GetAll().ToList().ForEach( _record => Console.Write( _record.Title ) );
        }
    }
پروژه اجرا کرده و نتیجه رو مشاهده کنید.
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت اول - نیاز به تامین کننده‌ی هویت مرکزی
- Identity server الزاما یک تامین کننده‌ی هویت خارج از شبکه‌ی شما مثل گوگل نیست و به صورت یک برنامه‌ی مجزای داخلی در IIS شبکه‌ی شما قابل نصب و استفاده است (در قسمت‌های بعدی جزئیات نصب و راه اندازی آن بررسی می‌شوند).
- برنامه‌ی شما پس از احراز هویت توسط Identity server، دسترسی به Id کاربر لاگین شده را خواهد داشت. سایر اطلاعات او را هم از طریق Claims دریافت می‌کند. مابقی آن مانند قبل است (کار با Claims هم در قسمت‌های بعدی بررسی می‌شوند).
مطالب
React 16x - قسمت 8 - ترکیب کامپوننت‌ها - بخش 2 - مدیریت state
در ادامه‌ی بحث ترکیب کامپوننت‌ها، پس از نمایش لیستی از کامپوننت‌های شمارشگر و مقدار دهی عدد آغازین آن‌ها، به همراه مدیریت حذف هر ردیف در قسمت قبل، اکنون می‌خواهیم دکمه‌ای را اضافه کنیم تا تمام شمارشگرها را به حالت اول خودشان بازگرداند. برای این منظور دکمه‌ی Reset را به ابتدای المان‌های کامپوننت Counters اضافه می‌کنیم:
<button
  onClick={this.handleReset}
  className="btn btn-primary btn-sm m-2"
>
  Reset
</button>
سپس متد رویدادگردان handleReset آن‌را به صورت زیر با تنظیم مقدار value هر counter به صفر و بازگشت آن و در نهایت به روز رسانی state کامپوننت با این آرایه‌ی جدید، پیاده سازی می‌کنیم:
  handleReset = () => {
    const counters = this.state.counters.map(counter => {
      counter.value = 0;
      return counter;
    });
    this.setState({ counters }); // = this.setState({ counters: counters });
  };


اکنون پس از ذخیره سازی فایل counters.jsx و بارگذاری مجدد برنامه در مرورگر، هرچقدر بر روی دکمه‌ی Reset کلیک کنیم ... اتفاقی رخ نمی‌دهد! حتی اگر به افزونه‌ی React developer tools نیز مراجعه کنیم، مشاهده خواهیم کرد که عمل تنظیم value به صفر، در تک تک کامپوننت‌های شمارشگر، به درستی صورت گرفته‌است؛ اما تغییرات به DOM اصلی منعکس نشده‌اند:


البته اگر به همین تصویر دقت کنید، هنوز مقدار count، در state آن 4 است. علت اینجا است که هر کدام از Counterها دارای local state خاص خودشان هستند و در آن‌ها، مقدار count به صورت زیر مقدار دهی شده‌است که در آن تغییرات بعدی این this.props.value، متصل به count نیست و count، فقط یکبار مقدار دهی می‌شود:
class Counter extends Component {
  state = {
    count: this.props.counter.value
  };
این قطعه‌ی از کد، تنها زمانی اجرا می‌شود که یک وهله از کلاس کامپوننت Counter، در حال ایجاد است. به همین جهت زمانیکه صفحه برای بار اول بارگذاری می‌شود، مقدار آغازین count به درستی دریافت می‌شود. اما با کلیک بر روی دکمه‌ی Reset، هرچند مقدار value هر شیء counter تعریف شده‌ی در کامپوننت والد تغییر می‌کند، اما local state کامپوننت‌های فرزند به روز رسانی نمی‌شوند و مقدار جدید value را دریافت نمی‌کنند. برای رفع یک چنین مشکلی نیاز است یک مرجع مشخص را برای مقدار دهی stateهای کامپوننت‌های فرزند ایجاد کنیم.


حذف Local state

اکنون می‌خواهیم در کامپوننت Counter، قسمت local state آن‌را به طور کامل حذف کرده و تنها از this.props جهت دریافت اطلاعاتی که نیاز دارد، استفاده کنیم. به این نوع کامپوننت‌ها، «‍Controlled component» نیز می‌گویند. یک کامپوننت کنترل شده دارای local state خاص خودش نیست و تمام داده‌های دریافتی را از طریق this.props دریافت می‌کند و هر زمانیکه قرار است داده‌ای تغییر کند، رخ‌دادی را به والد خود صادر می‌کند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل می‌شود.
برای پیاده سازی این مفهوم، ابتدا خاصیت state کامپوننت Counter را حذف می‌کنیم. سپس تمام ارجاعات به this.state را در این کامپوننت یافته و آن‌ها را تغییر می‌دهیم. اولین ارجاع، در متد handleIncrement به صورت this.state.count تعریف شده‌است:
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
 از این جهت که دیگر دارای local state نیستیم، داشتن متد this.setState در اینجا بی‌مفهوم است. در یک کامپوننت کنترل شده، هر زمانیکه قرار است داده‌ای ویرایش شود، این کامپوننت باید رخ‌دادی را صادر کرده و از والد خود درخواست تغییر اطلاعات را ارائه دهد؛ شبیه به this.props.onDelete ای که در قسمت قبل کامل کردیم. بنابراین کل متد handleIncrement را نیز حذف می‌کنیم. اینبار رخ‌داد onClick، سبب بروز رخداد onIncrement در والد خود خواهد شد:
<button
  onClick={() => this.props.onIncrement(this.props.counter)}
  className="btn btn-secondary btn-sm"
>
  Increment
</button>
همچنین دو متد دیگری که ارجاعی را به this.state داشتند، به صورت زیر جهت استفاده‌ی از this.props.counter.value، به روز رسانی می‌شوند:
  getBadgeClasses() {
    let classes = "badge m-2 badge-";
    classes += this.props.counter.value === 0 ? "warning" : "primary";
    return classes;
  }

  formatCount() {
    const { value } = this.props.counter; // Object Destructuring
    return value === 0 ? "Zero" : value;
  }
تا اینجا به صورت کامل local state این کامپوننت حذف و با this.props جایگزین شده و در نتیجه تحت کنترل کامپوننت والد آن قرار می‌گیرد.

در ادامه به کامپوننت Counters مراجعه کرده و متد رویدادگردانی را جهت پاسخگویی به رخ‌داد onIncrement رسیده‌ی از کامپوننت‌های فرزند، تعریف می‌کنیم:
  handleIncrement = counter => {
    console.log("handleIncrement", counter);
  };
سپس ارجاعی از این متد را به ویژگی onIncrement تعریف شده‌ی در المان Counter، متصل می‌کنیم:
  <Counter
    key={counter.id}
    counter={counter}
    onDelete={this.handleDelete}
    onIncrement={this.handleIncrement}
  />
اکنون هر زمانیکه بر روی دکمه‌ی Increment کلیک شود، this.props.onIncrement آن، سبب فراخوانی متد handleIncrement والد خود خواهد شد.

پیاده سازی کامل متد handleIncrement اینبار به صورت زیر است:
  handleIncrement = counter => {
    console.log("handleIncrement", counter);
    const counters = [...this.state.counters]; // cloning an array
    const index = counters.indexOf(counter);
    counters[index] = { ...counter }; // cloning an object
    counters[index].value++;
    console.log("this.state.counters", this.state.counters[index]);
    this.setState({ counters });
  };
همانطور که در قسمت‌های قبل نیز عنوان شد، در React نباید مقدار state را به صورت مستقیم ویرایش کرد؛ مانند مراجعه‌ی مستقیم به this.state.counters[index] و سپس تغییر خاصیت value آن‌. بنابراین باید یک clone از آرایه‌ی counters و سپس یک clone از شیء counter رسیده‌ی از کامپوننت فرزند را ایجاد کنیم تا این cloneها دیگر ارجاعی را به اشیاء اصلی ساخته شده‌ی از روی آن‌ها نداشته باشند (مهم‌ترین خاصیت یک clone) تا اگر خاصیت و مقداری را در آن‌ها تغییر دادیم، دیگر به شیء اصلی که از روی آن‌ها clone شده‌اند، منعکس نشوند. در اینجا از spread operator برای ایجاد این cloneها استفاده شده‌است. اکنون مقادیر خواص این cloneها را تغییر می‌دهیم و درنهایت این counters جدید را که خودش نیز یک clone است، به متد this.setState جهت به روز رسانی UI و همچنین state کامپوننت، ارسال می‌کنیم.

تا اینجا اگر برنامه را ذخیره کرده و منتظر به روز رسانی آن در مرورگر شویم، با کلیک بر روی Reset، تمام کامپوننت‌ها با هر وضعیتی که پیشتر داشته باشند، به حالت اول خود باز می‌گردند:



همگام سازی چندین کامپوننت با هم زمانیکه رابطه‌ی والد و فرزندی بین آن‌ها وجود ندارد


در ادامه می‌خواهیم یک منوی راهبری (یا همان NavBar در بوت استرپ) را به بالای صفحه اضافه کنیم و در آن جمع کل تعداد Counterهای رندر شده را نمایش دهیم؛ مانند نمایش تعداد آیتم‌های انتخاب شده‌ی توسط یک کاربر، در یک سبد خرید. برای پیاده سازی آن، درخت کامپوننت‌های React را مطابق شکل فوق تغییر می‌دهیم. یعنی مجددا کامپوننت App را در به عنوان کامپوننت ریشه‌ای انتخاب کرده که سایر کامپوننت‌ها از آن مشتق می‌شوند و همچنین کامپوننت مجزای NavBar را نیز اضافه خواهیم کرد.
برای این منظور به index.js مراجعه کرده و مجددا کامپوننت App را که غیرفعال کرده بودیم و بجای آن Counters را نمایش می‌دادیم، اضافه می‌کنیم:
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

سپس کامپوننت جدید NavBar را توسط فایل جدید src\components\navbar.jsx اضافه می‌کنیم تا منوی راهبری سایت را نمایش دهد:
import React, { Component } from "react";

class NavBar extends Component {
  render() {
    return (
      <nav className="navbar navbar-light bg-light">
        <a className="navbar-brand" href="#">
          Navbar
        </a>
      </nav>
    );
  }
}

export default NavBar;

اکنون به App.js مراجعه کرده و متد render آن‌را جهت نمایش درخت کامپوننت‌هایی که مشاهده کردید، تکمیل می‌کنیم:
import "./App.css";

import React from "react";

import Counters from "./components/counters";
import NavBar from "./components/navbar";

function App() {
  return (
    <React.Fragment>
      <NavBar />
      <main className="container">
        <Counters />
      </main>
    </React.Fragment>
  );
}

export default App;
ابتدا کامپوننت NavBar در بالای صفحه رندر می‌شود و سپس کامپوننت Counters در میانه‌ی صفحه. چون در اینجا چندین المان قرار است رندر شوند، از React.Fragment برای محصور کردن آن‌ها استفاده کرده‌ایم.
تا اینجا اگر برنامه را ذخیره کنیم تا در مرورگر بارگذاری مجدد شود، چنین شکلی حاصل شده‌است:


اکنون می‌خواهیم تعداد کامپوننت‌های شمارشگر را در navbar نمایش دهیم. پیشتر state کامپوننت Counters را توسط props، به کامپوننت‌های Counter رندر شده‌ی توسط آن انتقال دادیم. استفاده‌ی از این ویژگی به دلیل وجود رابطه‌ی والد و فرزندی بین این کامپوننت‌ها میسر شد. اما همانطور که در تصویر درخت کامپوننت‌های جدید تشکیل شده مشاهده می‌کنید، رابطه‌ی والد و فرزندی بین دو کامپوننت Counters و NavBar وجود ندارد. بنابراین اکنون این سؤال مطرح می‌شود که چگونه باید تعداد کل شمارشگرهای کامپوننت Counters را به کامپوننت NavBar، برای نمایش آن‌ها انتقال داد؟ در یک چنین حالت‌هایی که رابطه‌ی والد و فرزندی بین کامپوننت‌ها وجود ندارد و می‌خواهیم آن‌ها را همگام سازی کنیم و داده‌هایی را بین آن‌ها به اشتراک بگذاریم، باید state را به یک سطح بالاتر انتقال داد. یعنی در این مثال باید state کامپوننت Counters را به والد آن که اکنون کامپوننت App است، منتقل کرد. پس از آن چون هر دو کامپوننت NavBar و Counters، از کامپوننت App مشتق می‌شوند، اکنون می‌توان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت.


انتقال state به یک سطح بالاتر

برای انتقال state به یک سطح بالاتر، به کامپوننت Counters مراجعه کرده و خاصیت state آن‌را به همراه تمامی متدهایی که آن‌را تغییر می‌دهند و از آن استفاده می‌کنند، انتخاب و cut می‌کنیم. سپس به کامپوننت App مراجعه کرده و آن‌ها را در اینجا paste می‌کنیم. یعنی خاصیت state و متدهای handleDelete، handleReset و handleIncrement را از کامپوننت Counters به کامپوننت App منتقل می‌کنیم. این مرحله‌ی اول است. سپس نیاز است به کامپوننت Counters مراجعه کرده و ارجاعات به state و متدهای یاد شده را توسط props اصلاح می‌کنیم. برای این منظور ابتدا باید این props را در کامپوننت App مقدار دهی کنیم تا بتوانیم آن‌ها را در کامپوننت Counters بخوانیم؛ یعنی متد render کامپوننت App، تمام این خواص و متدها را باید به صورت ویژگی‌هایی به تعریف المان Counters اضافه کند تا خاصیت props آن بتواند به آن‌ها دسترسی داشته باشد:
  render() {
    return (
      <React.Fragment>
        <NavBar />
        <main className="container">
          <Counters
            counters={this.state.counters}
            onReset={this.handleReset}
            onIncrement={this.handleIncrement}
            onDelete={this.handleDelete}
          />
        </main>
      </React.Fragment>
    );
  }

پس از این تعاریف می‌توانیم به کامپوننت Counters بازگشته و ارجاعات فوق را توسط خاصیت props، در متد render آن اصلاح کنیم:
  render() {
    return (
      <div>
        <button
          onClick={this.props.onReset}
          className="btn btn-primary btn-sm m-2"
        >
          Reset
        </button>
        {this.props.counters.map(counter => (
          <Counter
            key={counter.id}
            counter={counter}
            onDelete={this.props.onDelete}
            onIncrement={this.props.onIncrement}
          />
        ))}
      </div>
    );
  }
در اینجا سه رویدادگردان و یک خاصیت counters، از طریق خاصیت props والد کامپوننت Counter که اکنون کامپوننت App است، خوانده می‌شوند.

پس از این نقل و انتقالات، اکنون می‌توانیم تعداد counters را در NavBar نمایش دهیم. برای این منظور ابتدا در کامپوننت App، به همان روشی که ویژگی counters={this.state.counters} را به تعریف المان Counters اضافه کردیم، شبیه به همین کار را برای کامپوننت NavBar نیز می‌توانیم انجام دهیم تا از طریق خاصیت props آن قابل دسترسی شود و یا حتی می‌توان به صورت زیر، تنها جمع کل را به آن کامپوننت ارسال کرد:
<NavBar
    totalCounters={this.state.counters.filter(c => c.value > 0).length}
/>

سپس در کامپوننت NavBar، عدد totalCounters فوق را که به تعداد کامپوننت‌هایی که مقدار value آن‌ها بیشتر از صفر است، اشاره می‌کند، از طریق خاصیت props خوانده و نمایش می‌دهیم:
class NavBar extends Component {
  render() {
    return (
      <nav className="navbar navbar-light bg-light">
        <a className="navbar-brand" href="#">
          Navbar{" "}
          <span className="badge badge-pill badge-secondary">
            {this.props.totalCounters}
          </span>
        </a>
      </nav>
    );
  }
}
که با ذخیره کردن این فایل و بارگذاری مجدد برنامه در مرورگر، به خروجی زیر خواهیم رسید:



کامپوننت‌های بدون حالت تابعی

اگر به کدهای کامپوننت NavBar دقت کنیم، تنها یک تک متد render در آن ذکر شده‌است و تمام اطلاعات مورد نیاز آن نیز از طریق props تامین می‌شود و دارای state و یا هیچ رویدادگردانی نیست. یک چنین کامپوننتی را می‌توان به یک «Stateless Functional Component» تبدیل کرد؛ کامپوننت‌های بدون حالت تابعی. در اینجا بجای اینکه از یک کلاس برای تعریف کامپوننت استفاده شود، می‌توان از یک function استفاده کرد (به همین جهت به آن functional می‌گویند). احتمالا نمونه‌ی آن‌را با کامپوننت App پیش‌فرض قالب create-react-app نیز مشاهده کرده‌اید که در آن فقط یک ()function App وجود دارد. البته در کدهای فوق چون نیاز به ذکر state، در کامپوننت App وجود داشت، آن‌را از حالت تابعی، به حالت کلاس استاندارد کامپوننت، تبدیل کردیم.
اگر بخواهیم کامپوننت بدون حالت NavBar را نیز تابعی کنیم، می‌توان به صورت زیر عمل کرد:
import React from "react";

// Stateless Functional Component
const NavBar = props => {
  return (
    <nav className="navbar navbar-light bg-light">
      <a className="navbar-brand" href="#">
        Navbar{" "}
        <span className="badge badge-pill badge-secondary">
          {props.totalCounters}
        </span>
      </a>
    </nav>
  );
};

export default NavBar;
برای اینکار قسمت return متد render کامپوننت را cut کرده و به داخل تابع NavBar منتقل می‌کنیم. بدنه‌ی این تابع را هم می‌توان توسط میان‌بر sfc که مخفف Stateless Functional Component است، در VSCode تولید کرد.
پیشتر در کامپوننت NavBar از شیء this استفاده شده بود. این روش تنها با کلاس‌های استاندارد کامپوننت کار می‌کند. در اینجا باید props را به عنوان پارامتر متد دریافت (همانند مثال فوق) و سپس از آن استفاده کرد.

البته لازم به ذکر است که انتخاب بین «کامپوننت‌های بدون حالت تابعی» و یک کامپوننت معمولی تعریف شده‌ی توسط کلاس‌ها، صرفا یک انتخاب شخصی است.

یک نکته: امکان Destructuring Arguments نیز در اینجا وجود دارد. یعنی بجای اینکه یکبار props را به عنوان پارامتر دریافت کرد و سپس توسط آن به خاصیت totalCounters دسترسی یافت، می‌توان نوشت:
const NavBar = ({ totalCounters }) => {
در این حالت شیء props دریافت شده توسط ویژگی Objects Destructuring، به totalCounters تجزیه می‌شود و سپس می‌توان تنها از همین متغیر دریافتی، به صورت {totalCounters} در کدها استفاده کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-08.zip
مطالب
ساخت کلیدهای امنیتی GunPG
یکی از روش‌های ارسال و رمزگذاری اطلاعات، استفاده از کلیدهای امنیتی مورد استفاده‌ی در سیستم یونیکس یا GnuPG است. استفاده از نرم افزار Gnu Privacy Guard یا گارد حفاظتی گنو، به ما این اجازه را می‌دهد که بتوانیم اطلاعاتمان را در بسترهای ارتباطی، با خیالی راحت‌تر ارسال کنیم و تا حد زیادی مطمئن باشیم که تنها فرد هدف توانایی دسترسی به اطلاعات را خواهد داشت. گارد امنیتی گنو زیر مجموعه‌ای از پروژه‌ی گنو است که دولت آلمان پایه ریز اصلی آن بوده است. این نرم افزار از یک روش رمزگذاری ترکیبی استفاده می‌کند که الگوریتم‌های کلیدهای برابر(متقارن) و کلید‌های عمومی (نامتقارن) جهت تبادل آسان کلید را شامل می‌شود. در حال حاضر که نسخه‌ی دو این برنامه ارائه شده است، برای رمزگذاری‌ها از کتابخانه‌ای به اسم libgcrypt استفاده می‌کند. یکی از مشکلات فعلی این پروژه، عدم وجود api‌های مناسبی جهت دسترسی راحت‌تر است و برای حل این مشکل، GPGME که مخفف GnuPG Made Easy ایجاد شد. بسیاری از برنامه‌ها و پلاگین‌های ارسال اطلاعات، امروزه همچون ارسال ایمیل، از این کلیدها بهره می‌برند.

پروژه‌های مرتبط با این قضیه اسم‌های مشابهی دارند که گاها بعضی افراد، هر کدام از اسم‌ها را که دوست دارند، به همه اطلاق می‌کنند؛ ولی تفاوت‌هایی در این بین وجود دارد:

  • OpenPGP: یک برنامه نیست و یک قانون و استانداری برای تهیه‌ی آن است؛ که رعایت اصول آن الزامی است و برنامه‌ی بالا، یک پیاده سازی از این استاندارد است.
  • PGP: یک برنامه، برای رمزگذاری اطلاعات است که مخفف Pretty Good Privacy است.
  • و GnuPG یا GPG که در بالا به آن اشاره شد.
برای ساخت کلید، ما از دستور یا برنامه‌ی GPG که که عمدتا در همه‌ی لینوکس‌ها مثل دبیان و مشتقات آن نصب است، استفاده می‌کنیم و اگر نصب نیست از طریق توزیع آن اقدام نمایید.
در صورتیکه از ویندوز استفاده می‌کنید، نیاز است ابتدا خط فرمان یونیکس را روی آن نصب کنید. برنامه‌ی Cygwin این امکان را به شما می‌دهد تا خط فرمان یونیکس و دستورات پیش فرض آن را داشته باشید. این برنامه در دو حالت ۳۲ بیتی و ۶۴ بیتی ایجاد شده است. از آنجا که گفتیم این برنامه شامل دستورات پیش فرض آن است، برای همین GPG باید به صورت یک بسته‌ی جداگانه نصب شود که در سایت آن می‌توانید بسته‌های مختلف آن‌را برای پلتفرم‌های مختلف را مشاهده کنید.

ساخت کلید


برای ساخت کلید دستور زیر را صادر کنید:
gpg --gen-key
اگر از نسخه‌های جدیدتر GPG استفاده می‌کنید، گزینه‌هایی به شکل زیر ایجاد می‌شوند؛ ولی اگر خیر، ممکن است تعداد و شماره‌ی گزینه‌ها متفاوت باشند که در این مورد دقت کنید. من در اینجا همان حالت پیش فرض، یعنی ۱ را انتخاب می‌کنم. این گزینه نحوه‌ی امضاء و یا رمزگذاری شما با استفاده از الگوریتم‌های RSA و DSA را مشخص می‌کند.
Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
در کل در هر حالتی، استفاده‌ی از RSA پیشنهاد می‌شود. بعد از آن، از شما اندازه‌ی کلید را می‌پرسد که همان مقدار پیش فرض خودش را وارد می‌کنیم:
What keysize do you want? (2048)
البته بسیاری ۱۰۲۴ بایت را نیز کافی می‌دانند.
بعد از آن مدت زمان اعتبار این کلید را از شما جویا می‌شود:
Key is valid for? (0)
هنگام این پرسش نحوه‌ی ورود زمان را به شما خواهد گفت که می‌تواند به شکل‌های زیر باشد:
دو هفته
2w
دو سال
2y
پس از آن هم یک تاییدیه از شما می‌گیرد و تاریخ انقضاء را به طور کامل برای شما می‌نویسد و سپس نیاز است که اطلاعاتی از قبیل نام و ایمیل و توضیح را وارد کنید:
You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
    "Heinrich Heine (Der Dichter) <heinrichh@duesseldorf.de>"

Real name: ali yeganeh.m
Email address: yeganehaym@gmail.com
Comment: androidbreadcrumb
You selected this USER-ID:
    "ali yeganeh.m (androidbreadcrumb) <yeganehaym@gmail.com>"
بعد از آن از شما می‌خواهد که کل عملیات را تایید و یا کنسل کنید؛ یا اگر اطلاعات بالا را اشتباه وارد کرده‌اید، اصلاح کنید. با زدن کلید O عملیات را تایید کنید. در این حین از شما یک کلید برای رمزگذاری می‌پرسد که باید آن را دو بار بدهید و کارتان در اینجا به پایان می‌رسد و کلید ایجاد می‌شود.
اگر مشکلی در ساخت کلید نباشد با ارسال دستور زیر باید آن را در لیست کلیدها ببینید:
ali@alipc:~$ gpg --list-keys
/home/ali/.gnupg/pubring.gpg
----------------------------
pub   2048R/8708016A 2015-10-23 [expires: 2065-10-10]
uid                  ali yeganeh.m (androidbreadcrumb) <yeganehaym@gmail.com>
sub   2048R/533B7E96 2015-10-23 [expires: 2065-10-10]
در اینجا کلید عمومی در خط pub بعد از  / قرار دارد؛ یعنی عبارت ۸۷۰۸۰۱۶A کلید عمومی ماست که بر روی هر سیستم و هر کلیدی متفاوت است.

تبدیل کد متنی به کد دودویی
یکی از روش‌های ارسال کدهای دودویی تبدیل آنان به یک قالب متنی ASCII است که به آن قالب ASCII Armor هم می‌گویند. سایت‌های زیادی وجود دارند که این عبارت متنی را از شما می‌خواهند. چرا که مثلا این امکان وجود دارد که کلیدی که کاربر به سمت آنان می‌فرستد، آسیب دیده باشد یا اینکه KeyServer‌ها در دسترس نباشند. در مورد این سرورها در ادامه صحبت خواهیم کرد. مثلا یکی از سایت‌هایی که به این عبارت‌ها نیاز دارد ‌‌‌‌Bintray است.

برای دریافت این کلید متنی باید دستور زیر را صادر کنید:
gpg --output mykey.asc --export -a $GPGKEY
که برای مثال ما می‌شود:
gpg --output mykey.asc --export -a 8708016A
و اگر کلید را با یک ویرایشگر متنی باز کنید، محتوایی شبیه محتوای زیر را خواهید دید:
ali@alipc:~$ cat mykey.asc
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1

mQENBFYqAJABCADcw5xPonh5Vj7nDk1CxDskq/VsO08XOa/i2OLOzatB4oK5x+0x
jxORxXMnIAR83PCK5/WkOBa64jnu3eiP3jKEwAykGGz/Z1bezC9TIP8y+PnsiDhT
aFArluUJx+RT5q7s27aKjqoc3fR/xuwLWopZt9uYzE/DQAPDsHdUoUg+fh4Hevm+
a8/3ncR7q6nM8gc9wk621Urb1HaRrILdmeh7ZpJcl8ZUbc+NObw357fGsjnpfHXO
rdCr7ClvNUq6I+IeGMQG/6040LeeaqhaRxPrUhbFjLA155gkSqzecxl7wQaYc71M
Zdlv+6Pt1B8nPAA3WXq0ypjU8A5bvmAQRD5LABEBAAG0OGFsaSB5ZWdhbmVoLm0g
KGFuZHJvaWRicmVhZGNydW1iKSA8eWVnYW5laGF5bUBnbWFpbC5jb20+iQE+BBMB
AgAoBQJWKgCQAhsDBQld/A8ABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDS
Lhq8hwgBanaHB/4reGxUjR6dB08ykfwQOx+raYHGqJlgawisE4qUHTkGaspyQaNy
yxh0vwKkGvg6nNy2VN1XFBc7jlHlrYqPPuPdg2B+1LvEghb30ESDbHUvk8NrJgDJ
C0257gxqWvUQTWvMC3FkSLdw3tyQ8dF7FxmSU79XcxVqGeseaDzMQrEasP0yJHsm
NJf8pvuD6qiWu3KSSoQmI/17Sj8s7eGJMh6o5YRFGHc1Bt9tCD+52bvt579Ju4vZ
tmQvxR4fNQo9sAeMqAJhIpF7IYcuyCEy+CQ847UkzE4f/OCCPxfV3samV/nnBJJ9
Ouu+68lk6Fpx4A0a3nEwqoAmMWxrbSSUFW97uQENBFYqAJABCAC4CzrUOKskE4hK
GVCjaOJKxhbuUdOrep6n3vof0fscs5Dy7h2oVh2vb12WH9X6pijJVPiUpGR4Mpu0
lO2Bu9Rwt38AQ6mRmL/hfzjEXSvKkdX7osk+1CVnnUaSdM9Ek2hWUH8JcN28z/WT
X9Bw8MCdZF7j1HvX/5ojghzMZyYM4elWJLBr1gON6xXAI6HR7DlnRkaVr8L9SYGm
FyAXZ0LzWYwG1Z1AnTyxff6v/Mn3p1/1E3aBA+LkQqBzHg2nBm4jCaFWfeCdiNBf
CHkY9r/Evo9hUPD+CtBNFwsUm1D4maZ0FFtIQ701QhVmupnub+rKoObC0AFj3abK
MCw9uo8TABEBAAGJASUEGAECAA8FAlYqAJACGwwFCV38DwAACgkQ0i4avIcIAWrz
rAf+K1IIMtBq3WlabfZQrgzFHQ62ugVJO/yI1ITkm4l08XHDf+ShqDg4urNuMDEe
oQD35MvB2BhER1jL6VR3qjLkZyZYJ+EQiSxEDWXooav3KvpWjhcqjQy79GFs8waH
E7ssGmWwaugVS/PJAmGQ+s8YWDNa6aCClmp2dJRiwBTyFdewNBLA2V32xzWCYxhI
YtEp+Kg15XuCDTRatOPWSFGSPe/paytmpGZc0XzU/W9sBpabhxVmcL4H6L07uCef
IOn/S5QXo3P9X/3ckmJ9GUb7rjdq1ivYgX53xI75jlePsmN/2f+3fNffUaZgFTTd
Uls+XCun7OVYSBBfjgRfQbTvoA==
=6j7i
-----END PGP PUBLIC KEY BLOCK-----
در صورتی که قصد دارید متن کلید خصوصی را به دست بیاورید، لازم است بعد از export- عبارت secret-key- را نیز اضافه کنی د؛ یعنی:
gpg --output mykey.asc --export-secret-key -a 8708016A


آپلود کلید به سرورهای کلید (Key Servers
)

 یکی از روش‌های به اشتراک گذاری کلید برای کاربران این است که از سرورهای کلید استفاده کنیم. یکبار آپلود روی یکی از این سرورها باعث می‌شود که به بقیه‌ی سرورها هم اضافه شود. یکی از این سرورهای کلید که خودم از آن استفاده می‌کنم، سرور ابونتو است و با استفاده از دستور زیر، همان کلید بالا را برای آن سرور ارسال می‌کنم:
gpg --send-keys --keyserver keyserver.ubuntu.com $GPGKEY

==>
gpg --send-keys --keyserver keyserver.ubuntu.com 8708016A
سپس از طریق کلید متنی، کلید آپلود شده را تایید می‌کنیم. به این آدرس رفته و محتوای کلید متنی خود را به طور کامل به همراه تگ‌های شروع و پایان کپی کنید و حتی می‌توانید کلید خود را از طریق کادر جست و جو پیدا کنید.

رمزگذاری
ابتدا در محیط یونیکس، یک فایل متنی ساده با متن hello ubuntu را ایجاد میکنم. در ادامه قصد دارم این فایل را رمزنگاری کنم:
ali@alipc:~$ cat >ali.txt
hello ubuntu
سپس همین فایل را رمزنگاری می‌کنم:
ali@alipc:~$ gpg --output myali.gpg --encrypt --recipient yeganehaym@gmail.com ali.txt
در این دستور ابتدا گفتیم که نام فایل خروجی ما myali.gpg است و می‌خواهیم آن را رمزگذاری کنیم که توسط کلیدی با ایمیل yeganehaym@gmail.com می‌باشد فایل ali.txt را رمزگذاری می‌کنیم.

رمزگشایی
برای رمزگشایی می‌توانید از طریق دستور زیر اقدام کنید:
gpg --output output.txt --decrypt myali.gpg

You need a passphrase to unlock the secret key for
user: "ali yeganeh.m (androidbreadcrumb) <yeganehaym@gmail.com>"
2048-bit RSA key, ID 533B7E96, created 2015-10-23 (main key ID 8708016A)
در اینجا دستور دادیم محتوای فایل رمزشده‌ی myali.gpg را رمزگشایی کن و محتوای آن را داخل فایلی با نام output.txt قرار بده. بعد از اجرای این دستور از شما عبارت رمزی را که در مرحله‌ی ساخت کلید دوبار از شما پرسید، درخواست می‌کند. در بعضی سیستم‌ها در همان ترمینال می‌پرسد، ولی بعضی سیستم‌ها مثل ابونتو که من از آن استفاده می‌کنم، به صورت گرافیکی یک کادر باز کرده و از شما خواهش می‌کند عبارت رمز را وارد کنید.
عبارت رمز را وارد کنید و حالا فایل output.txt را باز کنید:
ali@alipc:~$ cat output.txt 
hello ubuntu
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت سیزدهم- فعالسازی اعتبارسنجی دو مرحله‌ای
در انتهای «قسمت دوازدهم- یکپارچه سازی با اکانت گوگل»، کار «اتصال کاربر وارد شده‌ی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است» انجام شد. اما این مورد یک مشکل امنیتی را هم ممکن است ایجاد کند. اگر IDP ثالث، ایمیل اشخاص را تعیین اعتبار نکند، هر شخصی می‌تواند ایمیل دیگری را بجای ایمیل اصلی خودش در آنجا ثبت کند. به این ترتیب یک مهاجم می‌تواند به سادگی تنها با تنظیم ایمیل کاربری مشخص و مورد استفاده‌ی در برنامه‌ی ما در آن IDP ثالث، با سطح دسترسی او فقط با دو کلیک ساده به سایت وارد شود. کلیک اول، کلیک بر روی دکمه‌ی external login در برنامه‌ی ما است و کلیک دوم، کلیک بر روی دکمه‌ی انتخاب اکانت، در آن اکانت لینک شده‌ی خارجی است.
برای بهبود این وضعیت می‌توان مرحله‌ی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شده‌ی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال می‌کنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامه‌ی ما نیز می‌باشد. این اعتبارسنجی دو مرحله‌ای را می‌توان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمه‌ی عبور در IDP ما نیز اضافه کرد.


تنظیم میان‌افزار Cookie Authentication

مرحله‌ی اول ایجاد گردش کاری اعتبارسنجی دو مرحله‌ای، فعالسازی میان‌افزار Cookie Authentication در برنامه‌ی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public const string TwoFactorAuthenticationScheme = "idsrv.2FA";

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddAuthentication()
                .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme)
                .AddGoogle(authenticationScheme: "Google", configureOptions: options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    options.ClientId = Configuration["Authentication:Google:ClientId"];
                    options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
                });
        }


اصلاح اکشن متد Login برای هدایت کاربر به صفحه‌ی ورود اطلاعات کد موقتی

تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیش‌فرض، کدهای HttpContext.SignInAsync آن‌را حذف کرده و با Redirect به اکشن متد نمایش صفحه‌ی ورود کد موقتی ارسال شده‌ی به آدرس ایمیل کاربر، جایگزین می‌کنیم.
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class AccountController : Controller
    {
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    // ... 

            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
                    
                    var id = new ClaimsIdentity();
                    id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId));
                    await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));

                    await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId);

                    var redirectToAdditionalFactorUrl =
                        Url.Action("AdditionalAuthenticationFactor",
                            new
                            {
                                returnUrl = model.ReturnUrl,
                                rememberLogin = model.RememberLogin
                            });                   

                    // request for a local page
                    if (Url.IsLocalUrl(model.ReturnUrl))
                    {
                        return Redirect(redirectToAdditionalFactorUrl);
                    }

                    if (string.IsNullOrEmpty(model.ReturnUrl))
                    {
                        return Redirect("~/");
                    }

                    // user might have clicked on a malicious link - should be logged
                    throw new Exception("invalid return URL");
                }

                await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
        }
- در این اکشن متد، ابتدا مشخصات کاربر، از بانک اطلاعاتی بر اساس نام کاربری او، دریافت می‌شود.
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل می‌شود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شده‌ی در کلاس آغازین برنامه تطابق دارد، ایجاد می‌کنیم.
 await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
سپس کد موقتی به آدرس ایمیل کاربر ارسال می‌شود. برای این منظور سرویس جدید زیر را به برنامه اضافه کرده‌ایم:
    public interface ITwoFactorAuthenticationService
    {
        Task SendTemporaryCodeAsync(string subjectId);
        Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code);
    }
- کار متد SendTemporaryCodeAsync، ایجاد و ذخیره‌ی یک کد موقتی در بانک اطلاعاتی و سپس ارسال آن به کاربر است. البته در اینجا، این کد در صفحه‌ی Console برنامه لاگ می‌شود (یا هر نوع Log provider دیگری که برای برنامه تعریف کرده‌اید) که می‌توان بعدها آن‌را با کدهای ارسال ایمیل جایگزین کرد.
- متد IsValidTemporaryCodeAsync، کد دریافت شده‌ی از کاربر را با نمونه‌ی موجود در بانک اطلاعاتی مقایسه و اعتبار آن‌را اعلام می‌کند.


ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن

پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت می‌کنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهده‌ی لاگ برنامه)، دریافت کرده‌است، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال می‌کنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامه‌ی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations;

namespace DNT.IDP.Controllers.Account
{
    public class AdditionalAuthenticationFactorViewModel
    {
        [Required]
        public string Code { get; set; }

        public string ReturnUrl { get; set; }

        public bool RememberLogin { get; set; }
    }
}
که در آن، Code توسط کاربر تکمیل می‌شود و دو گزینه‌ی دیگر را از طریق مسیریابی و هدایت به این View دریافت خواهد کرد.
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش می‌دهد و در حالت Post، اطلاعات آن‌را از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account
{
    public class AccountController : Controller
    {
        [HttpGet]
        public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin)
        {
            // create VM
            var vm = new AdditionalAuthenticationFactorViewModel
            {
                RememberLogin = rememberLogin,
                ReturnUrl = returnUrl
            };

            return View(vm);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> AdditionalAuthenticationFactor(
            AdditionalAuthenticationFactorViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // read identity from the temporary cookie
            var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme);
            var tempUser = info?.Principal;
            if (tempUser == null)
            {
                throw new Exception("2FA error");
            }

            // ... check code for user
            if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code))
            {
                ModelState.AddModelError("code", "2FA code is invalid.");
                return View(model);
            }

            // login the user
            AuthenticationProperties props = null;
            if (AccountOptions.AllowRememberLogin && model.RememberLogin)
            {
                props = new AuthenticationProperties
                {
                    IsPersistent = true,
                    ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                };
            }

            // issue authentication cookie for user
            var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId());
            await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));
            await HttpContext.SignInAsync(user.SubjectId, user.Username, props);

            // delete temporary cookie used for 2FA
            await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme);

            if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl))
            {
                return Redirect(model.ReturnUrl);
            }

            return Redirect("~/");
        }
توضیحات:
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد می‌کند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، می‌توانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شده‌است، آن‌را به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی می‌کنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت می‌کنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.


View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شده‌است تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel

<div>
    <div class="page-header">
        <h1>2-Factor Authentication</h1>
    </div>

    @Html.Partial("_ValidationSummary")

    <div class="row">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">Input your 2FA code</h3>
            </div>
            <div class="panel-body">
                <form asp-route="Login">
                    <input type="hidden" asp-for="ReturnUrl" />
                    <input type="hidden" asp-for="RememberLogin" />

                    <fieldset>
                        <div class="form-group">
                            <label asp-for="Code"></label>
                            <input class="form-control" placeholder="Code" asp-for="Code" autofocus>
                        </div>

                        <div class="form-group">
                            <button class="btn btn-primary">Submit code</button>
                        </div>
                    </fieldset>
                </form>

            </div>
        </div>
    </div>
</div>

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

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


همچنین کد موقتی این مرحله را نیز در لاگ‌های برنامه مشاهده می‌کنید:


پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامه‌ی MVC Client هدایت می‌شویم.


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

دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه می‌کنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شده‌است، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل می‌کنیم.




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
مونیتور کردن میزان مصرف CPU در اس کیوال سرور

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

DECLARE @CPU_BUSY int, @IDLE int
SELECT @CPU_BUSY = @@CPU_BUSY, @IDLE = @@IDLE WAITFOR DELAY '000:00:01'
SELECT (@@CPU_BUSY - @CPU_BUSY)/((@@IDLE - @IDLE + @@CPU_BUSY - @CPU_BUSY) *1.00) *100 AS 'CPU Utilization by sqlsrvr.exe'

ماخذ

در ادامه قصد داریم، هر 5 دقیقه به صورت خودکار بررسی کنیم که آیا میزان مصرف CPU در اس کیوال سرور بالای 50 درصد است؟ و اگر بله، ایمیلی را به مسؤول مربوطه جهت بررسی ارسال کنیم.

بنابراین اولین کاری که باید صورت گیرد، فعال سازی Database Mail در اس کیوال سرور است که به صورت پیش فرض غیرفعال است. برای این منظور تنها کافی است اسکریپت زیر را بر روی سرور اجرا کنید:

USE [master]
GO
sp_configure 'show advanced options',1
GO
RECONFIGURE WITH OVERRIDE
GO
sp_configure 'Database Mail XPs',1
GO
--RECONFIGURE
GO
-- Create a New Mail Profile for Notifications
EXECUTE msdb.dbo.sysmail_add_profile_sp
@profile_name = 'DBA_Notifications',
@description = 'Profile for sending Automated DBA Notifications'
GO
-- Set the New Profile as the Default
EXECUTE msdb.dbo.sysmail_add_principalprofile_sp
@profile_name = 'DBA_Notifications',
@principal_name = 'public',
@is_default = 1 ;
GO
-- Create an Account for the Notifications
EXECUTE msdb.dbo.sysmail_add_account_sp
@account_name = 'SQLMonitor',
@description = 'Account for Automated DBA Notifications',
@email_address = 'nasiri@site.net', -- Change This
@display_name = 'SQL Monitor',
@mailserver_name = 'mail.site.net' -- Change This
GO
-- Add the Account to the Profile
EXECUTE msdb.dbo.sysmail_add_profileaccount_sp
@profile_name = 'DBA_Notifications',
@account_name = 'SQLMonitor',
@sequence_number = 1

GO

ماخذ

این اسکریپت برای اس کیوال سرورهای 2005 به بعد طراحی شده و تنها دو سطر آن‌را پیش از اجرا باید ویرایش کنید. سطر مربوط به email_address و mailserver_name . آدرس ایمیل درحقیقت آدرس ایمیل قسمت from پیغام ارسالی را تشکیل می‌دهد. نام سرور میل هم، منظور آدرس smtp server شما در شبکه است.

یا اگر علاقمند بودید که این‌کار را توسط ویزاردهای management studio انجام دهید (که در نهایت هیچ تفاوتی با اسکریپت فوق نخواهد داشت)، می‌توان به این مقاله رجوع کرد.

پس از اجرای اسکریپت فوق، برای بررسی صحت عملکرد فوق می‌توان دستور زیر را اجرا کرد:

--test
EXECUTE msdb.dbo.sp_send_dbmail
@recipients = 'nasiri@site.net', -- Change This
@Subject = 'Test Message generated from SQL Server DatabaseMail',
@Body = 'This is a test message from SQL Server DatabaseMail'

سایر پارامترهای این دستور را در MSDN می‌توان ملاحظه نمود.

تا اینجا اس کیوال سرور برای ارسال ایمیل آماده شد. در ادامه قصد داریم یک job جدید در اس کیوال سرور ایجاد کنیم تا تمام موارد فوق را لحاظ کند.


مطابق تصویر فوق ابتدا یک job جدید را آغاز خواهیم کرد.



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

DECLARE @CPUUsage INT
DECLARE @CPU_BUSY INT,
@IDLE INT

SELECT @CPU_BUSY = @@CPU_BUSY,
@IDLE = @@IDLE

WAITFOR DELAY '000:00:01'
SELECT @CPUUsage = (@@CPU_BUSY - @CPU_BUSY) / ((@@IDLE - @IDLE + @@CPU_BUSY - @CPU_BUSY) * 1.00)
* 100 -- CPU Utilization by sqlsrvr.exe


IF @CPUUsage > 50
BEGIN
DECLARE @msg NVARCHAR(1000)
SET @msg = 'Please check SQL server, CPU usage is ' + CAST(@CPUUsage AS NVARCHAR(50))
+ '%.'

EXECUTE msdb.dbo.sp_send_dbmail
@recipients = 'nasiri@site.net', -- Change This
@copy_recipients = 'nasiri@site.net', -- Change This
@Subject = 'CPU overload',
@Body = @msg
,@importance = 'High'
END



و در آخر زمان اجرای آن را به هر روز، هر 5 دقیقه یکبار تنظیم خواهیم کرد.

اگر نیاز به راه حلی پخته‌تر و بررسی متوسط چندین مقدار قبلی ، مقایسه آن‌ها و سپس ارسال ایمیل داشتید، می‌توان به فصل 14 کتاب Super SQL Server Systems مراجعه کرد.

مطالب
پیاده سازی Password Policy سفارشی توسط ASP.NET Identity
برای فراهم کردن یک تجربه کاربری ایمن‌تر و بهتر، ممکن است بخواهید پیچیدگی password policy را سفارشی سازی کنید. مثلا ممکن است بخواهید حداقل تعداد کاراکتر‌ها را تنظیم کنید، استفاده از چند حروف ویژه را اجباری کنید،  جلوگیری از استفاده نام کاربر در کلمه عبور و غیره. برای اطلاعات بیشتر درباره سیاست‌های کلمه عبور به این لینک مراجعه کنید. بصورت پیش فرض ASP.NET Identity کاربران را وادار می‌کند تا کلمه‌های عبوری بطول حداقل 6 کاراکتر وارد نمایند. در ادامه نحوه افزودن چند خط مشی دیگر را هم بررسی می‌کنیم.

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

  • تنظیمات پیش فرض باید تغییر کنند تا کلمات عبور حداقل 10 کاراکتر باشند
  • کلمه عبور حداقل یک عدد و یک کاراکتر ویژه باید داشته باشد
  • امکان استفاده از 5 کلمه عبور اخیری که ثبت شده وجود ندارد
در آخر اپلیکیشن را اجرا می‌کنیم و عملکرد این قوانین جدید را بررسی خواهیم کرد.


ایجاد اپلیکیشن جدید

در Visual Studio 2013 اپلیکیشن جدیدی از نوع ASP.NET MVC 4.5 بسازید.

در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. به قسمت Update بروید و تمام انتشارات جدید را در صورت وجود نصب کنید.

بگذارید تا به روند کلی ایجاد کاربران جدید در اپلیکیشن نگاهی بیاندازیم. این به ما در شناسایی نیازهای جدیدمان کمک می‌کند. در پوشه Controllers فایلی بنام AccountController.cs وجود دارد که حاوی متدهایی برای مدیریت کاربران است.

  • کنترلر Account از کلاس UserManager استفاده می‌کند که در فریم ورک Identity تعریف شده است. این کلاس به نوبه خود از کلاس دیگری بنام UserStore استفاده می‌کند که برای دسترسی و مدیریت داده‌های کاربران استفاده می‌شود. در مثال ما این کلاس از Entity Framework استفاده می‌کند که پیاده سازی پیش فرض است.
  • متد Register POST یک کاربر جدید می‌سازد. متد CreateAsync به طبع متد 'ValidateAsync' را روی خاصیت PasswordValidator فراخوانی می‌کند تا کلمه عبور دریافتی اعتبارسنجی شود.
var user = new ApplicationUser() { UserName = model.UserName };  
var result = await UserManager.CreateAsync(user, model.Password);
  
if (result.Succeeded)
{
    await SignInAsync(user, isPersistent: false);  
    return RedirectToAction("Index", "Home");
}

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


قانون 1: کلمه‌های عبور باید حداقل 10 کاراکتر باشند

بصورت پیش فرض خاصیت PasswordValidator در کلاس UserManager به کلاس MinimumLengthValidator تنظیم شده است، که اطمینان حاصل می‌کند کلمه عبور حداقل 6 کاراکتر باشد. هنگام وهله سازی UserManager می‌توانید این مقدار را تغییر دهید.
  • مقدار حداقل کاراکترهای کلمه عبور به دو شکل می‌تواند تعریف شود. راه اول، تغییر کنترلر Account است. در متد سازنده این کنترلر کلاس UserManager وهله سازی می‌شود، همینجا می‌توانید این تغییر را اعمال کنید. راه دوم، ساختن کلاس جدیدی است که از UserManager ارث بری می‌کند. سپس می‌توان این کلاس را در سطح global تعریف کرد. در پوشه IdentityExtensions کلاس جدیدی با نام ApplicationUserManager بسازید.
public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(): base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
    {
        PasswordValidator = new MinimumLengthValidator (10);
    }
}

  کلاس UserManager یک نمونه از کلاس IUserStore را دریافت می‌کند که پیاده سازی API‌های مدیریت کاربران است. از آنجا که کلاس UserStore مبتنی بر Entity Framework است، باید آبجکت DbContext را هم پاس دهیم. این کد در واقع همان کدی است که در متد سازنده کنترلر Account وجود دارد.

یک مزیت دیگر این روش این است که می‌توانیم متدهای UserManager را بازنویسی (overwrite) کنیم. برای پیاده سازی نیازمندهای بعدی دقیقا همین کار را خواهیم کرد.


  • حال باید کلاس ApplicationUserManager را در کنترلر Account استفاده کنیم. متد سازنده و خاصیت UserManager را مانند زیر تغییر دهید.
 public AccountController() : this(new ApplicationUserManager())
         {
         }
  
         public AccountController(ApplicationUserManager userManager)
         {
             UserManager = userManager;
         }
         public ApplicationUserManager UserManager { get; private set; }
حالا داریم از کلاس سفارشی جدیدمان استفاده می‌کنیم. این به ما اجازه می‌دهد مراحل بعدی سفارشی سازی را انجام دهیم، بدون آنکه کدهای موجود در کنترلر از کار بیافتند.
  • اپلیکیشن را اجرا کنید و سعی کنید کاربر محلی جدیدی ثبت نمایید. اگر کلمه عبور وارد شده کمتر از 10 کاراکتر باشد پیغام خطای زیر را دریافت می‌کنید.


قانون 2: کلمه‌های عبور باید حداقل یک عدد و یک کاراکتر ویژه داشته باشند

چیزی که در این مرحله نیاز داریم کلاس جدیدی است که اینترفیس IIdentityValidator را پیاده سازی می‌کند. چیزی که ما می‌خواهیم اعتبارسنجی کنیم، وجود اعداد و کاراکترهای ویژه در کلمه عبور است، همچنین طول مجاز هم بررسی می‌شود. نهایتا این قوانین اعتبارسنجی در متد 'ValidateAsync' بکار گرفته خواهند شد.
  • در پوشه IdentityExtensions کلاس جدیدی بنام CustomPasswordValidator بسازید و اینترفیس مذکور را پیاده سازی کنید. از آنجا که نوع کلمه عبور رشته (string) است از <IIdentityValidator<string استفاده می‌کنیم.
public class CustomPasswordValidator : IIdentityValidator<string> 
{  
    public int RequiredLength { get; set; }

    public CustomPasswordValidator(int length)
    {
        RequiredLength = length;
    }
  
    public Task<IdentityResult> ValidateAsync(string item)
    {
        if (String.IsNullOrEmpty(item) || item.Length < RequiredLength)
        {
            return Task.FromResult(IdentityResult.Failed(String.Format("Password should be of length {0}",RequiredLength)));
        }
  
    string pattern = @"^(?=.*[0-9])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*0-9]{10,}$";
   
    if (!Regex.IsMatch(item, pattern))  
    {
        return Task.FromResult(IdentityResult.Failed("Password should have one numeral and one special character"));
    }
  
    return Task.FromResult(IdentityResult.Success);
 }


  در متد ValidateAsync بررس می‌کنیم که طول کلمه عبور معتبر و مجاز است یا خیر. سپس با استفاده از یک RegEx وجود کاراکترهای ویژه و اعداد را بررسی می‌کنیم. دقت کنید که regex استفاده شده تست نشده و تنها بعنوان یک مثال باید در نظر گرفته شود.
  • قدم بعدی تعریف این اعتبارسنج سفارشی در کلاس UserManager است. باید مقدار خاصیت PasswordValidator را به این کلاس تنظیم کنیم. به کلاس ApplicationUserManager که پیشتر ساختید بروید و مقدار خاصیت PasswordValidator را به CustomPasswordValidator تغییر دهید.
public class ApplicationUserManager : UserManager<ApplicationUser>  
{  
    public ApplicationUserManager() : base(new UserStore<ApplicationUser(new ApplicationDbContext()))
    {
        PasswordValidator = new CustomPasswordValidator(10);
    }
 }

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


قانون 3: امکان استفاده از 5 کلمه عبور اخیر ثبت شده وجود ندارد

هنگامی که کاربران سیستم، کلمه عبور خود را بازنشانی (reset) می‌کنند یا تغییر می‌دهند، می‌توانیم بررسی کنیم که آیا مجددا از یک کلمه عبور پیشین استفاده کرده اند یا خیر. این بررسی بصورت پیش فرض انجام نمی‌شود، چرا که سیستم Identity تاریخچه کلمه‌های عبور کاربران را ذخیره نمی‌کند. می‌توانیم در اپلیکیشن خود جدول جدیدی بسازیم و تاریخچه کلمات عبور کاربران را در آن ذخیره کنیم. هربار که کاربر سعی در بازنشانی یا تغییر کلمه عبور خود دارد، مقدار Hash شده را در جدول تاریخچه بررسی میکنیم.
فایل IdentityModels.cs را باز کنید. مانند لیست زیر، کلاس جدیدی بنام 'PreviousPassword' بسازید.
public class PreviousPassword  
{
  
 public PreviousPassword()
 {
    CreateDate = DateTimeOffset.Now;
 }
  
 [Key, Column(Order = 0)]
  
 public string PasswordHash { get; set; }
  
 public DateTimeOffset CreateDate { get; set; }
  
 [Key, Column(Order = 1)]
  
 public string UserId { get; set; }
  
 public virtual ApplicationUser User { get; set; }
  
 }
در این کلاس، فیلد 'Password' مقدار Hash شده کلمه عبور را نگاه میدارد و توسط فیلد 'UserId' رفرنس می‌شود. فیلد 'CreateDate' یک مقدار timestamp ذخیره می‌کند که تاریخ ثبت کلمه عبور را مشخص می‌نماید. توسط این فیلد می‌توانیم تاریخچه کلمات عبور را فیلتر کنیم و مثلا 5 رکورد آخر را بگیریم.

Entity Framework Code First جدول 'PreviousPasswords' را می‌سازد و با استفاده از فیلدهای 'UserId' و 'Password' کلید اصلی (composite primary key) را ایجاد می‌کند. برای اطلاعات بیشتر درباره قرارداهای EF Code First به  این لینک  مراجعه کنید.

  • خاصیت جدیدی به کلاس ApplicationUser اضافه کنید تا لیست آخرین کلمات عبور استفاده شده را نگهداری کند.
public class ApplicationUser : IdentityUser
{
    public ApplicationUser() : base()
    {
        PreviousUserPasswords = new List<PreviousPassword>();
     }

     public virtual IList<PreviousPassword> PreviousUserPasswords { get; set; }
}

 همانطور که پیشتر گفته شد، کلاس UserStore پیاده سازی API‌های لازم برای مدیریت کاربران را در بر می‌گیرد. هنگامی که کاربر برای نخستین بار در سایت ثبت می‌شود باید مقدار Hash کلمه عبورش را در جدول تاریخچه کلمات عبور ذخیره کنیم. از آنجا که UserStore بصورت پیش فرض متدی برای چنین عملیاتی معرفی نمی‌کند، باید یک override تعریف کنیم تا این مراحل را انجام دهیم. پس ابتدا باید کلاس سفارشی جدیدی بسازیم که از UserStore ارث بری کرده و آن را توسعه می‌دهد. سپس از این کلاس سفارشی در ApplicationUserManager بعنوان پیاده سازی پیش فرض UserStore استفاده می‌کنیم. پس کلاس جدیدی در پوشه IdentityExtensions ایجاد کنید.
public class ApplicationUserStore : UserStore<ApplicationUser>
{ 
    public ApplicationUserStore(DbContext context) : base(context)  { }
  
    public override async Task CreateAsync(ApplicationUser user)
    {
        await base.CreateAsync(user);
        await AddToPreviousPasswordsAsync(user, user.PasswordHash);
    }
  
     public Task AddToPreviousPasswordsAsync(ApplicationUser user, string password)
     {
        user.PreviousUserPasswords.Add(new PreviousPassword() { UserId = user.Id, PasswordHash = password });
  
        return UpdateAsync(user);
     }
 }

 متد 'AddToPreviousPasswordsAsync' کلمه عبور را در جدول 'PreviousPasswords' ذخیره می‌کند.

هرگاه کاربر سعی در بازنشانی یا تغییر کلمه عبورش دارد باید این متد را فراخوانی کنیم. API‌های لازم برای این کار در کلاس UserManager تعریف شده اند. باید این متدها را override کنیم و فراخوانی متد مذکور را پیاده کنیم. برای این کار کلاس ApplicationUserManager را باز کنید و متدهای ChangePassword و ResetPassword را بازنویسی کنید.
public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        private const int PASSWORD_HISTORY_LIMIT = 5;

        public ApplicationUserManager() : base(new ApplicationUserStore(new ApplicationDbContext()))
        {
            PasswordValidator = new CustomPasswordValidator(10);
        }

        public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword)
        {
            if (await IsPreviousPassword(userId, newPassword))
            {
                return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password"));
            }

            var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword);

            if (result.Succeeded)
            {
                var store = Store as ApplicationUserStore;
                await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword));
            }

            return result;
        }

        public override async Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword)
        {
            if (await IsPreviousPassword(userId, newPassword))
            {
                return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password"));
            }

            var result = await base.ResetPasswordAsync(userId, token, newPassword);

            if (result.Succeeded)
            {
                var store = Store as ApplicationUserStore;
                await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword));
            }

            return result;
        }

        private async Task<bool> IsPreviousPassword(string userId, string newPassword)
        {
            var user = await FindByIdAsync(userId);

            if (user.PreviousUserPasswords.OrderByDescending(x => x.CreateDate).
                Select(x => x.PasswordHash).Take(PASSWORD_HISTORY_LIMIT)
                .Where(x => PasswordHasher.VerifyHashedPassword(x, newPassword) != PasswordVerificationResult.Failed).Any())
            {
                return true;
            }

            return false;
        }
    }
فیلد 'PASSWORD_HISTORY_LIMIT' برای دریافت X رکورد از جدول تاریخچه کلمه عبور استفاده می‌شود. همانطور که می‌بینید از متد سازنده کلاس ApplicationUserStore برای گرفتن متد جدیدمان استفاده کرده ایم. هرگاه کاربری سعی می‌کند کلمه عبورش را بازنشانی کند یا تغییر دهد، کلمه عبورش را با 5 کلمه عبور قبلی استفاده شده مقایسه می‌کنیم و بر این اساس مقدار true/false بر می‌گردانیم.

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

سورس کد این مثال را می‌توانید از این لینک دریافت کنید. نام پروژه Identity-PasswordPolicy است، و زیر قسمت Samples/Identity قرار دارد.

اشتراک‌ها
ارائه‌ی Raspberry Pi 4

به همراه پردازشگری 2 تا 4 برابر قوی‌تر، دو پورت خروجی تصویری میکرو HDMI با کیفیت تصویر 4K و پورت شبکه‌ی گیگابیتی آنبورد

ارائه‌ی Raspberry Pi 4
نظرات مطالب
ارسال ایمیل در ASP.NET Core
- مدتی هست که کار با پروتکل SSL و TLS، از داخل، با مشکل مواجه شده:

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

+ پارامتر shouldValidateServerCertificate اضافه شد.