12/08/2018, 17:05

Thực hiện refresh token trong Angular (v2, v4.3, v5+)

Refresh token là một feature tuy nhỏ nhưng không thể thiếu trong ứng dụng client nói chung và single-page web app nói riêng. Trong bài viết này, mình sẽ hướng dẫn các bạn thực hiện tính năng này trong Angular. Vì từ bản 4.3 trở đi, Http module bị deprecated vì Angular cho ra đời HttpClient module, ...

Refresh token là một feature tuy nhỏ nhưng không thể thiếu trong ứng dụng client nói chung và single-page web app nói riêng. Trong bài viết này, mình sẽ hướng dẫn các bạn thực hiện tính năng này trong Angular. Vì từ bản 4.3 trở đi, Http module bị deprecated vì Angular cho ra đời HttpClient module, nên mình sẽ hướng dẫn các bạn các thực hiện với cả Http và HttpClient module.

Logic chung

Người dùng đăng nhập vào app, access token (AT) và refresh token (RT) được lưu vào localStorage. Khi thực hiện một request nhưng server báo AT hết hạn, bạn cần tìm một thời điểm trước khi http request hoàn tất ở phía người dùng, request một AT mới (dựa vào RT đã lưu trước đó). Khi đã request một AT mới thành công, bạn lưu lại AT (cũng như RT), và thực hiện lại request thất bại trước đó.

Cách thực hiện

Với version < 4.3: can thiệp vào Http class

Trước tiên, các bạn có thể xem qua Http class gốc:

export declare class Http {
    protected _backend: ConnectionBackend;
    protected _defaultOptions: RequestOptions;
    constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions);
    /**
     * Performs any type of http request. First argument is required, and can either be a url or
     * a {@link Request} instance. If the first argument is a url, an optional {@link RequestOptions}
     * object can be provided as the 2nd argument. The options object will be merged with the values
     * of {@link BaseRequestOptions} before performing the request.
     */
    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `get` http method.
     */
    get(url: string, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `post` http method.
     */
    post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `put` http method.
     */
    put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `delete` http method.
     */
    delete(url: string, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `patch` http method.
     */
    patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `head` http method.
     */
    head(url: string, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `options` http method.
     */
    options(url: string, options?: RequestOptionsArgs): Observable<Response>;
}

Việc cần làm cụ thể lúc này là can thiệp vào tất cả các request method (như get, post, put, ...), bắt lỗi token hết hạn trước khi lỗi hiện ra trên màn hình người dùng, và thực hiện lại request.

Trước tiên chúng ta tạo một class mới đặt tên là my-http.ts extends Http class như sau:

import { Injectable } from '@angular/core';
import { XHRBackend, RequestOptions, Http } from '@angular/http';

@Injectable()
export class MyHttp extends Http {
  constructor(backend: XHRBackend,
              defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }
}

Trong đoạn code trên:

  • MyHttp sẽ được sử dụng giống như Http, vì thế chúng ta cần decorator Injectable()

Tiếp theo, nhìn vào class Http gốc, chúng ta có thể dễ dàng thấy được 1 method rất nổi bật là request() với chú thích "Performs any type of http request...":

Đúng vậy, đây chính là method chúng ta sẽ can thiệp. Tiếp tục implement method request:

import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs } from '@angular/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class MyHttp extends Http {
  constructor(backend: XHRBackend,
              defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }
  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options);
  }
}

Tại thời điểm này, MyHttp đã hoạt động, bạn có thể khai báo việc sử dụng MyHttp thay cho Http mặc định trong AppModule:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    // ...
  ],
  providers: [
    { provide: Http, useClass: HttpService },
    // other service
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Chúng ta quay lại với MyHttp class, tiếp theo chúng ta sẽ bắt lỗi token hết hạn:

import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import 'rxjs/Rx';

@Injectable()
export class HttpService extends Http {
  constructor(backend: XHRBackend,
              defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch(this.catchErrors(url, options))
  }

  catchErrors(url, options) {
    return (err: Response) => {
      const errObj = err.json();
      switch (err.status) {
        case 401:
          return super
            .post(`https://example-api-endpoint/refreshtoken`, {
              refreshToken: localStorage.getItem('refresh_token')
            })
            .map(res => res.json)
            .flatMap(refreshTokenResponseResult => {
                // TODO: lưu lại AT và RT
                return this.request(url, options);
            });
        default:
          return Observable.throw(errObj);
      }
    };
  }
}

Trong method catchErrors() bên trên:

  • các bạn đón lỗi token hết hạn dựa vào http status code, thường là 401
  • kiểu trả về của request() method là một Observable, tuy nhiên trong method catch chúng ta phải thực hiện một HttpRequest cũng trả về 1 observable trước khi trả về observable request (hoặc observable error), do đó, chúng ta cần dùng method flatMap của RxJS để trộn 2 observable này.
  • ở đây mình chỉ tập trung vào quá trình refresh token, những action nên có khác mình gợi ý trong các dòng TODO.

Đến đây, ứng dụng của chúng ta đã có thể refresh token tự động, bạn có thể chờ token của bạn hết hạn để test.

Với version từ 4.3 đến trước 5: sử dụng HttpInterceptor trong bộ HttpClient

Từ version 4.3 trở lên, Http module bị khai tử và Angular khuyến nghị chúng ta chuyển sang HttpClient module. Trong HttpClient Angular cung cấp cho chúng ta một khái niệm rõ ràng hơn trong việc can thiệp vào http request, đó là Interceptor. (tham khảo thêm tại https://angular.io/guide/http#intercepting-requests-and-responses).

Để thực hiện, thay vì extends Http class như lúc trước, chúng ta sẽ implements HttpInterceptor interface, một interface được thiết kế riêng biệt cho việc can thiệp vào request/response. Trước tiên chúng ta tạo một class với tên MyRefreshTokenInterceptor - và tất nhiên - implements HttpInterceptor.

import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class MyRefreshTokenInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req);
  }
}

Class trên chưa thực hiện gì, mới chỉ chuyển tiếp request sang Interceptor tiếp theo (nếu có). Tại đây, bạn đã có thể sử dụng Interceptor này bằng cách khai báo trong app.module.ts:

// ...
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    // ...
  ],
  providers: [
      {
        provide: HTTP_INTERCEPTORS,
        useClass: MyRefreshTokenInterceptor,
        multi: true
      }
    // ...
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Lúc này Interceptor của bạn đã có thể can thiệp vào request response (mặc dù nó chả làm gì cả             </div>
            
            <div class=

0