مطالب
تزریق وابستگی‌ها به صورت پویا در فروشگاه‌ساز Nop Commerce
این روش منحصر به Nop نیست و امکان استفاده‌ی از آن بر روی هر سورس دیگری نیز وجود دارد. همچنین اگر در رابطه با NopCommerce اطلاعاتی ندارید، میتوانید از اینجا جهت آشنا شدن با این فروشگاه ساز Asp.net core استفاده کنید.
همانطور که در جریان هستید، برای اینکه بحث DI را در پروژه داشته باشیم، باید به ازای هر سرویس مشخص کنیم که کدام اینترفیس، به کدام کلاس، map شود. به بیان دیگر باید مشخص کرد هر وقت یک شیء از Container درخواست شد، از چه کلاسی باید این شیء ساخته شود؛ در عین‌حال باید LifeTime وجود شیء در حافظه نیز مشخص شود. حال تصور کنید تعداد سرویس‌های شما در حال زیاد شدن است. در این حالت مجبور هستید دائما این سرویس‌ها را ثبت کنید؛ علاوه بر اینکه باید کدهای تکراری را جهت تعریف این سرویس‌ها بنویسید و باید به‌خاطر بسپارید که سرویس جدید را ثبت کنید. در این مقاله تلاش بر این است تا دیگر نیازی به تعریف کردن تک تک سرویس‌ها نباشد؛ به‌طوری که با رعایت دو قانون کلی بتوان سرویس‌ها را به صورت خودکار ثبت کرد.

مراحل پیاده سازی

 یک اینترفیس را به اسم ICustomService ایجاد کردم که  یک Prop به اسم InjectType دارد و مشخص میکند به چه صورتی این سرویس به ServiceCollection تزریق شود. از طرفی با استفاده از Order، الویت اضافه شدن سرویس به ServiceCollection را مشخص میکنیم و در نهایت با ImplementationType مشخص میکنیم سرویسی که اضافه شده، باید به یک اینترفیس Map شود یا خیر؟ اما مهم‌تر از اینکه ویژگی‌های تزریق وابستگی مشخص شود، مشخص میکند چه سرویس‌هایی توسط ما اضافه شده‌اند و از سرویس‌های nop تفکیک می‌شوند.
namespace Nop.Services
{
    public interface ICustomService
    {
        protected InjectType Inject { get;  }
        protected int Order { get; }
        protected ImplementationType implementationType { get; }
    }
    public enum ImplementationType
    {
        WithInterface = 0,
        WithoutInterface = 1
    }
    public enum InjectType
    {
        Scopped=0,
        Transit=1,
        SingleTon=2
    }
}

قانون اول

برای هر سرویسی که ایجاد میکنیم و میخواهیم به DI معرفی کنیم، آن سرویس باید ICustomService را پیاده سازی کرده باشد؛ دقیقا به خاطر دو دلیلی که در بالا به آن‌ها اشاره شد.

قانون دوم

هر کلاسی که Interface مرتبط به سرویس‌ها را پیاده سازی میکند، باید prop InjectType را در سازنده‌ی خودش مقدار دهی کند. بدین شکل متوجه میشویم از چه طریقی باید تزریق انجام شود. تا اینجا یک چارچوب را مشخص کردیم تا سرویس‌ها را بتوانیم تشخیص دهیم\ اما هنوز کار اصلی باقی مانده‌است. برای نمونه میتوان کد زیر را در نظر گرفت :

namespace Nop.Services
{
    public interface IMyCustomService: ICustomService
    {
        int ok();
    }
}
برای پیاده سازی سرویس ایجاد شده، کد زیر را ایجاد میکنیم :
namespace Nop.Services
{
    public class MyCustomService : IMyCustomService
    {
        public InjectType Inject { get;  }
        public int Order { get;  }
        public ImplementationType implementationType { get;  }

        public MyCustomService()
        {
            implementationType = ImplementationType.WithInterface;
            Inject = InjectType.Scopped;
            Order = 1;
        }
        public int ok()
        {
            return 10;
        }
    }
}

تعیین نقطه شروع

