احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: دوازده دقیقه

در قسمت قبل، ساختار ابتدایی کلاینت 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 را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
  • #
    ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۱ دی ۱۳۹۶، ساعت ۱۴:۵۳
    بنده طبق روش‌های فوق پیش رفتم. فقط بعد از لاگین، وقتی کاربر صفحه را دستی رفرش کند، هدر اصلا نمایش داده نمی‌شود؛ ولی درخواست‌ها سمت سرور Authenticate هستند و مقدار isLoggedIn  برابر false است. البته مقادیر Token ,Remember me در Local storage موجود  می‌باشند.
    ممنون میشوم راهنمایی بفرمایید .
    • #
      ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۱ دی ۱۳۹۶، ساعت ۱۵:۱۴
      - سازنده‌ی کلاس سرویس Auth هست که کار اطلاع رسانی به کامپوننت هدر را انجام می‌دهد. علت قرار گرفتن این قطعه کد، در هدر هم دقیقا اجرای آن با ریفرش صفحه است. بنابراین این قسمت و همچنین مشترکین به BehaviorSubject آن‌را بررسی کنید:
       constructor() {
          this.updateStatusOnPageRefresh();
        }
      - اگر با وجود مقادیر token در کش مرورگر، این مقادیر کار نمی‌کنند، نیاز به پیاده سازی «نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور» را دارید. ری استارت برنامه = تولید کلیدهای رمزنگاری جدید = غیرمعتبر شدن اطلاعات سمت کاربر و برگشت خوردن آن‌ها در سمت سرور
      • #
        ‫۶ سال و ۸ ماه قبل، دوشنبه ۱۱ دی ۱۳۹۶، ساعت ۱۵:۵۷
        مشکل از این بود که در متد لاگین RefreshToken ساخته نمی‌شد .
        برای رفع آن در متد isAuthUserLoggedIn، خط * به صورت زیر تغییر دادم.
        //const hasTokens = !this.isEmptyString(accessToken) && !this.isEmptyString(refreshToken); *
            const hasTokens = !this.isEmptyString(accessToken);
        و در سازنده کلاس و بعد از لاگین خط زیر رو کامنت کردم.
        this.scheduleRefreshToken();
        تشکر
  • #
    ‫۶ سال و ۸ ماه قبل، سه‌شنبه ۱۹ دی ۱۳۹۶، ساعت ۱۴:۰۷
    هنگام login کردن به پیغام زیر در console بر می‌خورم. مشکل از سمت سرور هست یا کد‌های Angular؟ البته سمت سرور در پروژه‌های دیگر Angular جواب می‌گیرم.
    POST http://localhost:5000/api/account/login 500 (Internal Server Error)
    Failed to load http://localhost:5000/api/account/login: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. The response had HTTP status code 500.
    • #
      ‫۶ سال و ۸ ماه قبل، سه‌شنبه ۱۹ دی ۱۳۹۶، ساعت ۱۴:۱۵
      ASP.NET Core در نگارش فعلی آن برای خطای 500 یا Internal server error (خطای سمت سرور که باید لاگ و بررسی شود)، حتی اگر CORS را تنظیم کرده باشید، هدرهای آن‌را ارسال نمی‌کند. به همین جهت مرورگر خطای عدم تنظیم این هدرها را نمایش می‌دهد (که مهم نیست و بی‌ربط به اصل موضوع خطای سمت سرور است). 
    • #
      ‫۶ سال و ۸ ماه قبل، سه‌شنبه ۱۹ دی ۱۳۹۶، ساعت ۱۷:۳۳
      برنامه جاری طوری تنظیم شده که اگر فایل dotnet_run.bat یا دستور «dotnet watch run» را اجرا کنید، تمام عملیات و خطاهای سمت سرور را در برنامه‌ی کنسول dotnet نمایش می‌دهد. فقط کافی است محتوای پنجره‌ی کنسول آن‌را بررسی کنید.
  • #
    ‫۶ سال و ۷ ماه قبل، شنبه ۱۴ بهمن ۱۳۹۶، ساعت ۱۷:۴۶
    با تغییر کلاس سرویس AppConfigService به شکل زیر :

    import { HttpClient } from "@angular/common/http";
    import { Injectable } from "@angular/core";
    
    @Injectable()
    export class AppConfigService {
    
        private config: IAppConfig;
    
        constructor(private http: HttpClient) { }
    
        loadClientConfig(): Promise<any> {
            return this.http.get<IAppConfig>("assets/client-config.json")
                .toPromise()
                .then(config => {
                    this.config = config;
                    console.log("Config", this.config);
                })
                .catch(err => {
                    return Promise.reject(err);
                });
        }
    
        get configuration(): IAppConfig {
            if (!this.config) {
                throw new Error("Attempted to access configuration property before configuration data was loaded.");
            }
            return this.config;
        }
    }
    
    export interface IAppConfig {
        apiEndpoint: string;
        loginPath: string;
        logoutPath: string;
        refreshTokenPath: string;
        accessTokenObjectKey: string;
        refreshTokenObjectKey: string;
        adminRoleName: string;
    }

    و تغییر ماژول CoreModule به شکل زیر :

    import { NgModule, Optional, SkipSelf, APP_INITIALIZER } from "@angular/core";
    import { CommonModule } from "@angular/common";
    import { RouterModule } from "@angular/router";
    import { HTTP_INTERCEPTORS } from "@angular/common/http";
    
    // import RxJs needed operators only once
    import "./services/rxjs-operators";
    
    import { HeaderComponent } from "./component/header/header.component";
    import { AuthGuard } from "./services/auth.guard";
    import { AuthInterceptor } from "./services/auth.interceptor";
    import { AuthService } from "./services/auth.service";
    import { AppConfigService } from "./services/app-config.service";
    import { BrowserStorageService } from "./services/browser-storage.service";
    
    
    @NgModule({
        imports: [CommonModule, RouterModule],
        exports: [
            // components that are used in app.component.ts will be listed here.
            HeaderComponent
        ],
        declarations: [
            // components that are used in app.component.ts will be listed here.
            HeaderComponent
        ],
        providers: [
            // global singleton services of the whole app will be listed here.
            BrowserStorageService,
            AppConfigService,
            AuthService,
            AuthGuard,
            {
                provide: HTTP_INTERCEPTORS,
                useClass: AuthInterceptor,
                multi: true
            },
            {
                provide: APP_INITIALIZER,
                useFactory: (config: AppConfigService) => () => config.loadClientConfig(),
                deps: [AppConfigService ],
                multi: true
            }
        ]
    })
    export class CoreModule {
        constructor( @Optional() @SkipSelf() core: CoreModule) {
            if (core) {
                throw new Error("CoreModule should be imported ONLY in AppModule.");
            }
        }
    }
    با خطای زیر مواجه شدم لطفا راهنمایی بفرمائید :

    Error: Provider parse errors:
    Cannot instantiate cyclic dependency! ApplicationRef ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1 
    • #
      ‫۶ سال و ۷ ماه قبل، شنبه ۱۴ بهمن ۱۳۹۶، ساعت ۱۸:۱۹
      یک نمونه از خطای cyclic dependency در مطلب  «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور» بحث شده‌است. ابتدا باید «private http: HttpClient» را از سازنده‌ی کلاس حذف کنید، سپس:
      - یک راه حل آن، تزریق «private injector: Injector» است و دریافت وهله‌ی سرویس به صورت (HttpClient)this.injector.get.
      - راه حل دوم، دریافت وابستگی مورد نیاز از طریق یک پارامتر (و نه توسط تزریق وابستگی‌ها در سازنده‌ی کلاس):
      loadClientConfig(httpClient: HttpClient): Promise<any> {
      و سپس دریافت آن به این صورت:
          {
            provide: APP_INITIALIZER,
            useFactory: (config: AppConfigService, httpClient: HttpClient) => () => config.loadClientConfig(httpClient),
            deps: [AppConfigService, HttpClient],
            multi: true
          },
      مثال‌های بیشتر در اینجا
      • #
        ‫۶ سال و ۳ ماه قبل، سه‌شنبه ۲۲ خرداد ۱۳۹۷، ساعت ۱۴:۴۰
        زمانیکه از راه حل اول استفاده میکنم:
          constructor(
            private injector: Injector,
            @Inject(APP_CONFIG) private appConfig: IAppConfig
          ) {}
        
          loadApiConfig(): Promise<any> {
            const http = this.injector.get<HttpClient>(HttpClient);
            const url = `${this.appConfig.apiEndpoint}/${
              this.appConfig.apiSettingsPath
            }`;
            return http
              .get<IApiConfig>(url)
              .toPromise()
              .then(config => {
                this.config = config;
                console.log('ApiConfig', this.config);
              })
              .catch(err => {
                console.error(
                  `Failed to loadApiConfig(). Make sure ${url} is accessible.`,
                  this.config
                );
                return Promise.reject(err);
              });
          }
        و در CoreModule :
         {
              provide: APP_INITIALIZER,
              useFactory: (config: ApiConfigService) => () => config.loadApiConfig(),
              deps: [ApiConfigService],
              multi: true
            }
        در لاین: 
        const http = this.injector.get<HttpClient>(HttpClient);
        با خطای زیر مواجه میشم:

        • #
          ‫۶ سال و ۳ ماه قبل، سه‌شنبه ۲۲ خرداد ۱۳۹۷، ساعت ۱۴:۵۹
          پروژه‌ای که ارسال شده دقیقا از همین روش استفاده می‌کند و مشکلی هم ندارد:


          No provider for HttpClient هم یعنی تنظیمات اولیه ماژول‌ها را کامل نکردید (در دو قسمت imports و exports).

    • #
      ‫۶ سال و ۷ ماه قبل، سه‌شنبه ۲۴ بهمن ۱۳۹۶، ساعت ۲۲:۵۶
      در این مورد برای تکمیل پروژه، جهت دریافت تنظیمات کلاینت از سمت سرور

      - ابتدا ApiSettingsController.cs  اضافه شد تا تنظیمات ApiSettings را به سمت کلاینت بازگشت دهد.
      - سپس
      api-config.service.ts جهت خواندن این تنظیمات تعریف و به ماژول Core اضافه شد تا در ابتدای اجرای برنامه‌ی کلاینت، پیش از هر کد دیگری اجرا شود.
      - تغییرات مورد نیاز آن‌را در اینجا می‌توانید مشاهده کنید و یا آخرین نگارش پروژه را دریافت کنید.

      برای آزمایش آن، اگر برنامه‌ی سرور (در ابتدا؛
      جهت مهیا شدن قسمت دریافت تنظیمات سمت سرور ) و سپس کلاینت را اجرا کنید، تنظیمات دریافتی را در کنسول توسعه دهندگان مرورگر، مشاهده خواهید کرد: