پس از
تکمیل کنترل دسترسیها به قسمتهای مختلف برنامه بر اساس نقشهای انتسابی به کاربر وارد شدهی به سیستم، اکنون نوبت به کار با سرور و دریافت اطلاعات از کنترلرهای محافظت شدهی آن است.
افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard
در اینجا قصد داریم صفحهای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شدهی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شدهی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه میکنیم:
>ng g c Dashboard/CallProtectedApi
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه میکنیم:
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]
}
];
توضیحات AuthGuard و AuthGuardPermission را در
قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقشهای Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل 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() {
}
}
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفتهاند. مقدار دهی آنها نیز توسط متد isAuthUserInRole که در
قسمت قبل توسعه دادیم، انجام میشود. اکنون که این دو خاصیت مقدار دهی شدهاند، میتوان از آنها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمتهای مختلف صفحه استفاده کرد:
<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);
}
}
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام میشود:
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 {}
در اینجا نحوهی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده میکنید.
در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعهدهندههای مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors:
Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
در سازندهی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شدهاست که این سرویس نیز دارای HttpClient تزریق شدهی در سازندهی آن است. به همین جهت Angular تصور میکند که ممکن است در اینجا یک بازگشت بینهایت بین این interceptor و سرویس Auth رخدهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار میکنند، در اینجا فراخوانی نمیکنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را میتوان به صورت ذیل برطرف کرد:
import { Injector } from "@angular/core";
constructor(private injector: Injector) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authService = this.injector.get(AuthService);
ابتدا سرویس Injector را به سازندهی کلاس AuthInterceptor تزریق میکنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازندهی کلاس):
@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,
) { }
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
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);
});
در اینجا ابتدا نیاز است سرویس Router، به سازندهی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیتهای 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
@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);
}
}
}
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه میکنیم:
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
});
}
}
}
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
callMyProtectedEditorsApiController() {
this.httpClient
.get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`)
.map(response => response || {})
.catch((error: HttpErrorResponse) => Observable.throw(error))
.subscribe(result => {
this.result = result;
});
}
چون این نقش جدید به کاربر جاری انتساب داده نشدهاست (جزو اطلاعات سمت سرور او نیست)، اگر آنرا توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت میشود:
نکتهی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور
اگر برنامهی سمت سرور ما که توکنها را اعتبارسنجی میکند، ریاستارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکنهای فعلی او برگشت خواهند خورد و از مرحلهی تعیین اعتبار رد نمیشوند. در این حالت کاربر خطای 401 را دریافت میکند. بنابراین پیاده سازی مطلب «
غیرمعتبر شدن کوکیهای برنامههای ASP.NET Core هاست شدهی در IIS پس از ریاستارت آن» را فراموش نکنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.