باید نقطه شروع به کار Nop را پیدا کنیم. از آنجایی که با معماری Nop جلو میرویم، با کمی بررسی و دیدن کد‌ها، به کلاسی میرسیم به اسم NopStartup در قسمت Nop.Web.Framework. مسیر دقیق آن: Nop.Web.Framework\Infrastructure\NopStartup.cs. حالا این کلاس چیست؟ در واقع هر کلاسی که از سرویس INopStartup ارث بری کرده باشد، اولویت پیدا میکند و قبل از کدهای دیگر اجرا می‌شود. باید کلاس جدیدی را به اسم مثلا CustomDependencyInjection ایجاد کنیم، با این تفاوت که حتما از کلاس NopStartup ارث بری کرده باشد و همچنین حتما باید متدی را به اسم ConfigureServices، بازنویسی کند. حالا داخل متدی که گفتم باید شروع کنیم به کار.

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

namespace Nop.Web.Framework.Infrastructure
{
    public class CustomDependencyInjection : NopStartup
    {
        private static bool IsSubInterface(Type t1, Type t2)
        {
            if (!t2.IsAssignableFrom(t1))
                return false;

            if (t1.BaseType == null)
                return true;

            return !t2.IsAssignableFrom(t1.BaseType);
        }
        public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
        {
            //-------------Get All Services-------------
            var asm = AppDomain.CurrentDomain
                 .GetAssemblies()
                 .Single(x => x.FullName.Contains("Nop.Services"));
            //-------------find Services that inheriance of ICustomService-------------
            var types = asm.DefinedTypes.Where(x => IsSubInterface(x, typeof(ICustomService)));
            //-----------Get All Custom Service Classess-------
            var allRelatedClassServices = types
                .Where(x => x.IsClass)
                .OrderBy(x=>(Int32)x.GetProperty("Order")
                .GetValue(Activator.CreateInstance(x), null));

            //-----------Get All Custom Service Interfaces-------
            var allRelatedInterfaceServices = types.Where(x => x.IsInterface);
            //-----------Matche Class Services To Related Interface Services-------
            TypeInfo interfaceService=null;
            foreach (var classService in allRelatedClassServices)
            {
                //-----------detect Implementation Type for service-----------
                var implementationValue = (ImplementationType)classService.GetProperty("implementationType")
                   .GetValue(Activator.CreateInstance(classService), null);

                //-----------detect inject type for service-----------
                var InjectValue = (InjectType)classService.GetProperty("Inject")
                   .GetValue(Activator.CreateInstance(classService), null);

                //-----------get related interface for service class-----------
                if (implementationValue == ImplementationType.WithInterface)
                    interfaceService = allRelatedInterfaceServices.Single(x => x.Name == $"I{classService.Name}");

               

                //----------finally Add Custom Service To Service Collection-----------
                switch (InjectValue)
                {
                    case InjectType.Scopped:
                        if(interfaceService!=null)
                            services.AddScoped(interfaceService, classService);
                        else
                            services.AddScoped(classService);
                        break;
                    case InjectType.Transit:
                        if (interfaceService != null)
                            services.AddTransient(interfaceService, classService);
                        else
                            services.AddTransient(classService);
                        break;
                    case InjectType.SingleTon:
                        if (interfaceService != null)
                            services.AddSingleton(interfaceService, classService);
                        else
                            services.AddSingleton(classService);
                        break;
                    default:
                        break;
                }
                interfaceService = null;
            }
        }
       
    }
}
نکته‌ی آخر آن که این داستان‌ها صرفا برای سرویس‌هایی هست که توسط برنامه نویس به پروژه‌ی Nop اضافه می‌شود.

لینک گیت‌هاب  
نظرات مطالب
مدیریت محل اعمال Google analytics در ASP.NET MVC
سلام؛ میخواستم بدونم در صورتیکه قصد داشته باشیم آنالیز ترافیک بازدید سایت رو بدون استفاده از سرویس‌های مختلف (PersianStat, Google Analytic)، در وب سایت خودمون پیاده سازی کنیم چه روش هایی پیشنهاد میکنین؟

وب سایت بر پایه MVC 5 ساخته شده. روشی که خودم در نظر دارم، یک جدول در بانک اطلاعاتی هست که شامل یک سری فیلد مانند IP و User-Agent و تاریخ و ساعت و ...

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

Session_Start در فایل Global گزینه‌ی خوبی هست برای اینکار ولی در وب سایت از Session استفاده نمیکنم و غیر فعالش کردم در وب کانفیگ، بنابراین جوابگو نیست.
ممنون میشم اگر بتونید راهنمایی کنید.
پروژه‌ها
بدست آوردن آمار بازدید وب سایت در ASP.NET MVC
پروژه بدست آوردن آمار کامل بازدید‌های یک وب سایت در ASP.NET MVC
نمونه آنلاین آن‌را می‌توانید در اینجا مشاهده کنید.

این پروژه در حال تکمیل میباشد و برای همکاری بر روی GITHUB قرار گرفته‌است.

امکانات پروژه
- افراد آنلاین
- بازدید امروز
- بازدید کل از زمان راه اندازی پروژه
- بازدید یونیک
- درصد استفاده از مرورگرها
- درصد استفاده از سیستم عامل ها
- در آینده آمارهای بیشتری به پروژه افزوده خواهند شد.

آمار بازدید به تفکیک کشورها

آمار بازدید به تفکیک کشور‌ها و درصد بازدید بر روی نمودار

آمار بازدید بر روی نقشه گوگل به تفکیک کشورها و تعداد بازدید

گاهی اوقات برای زنده نگاه داشتن (Keep Alive) اپلیکیشن باید در بازه‌های زمانی مشخص سایت را پینگ کرد. برای اینکه این بازدید‌ها در آمار ما تاثیر نگذارند، می‌توان از صفحه تنظیمات آن‌ها را جزو آمار حساب نکرد.


به روز رسانی اول :

  • افزوده شدن نموار جدید (pie chart,Donut chart,Bar chart)

  • شناسایی مرورگر Edge
  • داینامیک شدن جداول صفحه ایندکس

به روز رسانی دوم:

  • افزوده شدن جدول ارجاعات
  • افزوده شده مشخصات بازدید کننده کنونی
  • اصلاح font-awesome برای internet explorer

به روز رسانی سوم:

  • استفاده از HttpModule
  • استفاده از SignalR برای نمایش بلادرنگ کاربران آنلاین
  • اصلاح نمایش کاربران آنلاین
  • استفاده از JQuery Noty برای نمایش بلادرنگ کاربران آنلاین
  • اصلاح عدم نمایش آیکون برخی از مرورگرها و سیستم‌های عامل
  • افزوده شدن notification صوتی برای اعلان ورود و خروج کاربران


به روز رسانی چهارم:

  • افزوده شدن تاریخ شمسی
  • فیلتر و سامان دهی جدول ارجاعات
  • افزوده شدن جدول صفحات مشاهده شده سایت با تعداد بازدید
  • افزودن امکان مشاهده پر بازدید‌ترین و کم بازدیدترین روز
نظرات اشتراک‌ها
تحریم شکن بگذر!
در کل بنظر من وب سایت جالبی نیست وب سایت دولوپر اندروید
وارد کردم در قسمت مخصوص سایت هایی که پشتیبانی میکنه برای گذر از تحریم پیغام : بررسی و سپس پشتیبانی خواهد شد. متشکریم !  را نشان میدهد.
مطالب
غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامه‌های مبتنی بر ASP.NET Core
چند روز قبل، یکی از کانال‌های فنی معروف یوتیوب با بیش از 15 میلیون مشترک، هک و پاک شد! که داستان آن‌را در اینجا می‌توانید پیگیری کنید. در این هک، مهاجم در سعی اول، پیشنهاد پشتیبانی مالی از شبکه را داده و در ایمیل دوم، پس از جلب اعتماد اولیه، یک فایل به ظاهر PDF مفاد قرارداد را ارسال کرده که با کلیک بر روی آن، تمام کوکی‌های یوتیوب مالک کانال، سرقت و مورد سوء استفاده قرار گرفته! در یک چنین حالتی، مهم نیست که شما اعتبارسنجی دو مرحله‌ای را فعال کرده‌اید و یا از بهترین روش‌های رمزنگاری برای امن کردن اطلاعات کوکی و یا توکن خود استفاده کرده‌اید، همینقدر که اصل محتوای کوکی و یا توکن شما در اختیار شخص دیگری قرار گیرد، می‌تواند بدون نیاز به لاگین و دانستن کلمه‌ی عبور شما، بجای شما وارد سیستم شده و تغییرات دلخواهی را اعمال کند!
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه می‌توانیم یک چنین حملاتی را مشکل‌تر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکن‌ها و یا کوکی‌های سرقت شده، در برنامه‌های مبتنی بر ASP.NET Core بررسی خواهیم کرد.


