上次我們已經成功連上了後端,但是只要使用者ㄧ重新刷瀏覽器,又會回到未登入狀態,今天我們就來讓前端記住登入狀態。
在前端要記住一些資料,我們會用到 cookie ,local storage或者 session storage,至於它們的不同點,可以參考 stackoverflow這篇文章,我們先來做一個有關 local storage 的服務 (service)
第一步:造一個 src/app/services 目錄
mkdir servcies
第二步: 用 angular-cli 產生服務並自動註冊到 app.module.ts
ng generate service utils --module app 
第三步:加入 jsonwebtoken 來判斷 token 是否逾期
npm install @auth0/angular-jwt --save
記得使用這個套件,其它套件不能夠跟新的 HttpClientModule 相容
第四步:修改 app.module.ts,導入 JwtModule
// ... 省略
import { HttpClientModule } from '@angular/common/http';
import { JwtModule } from '@auth0/angular-jwt';
//... 省略
@NgModule({
    //... 
    imports: [
    //...
        HttpClientModule,
        JwtModule.forRoot({
            config: {
                tokenGetter: () => {
                    return localStorage.getItem('access_token');
                },
                whitelistedDomains: ['localhost:3000']
            }
        })
    ],
//...
記得同時導入 HttpClientModule 跟 JwtModule
第五步:修改 utils.service.ts
import { Injectable } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt';
export const TOKEN = 'access_token';
@Injectable()
export class UtilsService {
    constructor(
        private jwtHelper: JwtHelperService
    ) { }
    isTokenExpired(token: string = TOKEN): boolean {
        let jwtStr = this.getToken(token);
        if (jwtStr) {
            return this.jwtHelper.isTokenExpired(jwtStr);  // token expired?
        } else {
            return true;        // no token 
        }
    }
    writeToken(value: string, token: string = TOKEN) {
        localStorage.setItem(token, value);
    }
    getToken(token: string = TOKEN) {
        return localStorage.getItem(token);
    }
    removeToken(token: string = TOKEN) {
        if (this.getToken(token)) {
            localStorage.removeItem(token);
        }
    }
}
預設的 token 的關鍵(key) 為 access_token,為了可以寫入不同的關鍵,我們參數保留了 token 這個關鍵。
第六步: 將 index.ts 加入 src/app/services
export * from './utils.servcie';
第一步:導入公用服務
// ... 省略
import { UtilsService } from '../../services';
@Injectable()
export class UserService {
    loginStatus = new BehaviorSubject<boolean>(false);
    currentUser = new BehaviorSubject<User>(null);
    constructor(
        private http: HttpClient,
        private appConfig: AppConfig,
        private utils: UtilsService
    ) { }
// ... 省略
第二步:修改 login() 程式,後端驗證成功會傳回關鍵值 (value),如果使用者有勾選 記得我,我們用公用服務將它寫入 localstorage
// ... 省略
login(loginData): Observable<boolean> {
        return this.loginServer(loginData)
            .map((res: Response) => {
                if (res.success) {
                    this.loginStatus.next(true);
                    this.currentUser.next(loginData.username);
                    if (loginData.rememberMe) {
                        this.utils.writeToken(res.payload);
                    }
                    return true;
                } else {
                    return false;
                }
            },
//... 省略
第三步:修改 logout(),登出時將 token 清掉
   logout() {
        this.loginStatus.next(false);
        this.currentUser.next(null);
        this.utils.removeToken();
    }
第四步:加入 checkuser() 函數
// when startup
    checkUser(): Observable<boolean> {
        if (!this.utils.isTokenExpired()) {
            this.loginStatus.next(true);
            return of(true);
        } else {
            console.log('no token or token is expired');
            this.utils.removeToken();
            return of(false);
        }
    }
這個函數馬上會用到,基本上是先檢查 token 有沒有過期,沒有的話將 loginStatus 設為真
第五步:確認
做登入後檢查是否 localstorage 已經寫入,正常狀態如截圖
接下來我們要在瀏覽器重新啟動時來檢查這個 token,我們先來做一個服務
第一步:產生 startup.servcie.ts 在 src/app/services 下
ng generate servcie startup --module app
第二步:修改程式
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { UtilsService } from '.';
import { UserService } from '../user/service/user.service';
@Injectable()
export class StartupService {
    constructor(
        private injector: Injector,
        private utils: UtilsService,
        private userService: UserService
    ) { }
    load(): Promise<any> {
        return new Promise((resolve, reject) => {
            return this.userService.checkUser()
                .subscribe(res => {
                    if (res) {
                        setInterval(() => {
                            this.checkStatus();
                        }, 1000 * 60 * 5)    // check current status every 5 min
                    }
                    resolve(res);
                }, err => {
                    console.log(err);
                    reject(err);
                });
        });
    }
    checkStatus() {
        if (this.utils.isTokenExpired()) {   // if token expired
            this.userService.logout();
            const router = this.injector.get(Router);
            router.navigate(['/']);
            console.log('logout due to token expired');
        }
    }
}
Promise 函數回給系統,這裡我們用 load()
load() 函數中利用使用者服務的 checkuser() 函數,如果 token 還沒過期,使用者服務的 loginStatus 會變成 truecheckStatus(),如果逾期,做一個 logout() 的動作然後將頁面導回首頁Injector 來導入Router
第三步: 修改app.module.ts
//... 省略
import { NgModule, APP_INITIALIZER, Injector } from '@angular/core';
//... 省略
export function startupServiceFactory(startupService: StartupService): Function { return () => startupService.load(); }
// ... 省略
@NgModule({
    // ...
  providers: [
        UtilsService,
        StartupService,
        {
            provide: APP_INITIALIZER,
            useFactory: startupServiceFactory,
            deps: [StartupService, Injector],
            multi: true
        }
    ],
//... 省略
好了,現在當您登入系統,回到首頁,重新刷瀏覽器,應該會留在登入狀態(瀏覽列底色),但是看不到使用者名稱,也沒辦法登出,我們下次來解決這個問題。