Pango AIPango AI Team
Module 01: Chatbot Service (API)
Module 01 • API Gateway

Cơ Chế Xác Thực & MD5 Checksum

Hệ thống Pango AI áp dụng cơ chế xác thực kép:

  1. Trước khi đăng nhập: Bắt buộc tính toán mã MD5 Checksum kèm Timestamp động để gọi mutation ExternalLogin.
  2. Sau khi đăng nhập: Sử dụng JWT Access Token đính kèm trong header Authorization: Bearer <token>.
  3. Cơ chế phục hồi tự động: Tự động bắt lỗi 401 Unauthorized để xin cấp lại token mới trong suốt với người dùng.

1. Công Thức Tạo MD5 Checksum Khi Gọi ExternalLogin

Trước khi gửi mutation ExternalLogin, Client cần tạo chuỗi mã hóa MD5 theo công thức sau:

checkSumString = `${platformCode}:${requestTime}:${platformUserId}`
Checksum = MD5(checkSumString).toLowerCase()

Bảng Giải Thích Tham Số

Tham SốKiểu Dữ LiệuVí DụMô Tả
platformCodestring"CARESOFT" hoặc "APP"Mã nền tảng đối tác tích hợp (thường viết chữ hoa hoặc thường đồng nhất)
requestTimenumber1740200000000Unix Timestamp tính bằng mili-giây (Date.now()) tại thời điểm gửi request
platformUserIdstring"737a445da12ddbd3d5d40a88ee314864"User ID duy nhất của người dùng trong hệ thống của bạn

Code Mẫu Sinh Checksum (TypeScript / JavaScript)

TypeScript / JavaScript:

generateChecksum.ts
import md5 from 'crypto-js/md5';

export interface ChecksumParams {
  platformCode: string;
  requestTime: number;
  platformUserId: string;
}

export function generateChecksum({ platformCode, requestTime, platformUserId }: ChecksumParams): string {
  // 1. Ghép chuỗi theo quy tắc: platformCode:requestTime:platformUserId
  const rawString = `${platformCode}:${requestTime}:${platformUserId}`;

  // 2. Mã hóa MD5 và đưa về chữ thường
  const checksum = md5(rawString).toString().toLowerCase();

  return checksum;
}

// Ví dụ thực tế:
// platformCode   = "CARESOFT"
// requestTime    = 1740200000000
// platformUserId = "737a445da12ddbd3d5d40a88ee314864"
// => rawString   = "CARESOFT:1740200000000:737a445da12ddbd3d5d40a88ee314864"
// => Checksum    = "38b4c27f91..."

Flutter / Dart:

generate_checksum.dart
import 'dart:convert';
import 'package:crypto/crypto.dart';

String generateChecksum({
  required String platformCode,
  required int requestTime,
  required String platformUserId,
}) {
  final rawString = '$platformCode:$requestTime:$platformUserId';
  final bytes = utf8.encode(rawString);
  final digest = md5.convert(bytes);
  return digest.toString().toLowerCase();
}

Thời Gian Lệch Đồng Hồ (Clock Drift)

Hệ thống Gateway chỉ chấp nhận requestTime chênh lệch tối đa 5 phút so với giờ chuẩn Server để ngăn chặn tấn công Replay Attack. Hãy chắc chắn thiết bị client đồng bộ thời gian mạng (NTP).


2. Cơ Chế Auto-Recovery Khi Token 401 (Tự Động Re-Login)

Khi phiên đăng nhập hết hạn hoặc bị hủy ở backend, GraphQL Gateway sẽ phản hồi mã lỗi 401 Unauthorized.

Ứng dụng Client nên cài đặt Network Interceptor để tự động xin lại token và gửi lại request ban đầu mà không làm gián đoạn trải nghiệm người dùng:

Sơ Đồ Trực Quan (Interactive Flowchart)
Đang tạo biểu đồ trực quan...

Code Triển Khai Interceptor Tự Động Phục Hồi

apiClientWithRecovery.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { externalLogin } from './externalLogin';

let isRefreshing = false;
let failedQueue: Array<{
  resolve: (value?: any) => void;
  reject: (reason?: any) => void;
}> = [];

const processQueue = (error: any, token: string | null = null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

export const apiClient = axios.create({
  baseURL: process.env.VITE_API_URL,
  headers: {
    'Content-Type': 'application/json',
    'orgId': process.env.VITE_ORG_ID,
    'x-org-id': process.env.VITE_ORG_ID,
    'x-client-type': 'app',
    'x-external-private-key': process.env.VITE_EXTERNAL_PRIVATE_KEY,
  },
});

// Request Interceptor: Luôn tự đính kèm token mới nhất
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = localStorage.getItem('access_token');
  if (token && !config.headers.Authorization) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response Interceptor: Bắt lỗi 401 và tự refresh
apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then((token) => {
            originalRequest.headers.Authorization = `Bearer ${token}`;
            return apiClient(originalRequest);
          })
          .catch((err) => Promise.reject(err));
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        // Lấy thông tin user đã lưu để re-login
        const currentUser = JSON.parse(localStorage.getItem('user_info') || '{}');
        const loginRes = await externalLogin(process.env.VITE_API_URL!, currentUser);
        const newToken = loginRes.accessToken;

        localStorage.setItem('access_token', newToken);
        apiClient.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
        originalRequest.headers.Authorization = `Bearer ${newToken}`;

        processQueue(null, newToken);
        return apiClient(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.removeItem('access_token');
        // Điều hướng ra màn hình đăng nhập nếu cần
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);