توسعه‌ی یک سرویس تشخیص مرورگر و سیستم عامل شخص وارد شده‌ی به سیستم

یکی از روش‌های غیرممکن کردن یک چنین حملاتی، درج مشخصات سیستم عامل و مرورگر شخص وارد شده‌ی به سیستم، در کوکی و همچنین توکن صادر شده‌ی حاصل از اعتبارسنجی موفق است. سپس زمانیکه قرار است از اطلاعات این کوکی و یا توکن در برنامه استفاده شود، این اطلاعات را با اطلاعات درخواست جاری کاربر مقایسه کرده و در صورت عدم تطابق، درخواست او را برگشت می‌زنیم. برای مثال اگر عملیات لاگین، در ویندوز انجام شده و اکنون توکن و یا کوکی حاصل، در سیستم عامل اندروید در حاصل استفاده‌است، یعنی ... این عملیات مشکوک است و باید خاتمه یابد و کاربر باید مجبور به لاگین مجدد شود و نه اعتبارسنجی خودکار بدون زحمت!
برای این منظور می‌توان از کتابخانه‌ی UA-Parser استفاده کرد و توسط آن سرویس زیر را توسعه داد:
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using UAParser;

namespace ASPNETCore2JwtAuthentication.Services;

/// <summary>
///     To invalidate an old user's token from a new device
/// </summary>
public class DeviceDetectionService : IDeviceDetectionService
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ISecurityService _securityService;

    public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
    {
        _securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);

    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }

    public string GetDeviceDetailsHash(HttpContext context) =>
        _securityService.GetSha256Hash(GetDeviceDetails(context));

    public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);

    public string GetCurrentUserTokenDeviceDetailsHash() =>
        GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
    {
        if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
        {
            return null;
        }

        return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
    }

    public bool HasCurrentUserTokenValidDeviceDetails() =>
        HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
        string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
                      StringComparison.Ordinal);

    private static string GetUserAgent(HttpContext context)
    {
        if (context is null)
        {
            return null;
        }

        return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
                   ? userAgent.ToString()
                   : null;
    }
}
توضیحات:
اصل کار این سرویس در متد زیر رخ می‌دهد:
    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }
در اینجا با استفاده از کتابخانه‌ی UA-Parser، سعی می‌کنیم تا جزئیات مرورگر و سیستم عامل شخص را تهیه کنیم. سپس در قسمت دیگری از این سرویس، این اطلاعات را هش می‌کنیم. از این جهت که هم حجم آن کاهش یابد و بی‌جهت کوکی و یا توکن ما را حجیم نکند و هم بررسی محتوای آن جهت شبیه سازی آن، غیرممکن شود. هر مشخصات دریافتی در حین لاگین، همواره یک هش مشخص و یکتا را دارد. به همین جهت متدهای هش کردن اطلاعات را هم در اینجا مشاهده می‌کنید. به علاوه‌ی متد HasUserTokenValidDeviceDetails که کار آن، دریافت Claim مرتبط با این اطلاعات، از کوکی و یا توکن جاری و مقایسه‌ی آن با اطلاعات Http Request جاری است. اگر این دو یکی نبودند، یعنی احتمال سوء استفاده‌ی از اطلاعات شخص، وجود دارد.


اضافه کردن اطلاعات مشخصات دستگاه کاربر به کوکی و یا توکن او

