در مطلب آشنایی با Directiveها در AngularJS با نحوهی ایجاد Directive آشنا شدیم. هدف از این مطلب، آشنایی بیشتر با Directive در AngularJS است؛ یکی از بهترین فریم ورکهای جاوااسکریپتی، با قابلیت ایجاد کتابخانههایی از کامپوننتها که میتوانند به HTML اضافه شوند .
کتابخانههای جاوااسکریپتی زیادی وجود دارند. به عنوان مثال Bootstrap یکی از محبوبترین "front-end
framework" ها است که امکان تغییر در ظاهر المنتها را فراهم میکند و شامل
تعدادی کامپوننت جاوااسکریپتی نیز میباشد. مشکل کار، در هنگام استفاده از کامپوننت ها است. شخصی که در حال توسعهی HTML است
باید در کد جاوااسکریپتی خود از jQuery استفاده کند و بعنوان مثال یک Popover را فعال یا غیر فعال کند و این، یک فرآیند خسته کننده و مستعد خطا
است.
یک مثال ساده از Directives AngularJS و بررسی آن
var m = angular.module("myApp"); myApp.directive("myDir", function() { return { restrict: "E", scope: { name: "@", amount: "=", save: "&" }, template: "<div>" + " {{name}}: <input ng-model='amount' />" + " <button ng-click='save()'>Save</button>" + "</div>", replace: true, transclude: false, controller: [ "$scope", function ($scope) { … }], link: function (scope, element, attrs, controller) {…} } });
به الگوی نامگذاری directive دقت کنید. پیشوند my شبیه به یک namespace است. بنابراین اگر یک Application از دایرکتیوهای قرار گرفته در Module های متفاوت استفاده کند، به راحتی میتوان محل تعریف یک directive را تشخیص داد. این نام میتواند نشان دهندهی این باشد که این directive را خودتان توسعه دادهاید یا از یک directive توسعه داده شده توسط شخص دیگری در حال استفاده هستید. به هر حال این نحوهی نام گذاری یک اجبار نیست و به عنوان یک پیشنهاد است.
سازنده directive یک شیء را با تعدادی خاصیت باز میگرداند که تمامی آنها در سایت AngularJS توضیح داده شدهاند. در اینجا قصد داریم تا توضیحی مختصر در مورد کاری که این خصوصیات انجام میدهند داشته باشیم.
· restrict : تشخیص میدهد که آیا directive در HTML استفاده خواهد شد. گزینههای قابل استفاده ‘A’ ، ‘E’ ، ‘C’ برای attribute ، element ، class و یا comment است . پیش فرض ‘A’ برای attribute است. اما ما بیشتر علاقه به استفاده از ویژگی element برای ایجاد المنتهای UI داریم.
· scope : ایجاد یک scope ایزوله که متعلق به directive است و موجب ایزوله شدن آن از scope صدا زننده directive میشود. متغیرهای scope پدر از طریق خصوصیات تگ directive ارسال میشوند. این ایزوله کردن زمانی کاربردی است که در حال ایجاد کامپوننت هایی با قابلیت استفاده مجدد هستیم، که نباید متکی به scope پدر باشند. شیء scope در directive نام و نوع متغیرهای scope را تعیین میکنند. در مثال بالا سه متغیر برای scope تعریف شده است:
- name: "@" (by value, one-way) : علامت @ مشخص میکند که مقدار متغیر ارسال میشود. Directive یک رشته را دریافت میکند که شامل مقدار ارسال شده از scope پدر میباشد. Directive میتواند از آن استفاده کند، اما نمیتواند مقدار آن را در scope پدر تغییر دهد.
- amount: "=" (by reference, two-way) : علامت = مشخص میکند این متغیر با ارجاع ارسال میشود. Directive یک ارجاع به مقدار متغیر در scope اصلی دریافت میکند. مقدار میتواند هر نوع داده ای، شامل یک شیء complex یا یک آرایه باشد. Directive میتواند مقدار را در scope پدر تغییر دهد. این نوع متغیر، زمانیکه نیاز باشد directive مقدار را در scope پدر تغییر دهد، استفاده میشود.
- save: "&" (expression) : علامت & مشخص میکند این متغیر یک expression را که در scope پدر اجرا میشود، نگهداری میکند. اکنون directive قابلیت انجام کارهایی فراتر از تغییر یک مقدار را دارد. به عنوان مثال میتوان یک تابع را از scope پدر فراخوانی و نتیجهی اجرا را دریافت کرد.
· template : الگوی رشته ای که جایگزین المنت تعریف شده میشود. فرآیند جایگزینی تمامی خصوصیات را از المنت قدیمی به المنت جدید انتقال میدهد. به نحوه استفاده از متغیرهای تعریف شده در scope ایزوله دقت کنید. این مورد به شما امکان تعریف directive های macro-style را میدهد که نیاز به کد اضافهای، ندارند. اگرچه در بیشتر موارد الگو یک تگ ساده <div> است که از کدهای link که در زیر توضیح داده شده است استفاده میکند.
· replace : تعیین میکند که آیا الگوی directive باید جایگزین المنت شود. مقدار پیش فرض false است.
· transclude : تعیین کننده این است که محتوای directive باید در المنت کپی شود یا خیر. در مثال زیر المنت tab شامل المنتهای HTML دیگر است پس transclude برابر true است.
<body ng-app="components"> <h3>BootStrap Tab Component</h3> <tabs> <pane title="First Tab"> <div>This is the content of the first tab.</div> </pane> <pane title="Second Tab"> <div>This is the content of the second tab.</div> </pane> </tabs> </body>
· link : این تابع بیشتر منطق directive را شامل میشود. Link وظیفه دستکاری DOM ، ایجاد event listener ها و... را دارد. تابع Link پارامترهای زیر را دریافت میکند:
- scope : ارجاع به scope ایزوله شده directive دارد.
- element : ارجاع به المنتهای DOM که directive را تعریف کرده اند. تابع link معمولا برای دستکاری المنت از jQuery استفاده میکند. (یا از Angular's jqLite در صورتی که jQuery بارگذاری نشده باشد)
- controller : در مواقعی که از دایرکتیوهای تو در تو استفاده میشود کاربرد دارد. این پارامتر یک directive فرزند با ارجاعی به پدر را فراهم میکند، بنابراین موجب ارتباط directive ها میشود.
به عنوان مثال، این directive که پیاده سازی bootstrap tab را انجام داده است، میتوانید مشاهده نمایید.
موفق باشید
Microsoft.DotNet.Web.Spa.ProjectTemplates در آخرین نگارش آن، پشتیبانی از Angular CLI را هم افزودهاست. برای کار با آن و ایجاد یک پروژهی جدید بر مبنای آن دستورات ذیل را صادر کنید:
> dotnet new --install Microsoft.DotNet.Web.Spa.ProjectTemplates::2.0.0-preview1-final > dotnet new angular
توضیحات بیشتر:
About The Updated SPA Templates From ASP.NET Core
Migrating from the old ASP.NET Core Angular Spa template to the newer one
یک مثال: در ادامه یک پروژهی جدید مبتنی بر Angular CLI را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
> ng new angular-routing --routing
و تنظیمات مرتبط با آن به صورت خودکار به قسمت imports فایل app.module.ts اضافه میشوند و آمادهی استفاده هستند.
همچنین اگر به فایل src\app\app.component.html مراجعه کنیم، router-outlet نیز به آن افزوده شدهاست و مدیریت نمایش مسیریابیها در این قسمت انجام خواهد شد.
در ادامهی این مثال، دو کامپوننت جدید را به نامهای dashboard و customer ایجاد میکنیم:
>ng g c dashboard >ng g c customer
هدف این است که مسیریابیهایی را جهت کار و نمایش این کامپوننتها ایجاد کنیم. به همین جهت به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنید:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard/dashboard.component'; import { CustomerComponent } from './customer/customer.component'; const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', component: DashboardComponent }, { path: 'customer', component: CustomerComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
البته باید دقت داشت که چون پیشتر با اجرای دستورات ng g c، این کامپوننتها به صورت خودکار به تعاریف فایل app.module.ts اضافه شدهاند، امکان استفادهی از آنها در اینجا میسر است:
@NgModule({ declarations: [ AppComponent, DashboardComponent, CustomerComponent ],
پس از تعریف مسیریابیها، به فایل src\app\app.component.html مراجعه کرده و لینکهایی را به این مسیریابیهای جدید ایجاد میکنیم:
<h1> {{title}} </h1> <nav> <ul> <li><a href="" [routerLink]="['/dashboard']">Dashboard</a></li> <li><a href="" [routerLink]="['/customer']">Customer</a></li> </ul> </nav> <router-outlet></router-outlet>
اکنون اگر دستور کامپایل و گشودن برنامه را در مرورگر پیش فرض سیستم صادر کنیم:
> ng serve -o
با توجه به تنظیمات مسیریابی پیش فرض انجام شده، ابتدا مسیر http://localhost:4200/dashboard بارگذاری شدهاست.
ایجاد ماژولهای جدید به همراه تنظیمات مسیریابی آنها
در قسمت قبل با نحوهی ایجاد ماژولهای جدید توسط Angular CLI آشنا شدیم:
> ng g module admin
> ng g m admin --routing
سپس داخل این ماژول یک کامپوننت جدید را به نام admin ایجاد میکنیم:
> ng g c admin
برای مثال اگر نیاز به ایجاد کامپوننت دیگری به نام emails داخل این پوشه بود، باید به نحو ذیل عمل کرد:
> ng g c admin/email installing component create src\app\admin\email\email.component.css create src\app\admin\email\email.component.html create src\app\admin\email\email.component.spec.ts create src\app\admin\email\email.component.ts update src\app\admin\admin.module.ts
@NgModule({ imports: [ CommonModule, AdminRoutingModule ], declarations: [AdminComponent, EmailComponent] }) export class AdminModule { }
تا اینجا ماژول جدید admin را ایجاد کردهایم؛ اما برنامهی اصلی از آن اطلاعی ندارد. به همین جهت به فایل src\app\app.module.ts مراجعه کرده و این ماژول جدید را به آن معرفی میکنیم:
import { AdminModule } from './admin/admin.module'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, AdminModule, AppRoutingModule ],
در ادامه برای تعریف مسیریابیهای این ماژول جدید، به فایل src\app\admin\admin-routing.module.ts آن مراجعه کرده و ثابت routes آنرا مقدار دهی میکنیم:
import { AdminComponent } from './admin.component'; import { EmailComponent } from './email/email.component'; const routes: Routes = [ { path: 'admin', component: AdminComponent, children:[ { path:'', component:EmailComponent }, { path:'email', component:EmailComponent } ] } ];
سپس به فایل app\admin\admin.component.html نیز مراجعه کرده و router-outlet آنرا به آن اضافه میکنیم:
<p> admin works! </p> <router-outlet></router-outlet>
یک نکته: امکان تولید route guards نیز توسط Angular CLI برای محافظت از مسیریابی خاصی وجود دارد. برای این منظور میتوان دستور ذیل را صادر کرد:
>ng g guard auth
Windows 11 و امنیت مبتنی بر TPM آن
The Trusted Platform Module (TPM) is a chip that is either integrated into your PC’s motherboard or added separately into the CPU. Its purpose is to help protect encryption keys, user credentials, and other sensitive data behind a hardware barrier so that malware and attackers can’t access or tamper with that data.
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
انتشار ReSharper Ultimate 2018.3.2
ReSharper 2018.3.2 bug-fix update fixes:
- Many issues in C#, VB.NET, and TypeScript Code Analysis.
- More than a dozen issues related to parameter name hints.
- Missing “Find Code Dependent on Module” item in a context menu for a project node in the Solution Explorer tab.
- Several issues in Unit Testing.
- Some issues related to auto-detect naming style.
تعریف تزریق وابستگی تنظیمات برنامه
در مطلب «تزریق وابستگیها فراتر از کلاسها در برنامههای Angular» با روش تزریق ثوابت برنامه آشنا شدیم. در این مثال، برنامهی کلاینت بر روی پورت 4200 اجرا میشود و برنامهی سمت سرور وب، بر روی پورت 5000. به همین جهت نیاز است این آدرس پایه سمت سرور را در تمام قسمتهای برنامه که با سرور کار میکنند، در دسترس داشته باشیم و روش مناسب برای پیاده سازی آن همان قسمت «تزریق تنظیمات برنامه توسط تامین کنندهی مقادیر» مطلب یاد شدهاست. به همین جهت فایل جدید src\app\core\services\app.config.ts را در پوشهی core\services برنامه ایجاد میکنیم:
import { InjectionToken } from "@angular/core"; export let APP_CONFIG = new InjectionToken<string>("app.config"); export interface IAppConfig { apiEndpoint: string; loginPath: string; logoutPath: string; refreshTokenPath: string; accessTokenObjectKey: string; refreshTokenObjectKey: string; } export const AppConfig: IAppConfig = { apiEndpoint: "http://localhost:5000/api", loginPath: "account/login", logoutPath: "account/logout", refreshTokenPath: "account/RefreshToken", accessTokenObjectKey: "access_token", refreshTokenObjectKey: "refresh_token" };
سپس تنظیمات ابتدایی تزریق وابستگیهای IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام میدهیم:
import { AppConfig, APP_CONFIG } from "./app.config"; @NgModule({ providers: [ { provide: APP_CONFIG, useValue: AppConfig } ] }) export class CoreModule {}
طراحی سرویس Auth
پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شدهی به سیستم، در تمام قسمتهای برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهلهی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشهی Core\services و محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد میکنیم:
ng g s Core/services/Auth
create src/app/Core/services/auth.service.ts (110 bytes)
import { AuthService } from "./services/auth.service"; @NgModule({ providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService, AuthService, { provide: APP_CONFIG, useValue: AppConfig } ] }) export class CoreModule {}
در ادامه به تکمیل AuthService خواهیم پرداخت و قسمتهای مختلف آنرا مرور میکنیم.
اطلاع رسانی به کامپوننت Header در مورد وضعیت لاگین
در مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» با نحوهی کار با BehaviorSubject آشنا شدیم. در اینجا میخواهیم توسط آن، پس از لاگین موفق، وضعیت لاگین را به کامپوننت هدر صادر کنیم، تا لینک لاگین را مخفی کرده و لینک خروج از سیستم را نمایش دهد:
import { BehaviorSubject } from "rxjs/BehaviorSubject"; @Injectable() export class AuthService { private authStatusSource = new BehaviorSubject<boolean>(false); authStatus$ = this.authStatusSource.asObservable(); constructor() { this.updateStatusOnPageRefresh(); } private updateStatusOnPageRefresh(): void { this.authStatusSource.next(this.isLoggedIn()); }
در اینجا در سازندهی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی میشود. علت قرار دادن این متد در سازندهی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت بهخاطر سپاری کلمهی عبور، در اولین بار مشاهدهی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکنها و اعتبارسنجی آنها، گزارش وضعیت لاگین را ارائه میدهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی میشود.
در ادامه، متد next این BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.
تدارک ذخیره سازی توکنها در کش مرورگر
از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت میکنیم. به همین جهت یک enum را جهت مشخص سازی آنها تعریف خواهیم کرد:
export enum AuthTokenType { AccessToken, RefreshToken }
import { BrowserStorageService } from "./browser-storage.service"; export enum AuthTokenType { AccessToken, RefreshToken } @Injectable() export class AuthService { private rememberMeToken = "rememberMe_token"; constructor(private browserStorageService: BrowserStorageService) { } rememberMe(): boolean { return this.browserStorageService.getLocal(this.rememberMeToken) === true; } getRawAuthToken(tokenType: AuthTokenType): string { if (this.rememberMe()) { return this.browserStorageService.getLocal(AuthTokenType[tokenType]); } else { return this.browserStorageService.getSession(AuthTokenType[tokenType]); } } deleteAuthTokens() { if (this.rememberMe()) { this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.AccessToken]); this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.RefreshToken]); } else { this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.AccessToken]); this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.RefreshToken]); } this.browserStorageService.removeLocal(this.rememberMeToken); } private setLoginSession(response: any): void { this.setToken(AuthTokenType.AccessToken, response[this.appConfig.accessTokenObjectKey]); this.setToken(AuthTokenType.RefreshToken, response[this.appConfig.refreshTokenObjectKey]); } private setToken(tokenType: AuthTokenType, tokenValue: string): void { if (this.rememberMe()) { this.browserStorageService.setLocal(AuthTokenType[tokenType], tokenValue); } else { this.browserStorageService.setSession(AuthTokenType[tokenType], tokenValue); } } }
- متد rememberMe مشخص میکند که آیا وضعیت بهخاطر سپاری کلمهی عبور توسط کاربر انتخاب شدهاست یا خیر؟ این مقدار را نیز در local storage ماندگار ذخیره میکنیم تا در صورت بستن مرورگر و مراجعهی مجدد به آن، در دسترس باشد و به صورت خودکار پاک نشود.
- متد setToken، بر اساس وضعیت rememberMe، مقادیر توکنهای دریافتی از سرور را در local storage و یا session storage ذخیره میکند.
- متد getRawAuthToken بر اساس یکی از مقادیر enum ارسالی به آن، مقدار خام access_token و یا refresh_token ذخیره شده را بازگشت میدهد.
- متد deleteAuthTokens جهت حذف تمام توکنهای ذخیره شدهی توسط برنامه استفاده خواهد شد. نمونهی کاربرد آن در متد logout است.
- متد setLoginSession پس از لاگین موفق فراخوانی میشود. کار آن ذخیره سازی توکنهای دریافتی از سرور است. فرض آن نیز بر این است که خروجی json از طرف سرور، توکنها را با کلیدهایی دقیقا مساوی access_token و refresh_token بازگشت میدهد:
{"access_token":"...","refresh_token":"..."}
تکمیل متد ورود به سیستم
در صفحهی لاگین، کاربر نام کاربری، کلمهی عبور و همچنین گزینهی «بهخاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام Credentials در محل src\app\core\models\credentials.ts ایجاد میکنیم:
export interface Credentials { username: string; password: string; rememberMe: boolean; }
@Injectable() export class AuthService { constructor( @Inject(APP_CONFIG) private appConfig: IAppConfig, private http: HttpClient, private browserStorageService: BrowserStorageService ) { this.updateStatusOnPageRefresh(); } login(credentials: Credentials): Observable<boolean> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(`${this.appConfig.apiEndpoint}/${this.appConfig.loginPath}`, credentials, { headers: headers }) .map((response: any) => { this.browserStorageService.setLocal(this.rememberMeToken, credentials.rememberMe); if (!response) { this.authStatusSource.next(false); return false; } this.setLoginSession(response); this.authStatusSource.next(true); return true; }) .catch((error: HttpErrorResponse) => Observable.throw(error)); } }
در اینجا نیاز است اطلاعات شیء Credentials را به مسیر http://localhost:5000/api/account/login ارسال کنیم. به همین جهت نیاز به سرویس IAppConfig تزریق شدهی در سازندهی کلاس وجود دارد تا با دسترسی به this.appConfig.apiEndpoint، مسیر تنظیم شدهی در فایل src\app\core\services\app.config.ts را دریافت کنیم.
پس از لاگین موفق:
- ابتدا وضعیت rememberMe انتخاب شدهی توسط کاربر را در local storage مرورگر جهت مراجعات آتی ذخیره میکنیم.
- سپس متد setLoginSession، توکنهای دریافتی از شیء response را بر اساس وضعیت rememberMe در local storage ماندگار و یا session storage فرار، ذخیره میکند.
- در آخر با فراخوانی متد next مربوط به authStatusSource با پارامتر true، به تمام کامپوننتهای مشترک به این سرویس اعلام میکنیم که وضعیت لاگین موفق بودهاست و اکنون میتوانید نسبت به آن عکس العمل نشان دهید.
تکمیل متد خروج از سیستم
کار خروج، با فراخوانی متد logout صورت میگیرد:
@Injectable() export class AuthService { constructor( @Inject(APP_CONFIG) private appConfig: IAppConfig, private http: HttpClient, private router: Router ) { this.updateStatusOnPageRefresh(); } logout(navigateToHome: boolean): void { this.http .get(`${this.appConfig.apiEndpoint}/${this.appConfig.logoutPath}`) .finally(() => { this.deleteAuthTokens(); this.unscheduleRefreshToken(); this.authStatusSource.next(false); if (navigateToHome) { this.router.navigate(["/"]); } }) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { console.log("logout", result); }); } }
اعتبارسنجی وضعیت لاگین و توکنهای ذخیره شدهی در مرورگر
برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بستهی jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بستهی آن به پروژه اضافه شود:
> npm install jwt-decode --save
import * as jwt_decode from "jwt-decode";
getDecodedAccessToken(): any { return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken)); }
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
getDisplayName(): string { return this.getDecodedAccessToken().DisplayName; }
getAccessTokenExpirationDateUtc(): Date { const decoded = this.getDecodedAccessToken(); if (decoded.exp === undefined) { return null; } const date = new Date(0); // The 0 sets the date to the epoch date.setUTCSeconds(decoded.exp); return date; }
isAccessTokenTokenExpired(): boolean { const expirationDateUtc = this.getAccessTokenExpirationDateUtc(); if (!expirationDateUtc) { return true; } return !(expirationDateUtc.valueOf() > new Date().valueOf()); }
isLoggedIn(): boolean { const accessToken = this.getRawAuthToken(AuthTokenType.AccessToken); const refreshToken = this.getRawAuthToken(AuthTokenType.RefreshToken); const hasTokens = !this.isEmptyString(accessToken) && !this.isEmptyString(refreshToken); return hasTokens && !this.isAccessTokenTokenExpired(); } private isEmptyString(value: string): boolean { return !value || 0 === value.length; }
در قسمت بعد، از این سرویس اعتبارسنجی تکمیل شده جهت ورود به سیستم و تکمیل کامپوننت header استفاده خواهیم کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.