Angular管理使用者登入狀態

使用 Angular interceptor, guard 和 service 。

Gruard 是在 Angular 4.3或以上引入的權限和安全檢查的解決方案。

在 Routing 上加入 Guard 令路由的 Lifecycle (生命週期) 加入 GuardsCheckStart, GuardsCheckStart 等額外階段。

  1. auth.service.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    export class AuthService {
    constructor(
    private router: Router,
    ) {
    // 追蹤 Gruard 並保存初始化 URL, 取得 URL 後取消訂閱以避免內存洩漏
    this.events = this.router.events.subscribe(event => {
    if (event instanceof GuardsCheckEnd && event.shouldActivate === false) {
    this.initRoute.next(event.url);
    this.events.unsubscribe();
    }
    });
    }

    events: Subscription;
    // 使用 BehaviorSubject 創建 Observable
    isLoginSubject = new BehaviorSubject<boolean>(false);
    userInfo = {} as User;
    initRoute = new BehaviorSubject<string>('');

    login(info: LoginInfo): void {
    this.cfpay.login(info).subscribe(next => {
    this.userInfo = next.data;
    this.isLoginSubject.next(true);
    this.router.navigate(['']).then();
    });
    }

    logout(): void {
    this.isLoginSubject.next(false);
    this.cfpay.logout().subscribe();
    this.router.navigate(['/login']).then();
    delete this.userInfo;
    }

    // 刷新登陸狀態
    refresh(): void {
    this.cfpay.refresh().subscribe(value => {
    this.userInfo = value.data;
    this.isLoginSubject.next(true);
    }, () => {
    this.router.navigate(['/login']).then();
    });
    }

    isLoggedIn(): Observable<boolean> {
    return this.isLoginSubject.asObservable();
    }
    }
  1. app.guard.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    export class AppGuard implements CanActivate {
    constructor(public auth: AuthService, private router: Router) {
    }
    // 以 Angular Guard 進行驗證
    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    // 因為每次驗證都是單一行為,所以不需要訂閱 Observable,只需要取最後的值
    if (this.auth.isLoginSubject.value) {
    return true;
    } else {
    this.router.navigate(['/login']).then();
    return false;
    }
    }
    }
  1. app-routing.module.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const routes: Routes = [
    {path: 'login', component: LoginComponent},
    // 在需要驗証 Route 的 canActivate 上加入己創建的Guard
    {path: '', component: LayoutComponent, canActivate: [AppGuard]},
    {path: '**', component: NotfoundComponent},
    ];

    @NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
    })
    export class AppRoutingModule {
    }
  1. AppComponent.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    export class AppComponent implements OnInit {
    constructor(
    private router: Router
    private auth: AuthService,
    ) {
    }
    // 在主組件上初始化和重新取得登陸資料
    ngOnInit(): void {
    this.auth.refresh();
    }
    }
  1. login.component.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export class LoginComponent implements OnInit {
    constructor(private auth: AuthService,
    private router: Router) {
    }

    loginInfo: LoginInfo = {username: '', password: ''};
    // 如果沒有登陸資料取消訂閱 Router event 以避免內存消耗
    ngOnInit() {
    this.auth.events.unsubscribe();
    // 在取得登陸資料後,跳轉到之前保存的地址上
    this.auth.isLoggedIn().subscribe(sub => {
    if (sub) {
    this.router.navigate([this.auth.initRoute.value]).then();
    }
    });
    }

    login() {
    this.auth.login(this.loginInfo);
    }
    }

  1. auth.interceptor.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    export class AuthInterceptor implements HttpInterceptor {

    constructor(private router: Router,
    private auth: AuthService) {
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Httpclient 後處理攔截器,在攔截器到 401/Unauthorized 跳回登陸頁面
    return next.handle(request).pipe(tap((_: HttpEvent<any>) => {
    }, (err: any) => {
    if (err instanceof HttpErrorResponse && err.status === 401) {
    this.auth.isLoginSubject.next(false);
    this.router.navigate(['/login']).then();
    }
    }));
    }
    }