همانطور که عنوان شد، در متد HasUserTokenValidDeviceDetails، ابتدا مشخصات دستگاه موجود در کوکی و یا توکن دریافتی، استخراج می‌شود. به همین جهت نیاز است این مشخصات را دقیقا در حین لاگین موفق، به صورت یک Claim جدید، برای مثال از نوع ClaimTypes.System به مجموعه‌ی Claims کاربر اضافه کرد:
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
ClaimValueTypes.String, _configuration.Value.Issuer),


یکپارچه کردن DeviceDetectionService با اعتبارسنج‌های کوکی‌ها و توکن‌ها

پس از افزودن مشخصات سیستم کاربر وارد شده‌ی به سیستم، به صورت یک Claim جدید به توکن‌ها، روش اعتبارسنجی اطلاعات موجود در توکن رسیده، در رخ‌داد گردان OnTokenValidated است که امکان دسترسی به HttpContext و محتوای توکن را میسر می‌کند:
.AddJwtBearer(cfg =>
{
      cfg.Events = new JwtBearerEvents
      {
           OnTokenValidated = context =>
           {
               var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
              return tokenValidatorService.ValidateAsync(context);
           },
       };
  });
و یا اگر از کوکی‌ها استفاده می‌کنید، معادل آن به صورت زیر است:
.AddCookie(options =>
{
    options.Events = new CookieAuthenticationEvents
    {
       OnValidatePrincipal = context =>
       {
         var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
         return cookieValidatorService.ValidateAsync(context);
       }
    };
});

در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامه‌ی تولید کننده‌ی JWT در اینجا و برای یک برنامه‌ی مبتنی بر کوکی‌ها در اینجا می‌توانید مشاهده کنید.
مطالب دوره‌ها
نگاهی به SignalR Hubs
Hubs کلاس‌هایی هستند جهت پیاده سازی push services در SignalR و همانطور که در قسمت قبل عنوان شد، در سطحی بالاتر از اتصال ماندگار (persistent connection) قرار می‌گیرند. کلاس‌های Hubs بر مبنای یک سری قرار داد پیش فرض کار می‌کنند (ایده Convention-over-configuration) تا استفاده نهایی از آن‌ها را ساده‌تر کنند.
Hubs به نوعی یک فریم ورک سطح بالای RPC نیز محسوب می‌شوند (Remote Procedure Calls) و آن‌را برای انتقال انواع و اقسام داده‌ها بین سرور و کلاینت و یا فراخوانی متدی در سمت کلاینت یا سرور، بسیار مناسب می‌سازد. برای مثال اگر قرار باشد با persistent connection به صورت مستقیم کار کنیم، نیاز است تا بسیاری از مسایل serialization و deserialization اطلاعات را خودمان پیاده سازی و اعمال نمائیم.


قرار دادهای پیش فرض Hubs

- متدهای public کلاس‌های Hubs از طریق دنیای خارج قابل فراخوانی هستند.
- ارسال اطلاعات به کلاینت‌ها از طریق فراخوانی متدهای سمت کلاینت انجام خواهد شد. (نحوه تعریف این متدها در سمت سرور بر اساس قابلیت‌های dynamic اضافه شده به دات نت 4 است که در ادامه در مورد آن بیشتر بحث خواهد شد)


مراحل اولیه نوشتن یک Hub
الف) یک کلاس Hub را تهیه کنید. این کلاس، از کلاس پایه Hub تعریف شده در فضای نام Microsoft.AspNet.SignalR باید مشتق شود. همچنین این کلاس می‌تواند توسط ویژگی خاصی به نام HubName نیز مزین گردد تا در حین برپایی اولیه سرویس، از طریق زیرساخت‌های SignalR به نامی دیگر (یک alias یا نام مستعار خاص) قابل شناسایی باشد. متدهای یک هاب می‌توانند نوع‌های ساده یا پیچیده‌ای را بازگشت دهند و همه چیز در اینجا نهایتا به فرمت JSON رد و بدل خواهد شد (فرمت پیش فرض که در پشت صحنه از کتابخانه معروف JSON.NET استفاده می‌کند؛ این کتابخانه سورس باز به دلیل کیفیت بالای آن، از زمان ارائه MVC4 به عنوان جزئی از مجموعه کارهای مایکروسافت قرار گرفته است).
ب) مسیریابی و Routing را تعریف و اصلاح نمائید.
و ... از نتیجه استفاده کنید.


