- vừa được xem lúc

[Dio Flutter]: Tìm hiểu về Interceptor trong Dio và triển khai cơ chế Authentication.

0 0 26

Người đăng: Anh Quang

Theo Viblo Asia

Chào các bạn, có rất nhiều thư viện HTTP client mạnh mẽ cho Dart như: Http, Dio, Retrofit, Chopper... Hôm nay mình sẽ cùng nhau tìm hiểu về Interceptor trong package Dio nhé.

Chắc hẳn mọi người đã không còn xa lạ với khái niệm Interceptor trong lập trình, đặc biệt là khi làm việc với cơ chế Authentication. Cùng điểm lại một số khái niệm cơ bản nhé.

Khái niệm cơ bản.

Dio là ứng dụng khách HTTP mạnh mẽ dành cho Dart. Một số chức năng chính như sau:

  • Global Configuration
  • Interceptors
  • FormData
  • Request Cancellation
  • Retrying Requests
  • File Downloading
  • Timeout
  • Https certificate verification
  • Http2

Chúng ta có thể sử dụng cơ bản như sau:

import 'package:dio/dio.dart';
void getHttp() async { try { Response response = await Dio().get("http://www.google.com"); print(response); } catch (e) { print(e); }
}

Trong bài viết này chỉ tập trung vào Dio Interceptor. Chi tiết hơn về cách sử dụng, các bạn có thể tham khảo Document của Dio, Docs này viết khá là dễ hiểu: https://pub.dev/packages/dio

Authentication

Để một ứng dụng có thể hoạt động với cơ chế đăng ký, đăng nhập thì điều chúng ta cần quan tâm là Cơ chế Authentication (hay Xác thực người dùng).

Mỗi user khi đăng ký/đăng nhập thành công thì server sẽ trả về

  • access_token: Định danh user nào đăng nhập. Thông thường access_token sẽ tồn tại với thời gian khoảng 24h (hoặc ngắn hơn tùy nghiệp vụ của mỗi project). Thời gian này được gọi là Expired time.
  • refresh_token: Token dùng để lấy lại access_token mới khi access_token cũ hết hạn. Thời hạn tồn tại của refresh_token sẽ dài hơn access_token
  • expired time: thời gian tồn tại của access_token

Vậy, để duy trình đăng nhập thì cần phải có cơ chế refresh token hay Lấy lại access_token mới để duy trì đăng nhập (tiếp tục call những API có yêu cầu access_token). Mô hình như sau:

Ta có thể dễ dàng nhập thấy cả 2 mô hình gọi API đều thông qua 1 cơ chế Interceptor (bộ đánh chặn cả chiều đi và chiều về). Vậy Interceptor là gì?

Interceptor có thể hiểu như một bước tường lưới chặn các request, response của ứng dụng để cho phép kiểm tra, thêm vào header hoặc thay đổi các param của request, response. Nó cho phép chúng ta kiểm tra các token ứng dụng, Content-Type hoặc tự thêm các header vào request.

Các thành phần chính của Dio Interceptor:

  • onRequest(RequestOptions options): dùng để handle request trước khi gửi cho server.
  • onResponse(Response response): dùng để handle reponse trước khi gửi cho client.
  • onError(DioError error): handle error trước khi gửi cho client.
