در قسمت قبل، ساختار ابتدایی کلاینت Angular را تدارک دیدیم. در این قسمت قصد داریم سرویسی که زیر ساخت کامپوننت لاگین و عملیات ورود به سیستم را تامین میکند، تکمیل کنیم.
تعریف تزریق وابستگی تنظیمات برنامه
در مطلب «
تزریق وابستگیها فراتر از کلاسها در برنامههای 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"
};
در اینجا APP_CONFIG یک توکن منحصربفرد است که از آن جهت یافتن مقدار AppConfig که از نوع اینترفیس IAppConfig تعریف شدهاست، در سراسر برنامه استفاده خواهیم کرد.
سپس تنظیمات ابتدایی تزریق وابستگیهای IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام میدهیم:
import { AppConfig, APP_CONFIG } from "./app.config";
@NgModule({
providers: [
{ provide: APP_CONFIG, useValue: AppConfig }
]
})
export class CoreModule {}
اکنون هر سرویس و یا کامپوننتی در سراسر برنامه که نیاز به تنظیمات AppConfig را داشته باشد، کافی است با استفاده از ویژگی Inject(APP_CONFIG)@ آنرا درخواست کند.
طراحی سرویس Auth
پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شدهی به سیستم، در تمام قسمتهای برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهلهی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشهی Core\services و
محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد میکنیم:
ng g s Core/services/Auth
با این خروجی:
create src/app/Core/services/auth.service.ts (110 bytes)
و سپس تعریف آنرا به مدخل providers ماژول Core اضافه میکنیم:
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());
}
اکنون تمام کامپوننتهای برنامه میتوانند مشترک $authStatus شده و همواره آخرین وضعیت لاگین را دریافت کنند و نسبت به تغییرات آن عکس العمل نشان دهند (برای مثال قسمتی را نمایش دهند و یا قسمتی را مخفی کنند).
در اینجا در سازندهی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی میشود. علت قرار دادن این متد در سازندهی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت بهخاطر سپاری کلمهی عبور، در اولین بار مشاهدهی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکنها و اعتبارسنجی آنها، گزارش وضعیت لاگین را ارائه میدهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی میشود.
در ادامه، متد next این BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.
تدارک ذخیره سازی توکنها در کش مرورگر
از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت میکنیم. به همین جهت یک enum را جهت مشخص سازی آنها تعریف خواهیم کرد:
export enum AuthTokenType {
AccessToken,
RefreshToken
}
سپس باید این توکنها را پس از لاگین موفق در کش مرورگر ذخیره کنیم که با مقدمات آن در مطلب «
ذخیره سازی اطلاعات در مرورگر توسط برنامههای Angular» پیشتر آشنا شدیم. از همان سرویس BrowserStorageService مطلب یاد شده، در اینجا نیز استفاده خواهیم کرد:
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);
}
}
}
ابتدا سرویس BrowserStorageService به سازندهی کلاس تزریق شدهاست و سپس نیاز است بر اساس گزینهی «بهخاطر سپاری کلمهی عبور»، نسبت به انتخاب محل ذخیره سازی توکنها اقدام کنیم. اگر گزینهی rememberMe توسط کاربر در حین لاگین انتخاب شود، از local storage ماندگار و اگر خیر، از session storage فرار مرورگر برای ذخیره سازی توکنها و سایر اطلاعات مرتبط استفاده خواهیم کرد.
- متد 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":"..."}
اگر این کلیدها در برنامهی شما نام دیگری را دارند، محل تغییر آنها در فایل app.config.ts است.
تکمیل متد ورود به سیستم
در صفحهی لاگین، کاربر نام کاربری، کلمهی عبور و همچنین گزینهی «بهخاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام 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));
}
}
متد login یک Observable از نوع boolean را بازگشت میدهد. به این ترتیب میتوان مشترک آن شد و در صورت دریافت true یا اعلام لاگین موفق، کاربر را به صفحهای مشخص هدایت کرد.
در اینجا نیاز است اطلاعات شیء 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);
});
}
}
در اینجا در ابتدا متد logout سمت سرور که در مسیر http://localhost:5000/api/account/logout قرار دارد فراخوانی میشود. پس از آن در پایان کار در متد finally (چه عملیات فراخوانی logout سمت سرور موفق باشد یا خیر)، ابتدا توسط متد deleteAuthTokens تمام توکنها و اطلاعات ذخیره شدهی در مرورگر حذف میشوند. در ادامه با فراخوانی متد next مربوط به authStatusSource با مقدار false، به تمام مشترکین سرویس جاری اعلام میکنیم که اکنون وقت عکس العمل نشان دادن به خروجی سیستم و به روز رسانی رابط کاربری است. همچنین اگر پارامتر navigateToHome نیز مقدار دهی شده بود، کاربر را به صفحهی اصلی برنامه هدایت میکنیم.
اعتبارسنجی وضعیت لاگین و توکنهای ذخیره شدهی در مرورگر
برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بستهی
jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بستهی آن به پروژه اضافه شود:
> npm install jwt-decode --save
در ادامه برای استفادهی از آن، ابتدا بستهی آنرا import میکنیم:
import * as jwt_decode from "jwt-decode";
و سپس توسط متد jwt_decode آن میتوان به اصل اطلاعات توکن دریافتی از طرف سرور، دسترسی یافت:
getDecodedAccessToken(): any {
return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken));
}
این توکن خام، پس از decode، یک چنین فرمت نمونهای را دارد که در آن، شمارهی کاربری (nameidentifier)، نام کاربری (name)، نام نمایشی کاربر (DisplayName)، نقشهای او (قسمت role) و اطلاعات تاریخ انقضای توکن (خاصیت exp)، مشخص هستند:
{
"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"
}
برای مثال اگر خواستیم به خاصیت DisplayName این شیء decode شده دسترسی پیدا کنیم، میتوان به صورت ذیل عمل کرد:
getDisplayName(): string {
return this.getDecodedAccessToken().DisplayName;
}
و یا خاصیت exp آن، بیانگر تاریخ انقضای توکن است. برای تبدیل آن به نوع Date، ابتدا باید به این خاصیت در توکن decode شده دسترسی یافت و سپس توسط متد setUTCSeconds آنرا تبدیل به نوع Date کرد:
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 که وضعیت لاگین بودن کاربر جاری را مشخص میکند، به صورت ذیل تعریف میشود:
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;
}
ابتدا بررسی میکنیم که آیا توکنهای درخواست شدهی از کش مرورگر، وجود خارجی دارند یا خیر؟ پس از آن تاریخ انقضای access token را نیز بررسی میکنیم. تا همین اندازه جهت تعیین اعتبار این توکنها در سمت کاربر کفایت میکنند. در سمت سرور نیز این توکنها به صورت خودکار توسط برنامه تعیین اعتبار شده و امضای دیجیتال آنها بررسی میشوند.
در قسمت بعد، از این سرویس اعتبارسنجی تکمیل شده جهت ورود به سیستم و تکمیل کامپوننت header استفاده خواهیم کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.