تهیه اولین برنامه با SignalR

ابتدا یک پروژه خالی ASP.NET را آغاز کنید (مهم نیست MVC باشد یا WebForms). برای سادگی بیشتر، در اینجا یک ASP.NET Empty Web application درنظر گرفته شده است. در ادامه قصد داریم یک برنامه Chat را تهیه کنیم؛ از این جهت که توسط یک برنامه Chat بسیاری از مفاهیم مرتبط با SignalR را می‌توان در عمل توضیح داد.
اگر از VS 2012 استفاده می‌کنید، گزینه SignalR Hub class جزئی از آیتم‌های جدید قابل افزودن به پروژه است (منوی پروژه، گزینه new item آن) و پس از انتخاب این قالب خاص، تمامی ارجاعات لازم نیز به صورت خودکار به پروژه جاری اضافه خواهند شد.


و اگر از VS 2010 استفاده می‌کنید، نیاز است از طریق NuGet ارجاعات لازم را به پروژه خود اضافه نمائید:
 PM> Install-Package Microsoft.AspNet.SignalR
اکنون یک کلاس خالی جدید را به نام ChatHub، به آن اضافه کنید. سپس کدهای آن را به نحو ذیل تغییر دهید:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR02
{
    [HubName("chat")]
    public class ChatHub : Hub
    {
        public void SendMessage(string message)
        {
            Clients.All.hello(message);
        }
    }
}
همانطور که ملاحظه می‌کنید این کلاس از کلاس پایه Hub مشتق شده و توسط ویژگی HubName، نام مستعار chat را یافته است.
کلاس پایه Hub یک سری متد و خاصیت را در اختیار کلاس‌های مشتق شده از آن قرار می‌دهد. ساده‌ترین راه برای آشنایی با این متدها و خواص مهیا، کلیک راست بر روی نام کلاس پایه Hub و انتخاب گزینه Go to definition است.
برای نمونه در کلاس ChatHub فوق، از خاصیت Clients برای دسترسی به تمامی آن‌ها و سپس فراخوانی متد dynamic ایی به نام hello که هنوز وجود خارجی ندارد، استفاده شده است.
اهمیتی ندارد که این کلاس در اسمبلی اصلی برنامه وب قرار گیرد یا مثلا در یک class library به نام Services. همینقدر که از کلاس Hub مشتق شود به صورت خودکار در ابتدای برنامه اسکن گردیده و یافت خواهد شد.

مرحله بعد، افزودن فایل global.asax به برنامه است. زیرا برای کار با SignalR نیاز است تنظیمات Routing و مسیریابی خاص آن‌را اضافه نمائیم. پس از افرودن فایل global.asax، به فایل Global.asax.cs مراجعه کرده و در متد Application_Start آن تغییرات ذیل را اعمال نمائید:
using System;
using System.Web;
using System.Web.Routing;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs();
        }
    }
}

یک نکته مهم
 اگر از ASP.NET MVC استفاده می‌کنید، این تنظیم مسیریابی باید پیش از تعاریف پیش فرض موجود قرار گیرد. در غیراینصورت مسیریابی‌های SignalR کار نخواهند کرد.

اکنون برای آزمایش برنامه، برنامه را اجرا کرده و مسیر ذیل را فراخوانی کنید:
 http://localhost/signalr/hubs
در این حال اگر برنامه را برای مثال با مرورگر chrome باز کنید، در این آدرس، فایل جاوا اسکریپتی SignalR، قابل مشاهده خواهد بود. مرورگر IE پیغام می‌دهد که فایل را نمی‌تواند باز کند. اگر به انتهای خروجی آدرس مراجعه کنید، چنین سطری قابل مشاهده است:
  proxies.chat = this.createHubProxy('chat');
و کلمه chat دقیقا از مقدار معرفی شده توسط ویژگی HubName دریافت گردیده است.

تا اینجا ما موفق شدیم اولین Hub خود را تشکیل دهیم.


بررسی پروتکل Hub

