จัดการ Token Expiry ใน Angular

Oct. 26, 2024 · boychawin

ในบทความนี้ เราจะสร้าง AuthInterceptor ซึ่งจะดักจับทุกๆ request เพื่อเช็ค token ก่อนส่งไปยังเซิร์ฟเวอร์ หากพบว่า token หมดอายุ ระบบจะเรียกใช้ refresh token และอัปเดต access token ใหม่โดยอัตโนมัติ

ทำไมต้องใช้ AuthInterceptor?

การมี AuthInterceptor ช่วยให้คุณสามารถ

  • จัดการการหมดอายุของ Token: อัตโนมัติให้ผู้ใช้เข้าสู่ระบบใหม่โดยไม่ต้องทำให้การใช้งานหยุดชะงัก
  • เพิ่มความปลอดภัย: ปกป้องข้อมูลผู้ใช้โดยการใช้ Bearer token ในทุกๆ request
  • ประสบการณ์ผู้ใช้ที่ดีขึ้น: ผู้ใช้ไม่ต้องล็อกอินซ้ำเมื่อ token หมดอายุ

โครงสร้างโค้ด AuthInterceptor

  1. ตั้งค่า AuthInterceptor ในโมดูลหลัก

ใน Angular, เพื่อให้ AuthInterceptor ทำงานได้ คุณต้องทำการเพิ่มมันลงใน providers ของ AppModule ดังนี้

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppComponent } from './app.component';
import { AuthInterceptor } from '../shared/interceptor/auth.interceptor';

@NgModule({
  declarations: [
    AppComponent,
    // ส่วนประกาศคอมโพเนนต์ต่าง ๆ
  ],
  imports: [
    BrowserModule,
    HttpClientModule, // ต้องเพิ่ม HttpClientModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true,
    },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
  1. โค้ดของ AuthInterceptor

โค้ดด้านล่างนี้เป็นตัวอย่างของ AuthInterceptor ที่ดักจับ request และทำการเช็คว่า access token หมดอายุหรือไม่ หากหมดอายุจะทำการเรียก refresh token อัตโนมัติ

import { Injectable } from '@angular/core';
import {
    HttpErrorResponse,
    HttpEvent,
    HttpHandler,
    HttpInterceptor,
    HttpRequest,
} from '@angular/common/http';
import { CookieService } from 'ngx-cookie-service';
import { AuthService } from '../service/authen/auth.service';
import {
    BehaviorSubject,
    catchError,
    filter,
    finalize,
    first,
    Observable,
    switchMap,
    throwError,
} from 'rxjs';

interface IAuthRes {
    accessToken: string;
    refreshToken: string;
}

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    private isRefreshing = false;
    private refreshTokenSubject: BehaviorSubject<string> =
        new BehaviorSubject<string>('');

    constructor(
        private cookieService: CookieService,
        private authService: AuthService
    ) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): any {
        let modifiedRequest = this.setAuthHeader(
            request,
            this.cookieService.get('accessToken'),
        );
        if (request.url.includes('/api/auth/refreshToken')) {
            modifiedRequest = this.setAuthHeader(
                request,
                this.cookieService.get('refreshToken'),
            );
        }
        return next.handle(modifiedRequest).pipe(
            catchError((error: HttpErrorResponse) => {
                if (
                    error.status == 401 &&
                    !request.url.includes('/api/auth/refreshToken')
                ) {
                    return this.refreshAuth(request, next);
                } else if (error.status === 403) {
                    console.log("ไม่มีสิทธิ์เข้าใช้งาน");
                }
                return throwError(() => error);
            }),
        );
    }

    refreshAuth(
        request: HttpRequest<any>,
        next: HttpHandler,
    ): Observable<HttpEvent<any>> {
        if (!this.isRefreshing) {
            this.isRefreshing = true;
            this.refreshTokenSubject.next('');

            return this.authService
                .getRefreshToken(this.cookieService.get('refreshToken'))
                .pipe(
                    switchMap((res: IAuthRes) => {
                        this.cookieService.set('accessToken', res.accessToken, {
                            path: '/',
                            secure: true,
                            sameSite: 'Strict',
                        });
                        this.cookieService.set('refreshToken', res.refreshToken, {
                            path: '/',
                            secure: true,
                            sameSite: 'Strict',
                        });
                        this.refreshTokenSubject.next(res.accessToken);
                        return next.handle(this.setAuthHeader(request, res.accessToken));
                    }),
                    catchError((error) => {
                        console.log('TOKEN หมดอายุ กรุณา Login เพื่อเข้าสู่ระบบใหม่');
                        return throwError(() => error);
                    }),
                    finalize(() => (this.isRefreshing = false)),
                );
        }

        return this.refreshTokenSubject.pipe(
            filter((token) => Boolean(token)),
            first(),
            switchMap((token) => next.handle(this.setAuthHeader(request, token))),
        );
    }

    setAuthHeader(request: HttpRequest<any>, accessToken: string) {
        return request.clone({
            setHeaders: {
                'Authorization': `Bearer ${accessToken}`,
            },
        });
    }
}
  1. AuthService สำหรับการจัดการ refresh token

AuthService จะเรียก API เพื่อขอ access token ใหม่เมื่อ token หมดอายุ โดยส่ง refresh token ไปให้เซิร์ฟเวอร์ตรวจสอบ

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { RefreshToken } from './model/auth.model';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  constructor(private httpClient: HttpClient) {}

  getRefreshToken(token: string): Observable<RefreshToken> {
    const url = `http://localhost:4001/api/refreshToken`;
    return this.httpClient.post<RefreshToken>(url, {
      headers: {
        'Authorization': `Bearer ${token}`,
      },
    });
  }
}

สรุป

การใช้ AuthInterceptor ช่วยให้คุณสามารถจัดการการหมดอายุของ token เมื่อ access token หมดอายุ ระบบจะทำการเรียกใช้ refresh token อัตโนมัติ ซึ่งทำให้ผู้ใช้สามารถเข้าถึงข้อมูลหรือดำเนินการต่างๆ ได้โดยไม่ต้องล็อกอินซ้ำ ช่วยให้ระบบมีความปลอดภัย และมีประสบการณ์การใช้งานที่ราบรื่น