dio.interceptors.add(InterceptorsWrapper( onRequest:(options, handler){ // Do something before request is sent return handler.next(options); //continue // If you want to resolve the request with some custom data, // you can resolve a `Response` object eg: `handler.resolve(response)`. // If you want to reject the request with a error message, // you can reject a `DioError` object eg: `handler.reject(dioError)` }, onResponse:(response,handler) { // Do something with response data return handler.next(response); // continue // If you want to reject the request with a error message, // you can reject a `DioError` object eg: `handler.reject(dioError)` }, onError: (DioError e, handler) { // Do something with response error return handler.next(e);//continue // If you want to resolve the request with some custom data, // you can resolve a `Response` object eg: `handler.resolve(response)`. }
));

Cấu hình Interceptor cơ bản

// Khai báo var dio = Dio(); dio.interceptors .add(InterceptorsWrapper(onRequest: (options, handler) async { if (!options.path.contains('http')) { // Cấu hình đường path để call api, thành phần gồm // - Enviroment.api: Enpoint api theo môi trường, có thể dùng package dotenv // để cấu hình biến môi trường. Ví dụ: https://api-tech.com/v1 // - options.path: đường dẫn cụ thể API. Ví dụ: "user/user-info" options.path = Enviroment.apiUrl + options.path; } // Đoạn này dùng để config timeout api từ phía client, tránh việc call 1 API // bị lỗi trả response quá lâu. options.connectTimeout = 3000; options.receiveTimeout = 3000; // Gắn access_token vào header, gửi kèm access_token trong header mỗi khi call API options.headers['Authorization'] = "Bearer $accessToken"; }, onResponse: (Response response, handler) { // Do something with response data return handler.next(response); }, onError: (DioError error, handler) async { return handler.next(error); }));

Cơ chế refresh token

// Khai báo var dio = Dio(); dio.interceptors .add(InterceptorsWrapper(onRequest: (options, handler) async { final _prefs = await SharedPreferences.getInstance(); if (!options.path.contains('http')) { // Cấu hình đường path để call api, thành phần gồm // - Enviroment.api: Enpoint api theo môi trường, có thể dùng package dotenv // để cấu hình biến môi trường. Ví dụ: https://api-tech.com/v1 // - options.path: đường dẫn cụ thể API. Ví dụ: "user/user-info" options.path = Enviroment.apiUrl + options.path; } // Đoạn này dùng để config timeout api từ phía client, tránh việc call 1 API // bị lỗi trả response quá lâu. options.connectTimeout = 3000; options.receiveTimeout = 3000; // Lấy các token được lưu tạm từ local storage String? accessToken = _prefs.getString('accessToken'); String? expiredTime = _prefs.getString('expiredTime'); String? refreshToken = _prefs.getString('refreshToken'); // Kiểm tra xem user có đăng nhập hay chưa. Nếu chưa thì call handler.next(options) // để trả data về tiếp client if (accessToken == null || expiredTime == null || refreshToken == null) { return handler.next(options); } // Tính toán thời gian token expired final expiredTimeConvert = DateTime.parse(expiredTime); final isExpired = DateTime.now().isAfter(expiredTimeConvert); if (isExpired) { try { final response = await dio.post( 'https://api-tech.com/v1/auth/user-refresh-token', data: refreshToken, ); if (response.statusCode == 200) { //! EXPIRED SESSION if (response.data != false) { options.headers['Authorization'] = "Bearer ${response.data["accessToken"]}"; final expiredTime = DateTime.now() .add(Duration(seconds: response.data["expiresIn"] - 240)); await _prefs.setString( "accessToken", response.data["accessToken"]); await _prefs.setString("expiredTime", expiredTime.toString()); } else { // logout(); } } else { // logout(); } return handler.next(options); } on DioError catch (error) { // logout(); return handler.reject(error, true); } } else { // Gắn access_token vào header, gửi kèm access_token trong header mỗi khi call API options.headers['Authorization'] = "Bearer $accessToken"; return handler.next(options); } }, onResponse: (Response response, handler) { // Do something with response data return handler.next(response); }, onError: (DioError error, handler) async { // Ghi log những lỗi gửi về Sentry hoặc Firebase crashlytics SentryLogError().additionalData(error); if (error.response?.statusCode == 401) { // Đăng xuất khi hết session logout(); } return handler.next(error); }));

InterceptorsWrapper vs QueuedInterceptorsWrapper

Đoạn code trên sử dụng InterceptorsWrapper. Vậy thì InterceptorsWrapper với QueuedInterceptorsWrapper khác nhau những gì:

  • InterceptorsWrapper: có thể được thực hiện đồng thời, nghĩa là tất cả các yêu cầu nhập vào trình chặn cùng một lúc, thay vì thực hiện tuần tự.
  • QueuedInterceptorsWrapper: cung cấp cơ chế truy cập tuần tự (từng cái một) cho các thiết bị chặn.

Cụ thể trường hợp trên sẽ bị 1 vấn đề như sau: Nếu có 3 API được gọi cùng lúc khi khởi chạy app thì Interceptor sẽ run 3 API cùng lúc, khi 1 trong 3 API lấy được access_token mới thì 2 API còn lại vẫn dùng access_token → Bị fail 2 API đó.

→ Vì thế chúng ta thay InterceptorsWrapper bằng QueuedInterceptorsWrapper để 3 API vào Interceptor 1 cách tuần tự. Khi 1 trong 3 API lấy được access_token thì sẽ đính kèm access_token mới vào header → 2 API còn lại sẽ dùng access_token để call API lấy được data.

Khi nào thì refresh token

Thông thường ta có thể refresh token bằng 1 trong 2 cách sau:

  • Kiểm tra expired time để refresh token trước khi access_token hết hạn (như cách trên). Ưu điểm của cách này sẽ
  • Không tốn 1 lần call API bị fail với access_token cũ → Gọi lại api bị lỗi với access_token mới → Success.

    • Tăng tính bảo mật hơn vì người khác sẽ khó phân biệt được lỗi 401 từ api trả về là do user hết session hay bị expired access_token.
  • Khi access_token đã hết hạn, gọi API mới thì trả về 401. Lỗi này sẽ được báo ở hàm onError của Dio Interceptor.
onError: (DioError error, handler) async { final _prefs = await SharedPreferences.getInstance() String? refreshToken = _prefs.getString('refreshToken'); if ((error.response?.statusCode == 401 && error.response?.data['message'] == "Invalid JWT")) { if (refreshToken) { await refreshTokenFunc(); return handler.resolve(await _retry(error.requestOptions)); } } return handler.next(error);
}

Để gọi lại API bị lỗi access_token hết hạn

Future<void> refreshTokenFunc() async { final refreshToken = await _storage.read(key: 'refreshToken'); final response = await dio.post('/auth/refresh', data: {'refreshToken': refreshToken}); if (response.statusCode == 201) { accessToken = response.data; } else { accessToken = null; await preferences.clear(); } }

Tài liệu tham khảo

https://pub.dev/packages/dio#interceptors

Lời kết

Qua bài viết vừa rồi, chúng ta đã tìm hiểu các khái niệm về Interceptor trong Dio và triển khai một Cơ chế Xác thực người dùng cơ bản. Hẹn gặp các bạn ở bài viết tiếp theo nhé. Best regards.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Học Flutter từ cơ bản đến nâng cao. Phần 1: Làm quen cô nàng Flutter

Lời mở đầu. Gần đây, Flutter nổi lên và được Google PR như một xu thế của lập trình di động vậy.

0 0 281

- vừa được xem lúc

Học Flutter từ cơ bản đến nâng cao. Phần 3: Lột trần cô nàng Flutter, BuildContext là gì?

Lời mở đầu. Màn làm quen cô nàng FLutter ở Phần 1 đã gieo rắc vào đầu chúng ta quá nhiều điều bí ẩn về nàng Flutter.

0 0 215

- vừa được xem lúc

Flutter Animation: Creating medium’s clap animation in flutte Part II

Trong phần 1 mình đã giới thiệu với các bạn cơ bản về Animation trong Flutter. Score Widget Size Animation.

0 0 64

- vừa được xem lúc

Flutter - GetX - Using GetConnect to handle API request (Part 4)

Giới thiệu. Xin chào các bạn, lại là mình với series về GetX và Flutter.

0 0 359

- vừa được xem lúc

StatefulWidget và StatelessWidget trong Flutter

I. Mở đầu. Khi các bạn build một ứng dụng với Flutter thì Widgets là thứ không thể thiếu đúng không ạ. Và 2 loại Widget không thể thiếu đó là StatefullWidget và StatelessWidget.

0 0 145

- vừa được xem lúc

Tìm hiểu về Riverpod - Provider nhưng không hắn :v

Trong Flutter có rất nhiều các quản lý state: Provider, Bloc, GetX, Redux,... khó mà nói cái nào tốt hơn cái nào. Tuy nhiên nếu bạn đã làm quen với Provider thì không ngại để tìm hiểu thêm về Riverpod. Một bản nâng cấp của Provider. Nếu bạn để ý thì cái tên "Riverpod" là các chữ cái của "Provider" đ

0 0 67