اکنون که اولین Hub خود را ایجاد کرده‌ایم، بد نیست اندکی با زیر ساخت آن نیز آشنا شویم.
مطابق مسیریابی تعریف شده در Application_Start، مسیر ابتدایی دسترسی به SignalR با افزودن اسلش SignalR به انتهای مسیر ریشه سایت بدست می‌آید و اگر به این آدرس یک اسلش hubs را نیز اضافه کنیم، فایل js metadata مرتبط را نیز می‌توان دریافت و مشاهده کرد.

زمانیکه یک کلاینت قصد اتصال به یک Hub را دارد، دو مرحله رخ خواهد داد:
الف) negotiate: در این حالت امکانات قابل پشتیبانی از طرف سرور مورد پرسش قرار می‌گیرند و سپس بهترین حالت انتقال، انتخاب می‌گردد. این انتخاب‌ها به ترتیب از چپ به راست خواهند بود:
 Web socket -> SSE -> Forever frame -> long polling


به این معنا که اگر برای مثال امکانات Web sockets مهیا بود، در همینجا کار انتخاب نحوه انتقال اطلاعات خاتمه یافته و Web sockets انتخاب می‌شود.
تمام این مراحل نیز خودکار است و نیازی نیست تا برای تنظیمات آن کار خاصی صورت گیرد. البته در سمت کلاینت، امکان انتخاب یکی از موارد یاد شده به صورت صریح نیز وجود دارد.
ب) connect: اتصالی ماندگار برقرار می‌گردد.

در پروتکل Hub تمام اطلاعات JSON encoded هستند و یک سری مخفف‌هایی را در این بین نیز ممکن است مشاهده نمائید که معنای آن‌ها به شرح زیر است:
 C: cursor
M: Messages
H: Hub name
M: Method name
A: Method args
T: Time out
D: Disconnect
این مراحل را در قسمت بعد، پس از ایجاد یک کلاینت، بهتر می‌توان توضیح داد.


روش‌های مختلف ارسال اطلاعات به کلاینت‌ها

به چندین روش می‌توان اطلاعاتی را به کلاینت‌ها ارسال کرد:
1) استفاده از خاصیت Clients موجود در کلاس Hub
2) استفاده از خواص و متد‌های dynamic
در این حالت اطلاعات متد dynamic و پارامترهای آن به صورت JSON encoded به کلاینت ارسال می‌شوند (به همین جهت اهمیتی ندارند که در سرور وجود خارجی دارند یا خیر و به صورت dynamic تعریف شده‌اند).
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR02
{
    [HubName("chat")]
    public class ChatHub : Hub
    {
        public void SendMessage(string message)
        {
            var msg = string.Format("{0}:{1}", Context.ConnectionId, message);
            Clients.All.hello(msg);
        }
    }
}
برای نمونه در اینجا متد hello به صورت dynamic تعریف شده است (جزئی از متدهای خاصیت All نیست و اصلا در سمت سرور وجود خارجی ندارد) و خواص Context و Clients، هر دو در کلاس پایه Hub قرار دارند.
حالت Clients.All به معنای ارسال پیامی به تمام کلاینت‌های متصل به هاب ما هستند.

3) روش‌های دیگر، استفاده از خاصیت dynamic دیگری به نام Caller است که می‌توان بر روی آن متد دلخواهی را تعریف و فراخوانی کرد.
 //این دو عبارت هر دو یکی هستند
Clients.Caller.hello(msg);
Clients.Client(Context.ConnectionId).hello(msg);
انجام اینکار با روش ارائه شده در سطر دومی که ملاحظه می‌کنید، در عمل یکی است؛ از این جهت که Context.ConnectionId همان ConnectionId فراخوان می‌باشد.
در اینجا پیامی صرفا به فراخوان جاری سرویس ارسال می‌گردد.

4) استفاده از خاصیت dynamic ایی به نام Clients.Others
 Clients.Others.hello(msg);
در این حالت، پیام، به تمام کلاینت‌های متصل، منهای کلاینت فراخوان ارسال می‌گردد.

5) استفاده از متد Clients.AllExcept
این متد می‌تواند آرایه‌ای از ConnectionId‌هایی را بپذیرد که قرار نیست پیام ارسالی ما را دریافت کنند.

