ایجاد پروژهی ASP.NET Core Web API
برای تامین اطلاعات برنامهی سمت کلاینت Blazor WASM و همچنین فراهم آوردن زیرساخت اعتبارسنجی کاربران آن، نیاز به یک پروژهی ASP.NET Core Web API داریم که آنرا با اجرای دستور dotnet new webapi در یک پوشهی خالی، برای مثال به نام BlazorWasm.WebApi ایجاد میکنیم.
البته این پروژه، از زیرساختی که در برنامهی Blazor Server بررسی شدهی تا این قسمت، ایجاد کردیم نیز استفاده خواهد کرد. همانطور که پیشتر نیز عنوان شد، هدف از قسمت Blazor Server مثال این سری، آشنایی با مدل برنامه نویسی خاص آن بود؛ وگرنه میتوان کل این پروژه را با Blazor Server و یا کل آنرا با Web API + Blazor WASM نیز پیاده سازی کرد. در این مثال، قسمتهای مدیریتی برنامهی مدیریت هتل را توسط Blazor Server (مانند قسمتهای تعریف اتاقها و امکانات رفاهی هتل) و قسمت مخصوص کاربران آنرا مانند رزرو کردن اتاقها، توسط Blazor WASM پیاده سازی میکنیم. به همین جهت قسمتهایی از این دو پروژه، مانند سرویسهای استفاده شدهی در پروژهی Blazor server، در پروژهی Web API مکمل Blazor WASM، قابلیت استفادهی مجدد را دارند.
افزودن سرویسهای آغازین مورد نیاز، به پروژهی Web API
در فایل آغازین BlazorWasm\BlazorWasm.WebApi\Startup.cs، برای شروع به تکمیل Web API، نیاز به این سرویسها را داریم:
namespace BlazorWasm.WebApi { public class Startup { //... public void ConfigureServices(IServiceCollection services) { services.AddAutoMapper(typeof(MappingProfile).Assembly); services.AddScoped<IHotelRoomService, HotelRoomService>(); services.AddScoped<IAmenityService, AmenityService>(); services.AddScoped<IHotelRoomImageService, HotelRoomImageService>(); var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); //...
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\BlazorServer\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" /> <ProjectReference Include="..\..\BlazorServer\BlazorServer.Services\BlazorServer.Services.csproj" /> <ProjectReference Include="..\..\BlazorServer\BlazorServer.Models.Mappings\BlazorServer.Models.Mappings.csproj" /> </ItemGroup> </Project>
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true" } }
تعریف کنترلر HotelRoom
در ادامه کدهای اولین کنترلر Web API را مشاهده میکنید که مرتبط است با بازگشت اطلاعات تمام اتاقهای ثبت شده و یا بازگشت اطلاعات یک اتاق ثبت شده:
using BlazorServer.Models; using BlazorServer.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading.Tasks; namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]")] public class HotelRoomController : ControllerBase { private readonly IHotelRoomService _hotelRoomService; public HotelRoomController(IHotelRoomService hotelRoomService) { _hotelRoomService = hotelRoomService; } [HttpGet] public IAsyncEnumerable<HotelRoomDTO> GetHotelRooms() { return _hotelRoomService.GetAllHotelRoomsAsync(); } [HttpGet("{roomId}")] public async Task<IActionResult> GetHotelRoom(int? roomId) { if (roomId == null) { return BadRequest(new ErrorModel { Title = "", ErrorMessage = "Invalid Room Id", StatusCode = StatusCodes.Status400BadRequest }); } var roomDetails = await _hotelRoomService.GetHotelRoomAsync(roomId.Value); if (roomDetails == null) { return BadRequest(new ErrorModel { Title = "", ErrorMessage = "Invalid Room Id", StatusCode = StatusCodes.Status404NotFound }); } return Ok(roomDetails); } } }
- ErrorModel آنرا در همان پروژهی قبلی مدلها، در فایل BlazorServer\BlazorServer.Models\ErrorModel.cs به صورت زیر ایجاد کردهایم:
namespace BlazorServer.Models { public class ErrorModel { public string Title { get; set; } public int StatusCode { get; set; } public string ErrorMessage { get; set; } } }
یکی از مزایای آن، امکان آزمایش API تهیه شده، بدون نیاز به تهیهی هیچ نوع کلاینت خاصی است. برای مثال اگر بر روی api/hotelroom آن کلیک کنیم، گزینهی «try it out» آن ظاهر شده و با کلیک بر روی آن، اینبار دکمهی execute ظاهر میشود. در ادامه با کلیک بر روی دکمهی اجرای آن، اکشن متد GetHotelRooms اجرا شده و خروجی زیر ظاهر میشود:
و یا اگر بخواهیم متد GetHotelRoom را توسط آن آزمایش کنیم، بر اساس پارامترهای آن، رابط کاربری زیر را تشکیل میدهد که امکان دریافت شمارهی اتاق را دارد:
انجام تنظیمات ابتدایی CORS و خروجی JSON برنامه
قرار است این API را از طریق پروژهی Blazor سمت کلاینت خود استفاده کنیم که آدرس آن، با آدرس API یکی نیست. به همین جهت نیاز است تنظیمات CORS را به صورت زیر اضافه کنیم:
namespace BlazorWasm.WebApi { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddCors(o => o.AddPolicy("HotelManagement", builder => { builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); })); services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; // To avoid `JsonSerializationException: Self referencing loop detected error` options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; }); // ... } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseCors("HotelManagement"); app.UseRouting(); app.UseAuthentication(); // ... } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-24.zip
افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard
در اینجا قصد داریم صفحهای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شدهی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شدهی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه میکنیم:
>ng g c Dashboard/CallProtectedApi
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component"; const routes: Routes = [ { path: "callProtectedApi", component: CallProtectedApiComponent, data: { permission: { permittedRoles: ["Admin", "User"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شدهی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active"> <a [routerLink]="['/callProtectedApi']">Call Protected Api</a> </li>
نمایش و یا مخفی کردن قسمتهای مختلف صفحه بر اساس نقشهای کاربر وارد شدهی به سیستم
در ادامه میخواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شدهی سمت سرور را بازگشت دهند. دکمهی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمهی دوم توسط کاربری با نقشهای Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر میدهیم:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-call-protected-api", templateUrl: "./call-protected-api.component.html", styleUrls: ["./call-protected-api.component.css"] }) export class CallProtectedApiComponent implements OnInit { isAdmin = false; isUser = false; result: any; constructor(private authService: AuthService) { } ngOnInit() { this.isAdmin = this.authService.isAuthUserInRole("Admin"); this.isUser = this.authService.isAuthUserInRole("User"); } callMyProtectedAdminApiController() { } callMyProtectedApiController() { } }
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()"> Call Protected Admin API [Authorize(Roles = "Admin")] </button> <button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()"> Call Protected API ([Authorize]) </button> <div *ngIf="result"> <pre>{{result | json}}</pre> </div>
دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال میکند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل میتوان در AuthService تولید نمود:
getBearerAuthHeader(): HttpHeaders { return new HttpHeaders({ "Content-Type": "application/json", "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}` }); }
روش دوم انجام اینکار که مرسومتر است، اضافه کردن خودکار این هدر به تمام درخواستهای ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل میکنیم:
import { Injectable } from "@angular/core"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http"; import { Observable } from "rxjs/Observable"; import { AuthService, AuthTokenType } from "./auth.service"; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); } return next.handle(request); } }
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام میشود:
import { HTTP_INTERCEPTORS } from "@angular/common/http"; import { AuthInterceptor } from "./services/auth.interceptor"; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class CoreModule {}
در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعهدهندههای مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors: Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
import { Injector } from "@angular/core"; constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService);
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService); const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); } return next.handle(request); } }
تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
اکنون پس از افزودن AuthInterceptor، میتوان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویسهای Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازندهی CallProtectedApiComponent تزریق میکنیم:
constructor( private authService: AuthService, private httpClient: HttpClient, @Inject(APP_CONFIG) private appConfig: IAppConfig, ) { }
callMyProtectedAdminApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); } callMyProtectedApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); }
در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:
مدیریت خودکار خطاهای عدم دسترسی ارسال شدهی از سمت سرور
ممکن است کاربری درخواستی را به منبع محافظت شدهای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده میتوان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحهی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
return next.handle(request) .catch((error: any, caught: Observable<HttpEvent<any>>) => { if (error.status === 401 || error.status === 403) { this.router.navigate(["/accessDenied"]); } return Observable.throw(error); });
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor( private injector: Injector, private router: Router) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService); const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); return next.handle(request) .catch((error: any, caught: Observable<HttpEvent<any>>) => { if (error.status === 401 || error.status === 403) { this.router.navigate(["/accessDenied"]); } return Observable.throw(error); }); } else { // login page return next.handle(request); } } }
using ASPNETCore2JwtAuthentication.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; namespace ASPNETCore2JwtAuthentication.WebApp.Controllers { [Route("api/[controller]")] [EnableCors("CorsPolicy")] [Authorize(Policy = CustomRoles.Editor)] public class MyProtectedEditorsApiController : Controller { public IActionResult Get() { return Ok(new { Id = 1, Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]", Username = this.User.Identity.Name }); } } }
callMyProtectedEditorsApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); }
نکتهی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور
اگر برنامهی سمت سرور ما که توکنها را اعتبارسنجی میکند، ریاستارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکنهای فعلی او برگشت خواهند خورد و از مرحلهی تعیین اعتبار رد نمیشوند. در این حالت کاربر خطای 401 را دریافت میکند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکیهای برنامههای ASP.NET Core هاست شدهی در IIS پس از ریاستارت آن» را فراموش نکنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
تیم 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); } } }
تایید حسابهای کاربری : چگونه کار میکند
بدون شک با تایید حسابهای کاربری توسط ایمیل آشنا هستید. حساب کاربری ای ایجاد میکنید و ایمیلی به آدرس شما ارسال میشود که حاوی لینک فعالسازی است. با کلیک کردن این لینک حساب کاربری شما تایید شده و میتوانید به سایت وارد شوید.
اگر به کنترلر 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); }
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); } }
پیاده سازی سرویس ایمیل با استفاده از 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
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);
نمایی که این متد باز میگرداند یعنی 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). بدین منظور میتوانید یکی از پستهای اخیر را مطالعه کنید.
ASP.NET is one of the most successful web application development frameworks by Microsoft. With every update, new and extended features are added that help developers deploy highly scalable and high-performance web applications.
When coupled with application monitoring and other performance tools, such as a profiler, ASP.NET becomes a powerful solution for building incredible apps.
Within the framework itself, there are myriad features to help you overcome common development challenges, do more with your apps, and boost performance.
public class IdentityUser : IUser { public IdentityUser(); public IdentityUser(string userName); public virtual ICollection<identityuserclaim> Claims { get; } public virtual string Id { get; set; } public virtual ICollection<identityuserlogin> Logins { get; } public virtual string PasswordHash { get; set; } public virtual ICollection<identityuserrole> Roles { get; } public virtual string SecurityStamp { get; set; } public virtual string UserName { get; set; } }
public class ApplicationUser : IdentityUser { public string Email { get; set; } }
public class RegisterViewModel { [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } [Required] [Display(Name = "Email address")] public string Email { get; set; } }
<div class="form-group"> @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) </div> </div>
public ActionResult About() { ViewBag.Message = "Your application description page."; UserManager<ApplicationUser> UserManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())); var user = UserManager.FindById(User.Identity.GetUserId()); if (user != null) ViewBag.Email = user.Email; else ViewBag.Email = "User not found."; return View(); }