6) ارسال اطلاعات به گروه‌ها
تعداد مشخصی از ConnectionIdها یک گروه را تشکیل می‌دهند؛ مثلا اعضای یک chat room.
        public void JoinRoom(string room)
        {
            this.Groups.Add(Context.ConnectionId, room);
        }

        public void SendMessageToRoom(string room, string msg)
        {
            this.Clients.Group(room).hello(msg);
        }
در اینجا نحوه الحاق یک کلاینت به یک room یا گروه را مشاهده می‌کنید. همچنین با مشخص بودن نام گروه، می‌توان صرفا اطلاعاتی را به اعضای آن گروه خاص ارسال کرد.
خاصیت Group در کلاس پایه Hub تعریف شده است.
نکته مهمی را که در اینجا باید درنظر داشت این است که اطلاعات گروه‌ها به صورت دائمی در سرور ذخیره نمی‌شوند. برای مثال اگر سرور ری استارت شود، این اطلاعات از دست خواهند رفت.


آشنایی با مراحل طول عمر یک Hub

اگر به تعاریف کلاس پایه Hub دقت کنیم:
    public abstract class Hub : IHub, IDisposable
    {
        protected Hub();
        public HubConnectionContext Clients { get; set; }
        public HubCallerContext Context { get; set; }
        public IGroupManager Groups { get; set; }

        public void Dispose();
        protected virtual void Dispose(bool disposing);
        public virtual Task OnConnected();
        public virtual Task OnDisconnected();
        public virtual Task OnReconnected();
    }
در اینجا، تعدادی از متدها virtual تعریف شده‌اند که تمامی آن‌ها را در کلاس مشتق شده نهایی می‌توان override و مورد استفاده قرار داد. به این ترتیب می‌توان به اجزا و مراحل مختلف طول عمر یک Hub مانند برقراری اتصال یا قطع شدن آن، دسترسی یافت. تمام این متدها نیز با Task معرفی شده‌اند؛ که معنای غیرهمزمان بودن پردازش آن‌ها را بیان می‌کند.
تعدادی از این متدها را می‌توان جهت مقاصد logging برنامه مورد استفاده قرار داد و یا در متد OnDisconnected اگر اطلاعاتی را در بانک اطلاعاتی ذخیره کرده‌ایم، بر این اساس می‌توان وضعیت نهایی را تغییر داد.


ارسال اطلاعات از یک Hub به Hub دیگر در برنامه

فرض کنید یک Hub دوم را به نام MinitorHub به برنامه اضافه کرده‌اید. اکنون قصد داریم از داخل ChatHub فوق، اطلاعاتی را به آن ارسال کنیم. روش کار به نحو زیر است:
        public override System.Threading.Tasks.Task OnDisconnected()
        {
            sendMonitorData("OnDisconnected", Context.ConnectionId);
            return base.OnDisconnected();
        }

        private void sendMonitorData(string type, string connection)
        {
            var ctx = GlobalHost.ConnectionManager.GetHubContext<MonitorHub>();
            ctx.Clients.All.newEvenet(type, connection);
        }
در اینجا با override کردن OnDisconnected به رویداد خاتمه اتصال یک کلاینت دسترسی یافته‌ایم. سپس قصد داریم این اطلاعات را توسط متد sendMonitorData به Hub دومی به نام MonitorHub ارسال کنیم که نحوه پیاده سازی آن‌را در کدهای فوق ملاحظه می‌کنید. GlobalHost.ConnectionManager یک dependency resolver توکار تعریف شده در SignalR است.
مورد استفاده دیگر این روش، ارسال اطلاعات به کلاینت‌ها از طریق کدهای یک برنامه تحت وب است (که در همان پروژه هاب واقع شده است). برای مثال در یک اکشن متد یا یک روال رویدادگردان کلیک نیز می‌توان از GlobalHost.ConnectionManager استفاده کرد.
مطالب
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). بدین منظور می‌توانید یکی از پست‌های اخیر را مطالعه کنید.

مسیرراه‌ها
+AngularJS 2.0
شروع به کار با AngularJS 2.0 و TypeScript
فرم‌های مبتنی بر قالب‌ها در Angular
Angular Animation
آپلود فایل
Angular Material 6x
  • ابزارهای توسعه