203 lines
6.3 KiB
TypeScript
203 lines
6.3 KiB
TypeScript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
|
import { IEncryptor, ISigner, CryptoConfig, EncryptedRequest, EncryptedResponse } from '../crypto/interface';
|
|
import { uint8ArrayToBase64, base64ToUint8Array, stringToUint8Array, uint8ArrayToString } from '../utils/base64';
|
|
import { generateUUID } from '../utils/uuid';
|
|
|
|
/**
|
|
* 加密Axios实例
|
|
*/
|
|
export class EncryptedAxios {
|
|
private axiosInstance: AxiosInstance;
|
|
private encryptor: IEncryptor | null = null;
|
|
private signer: ISigner | null = null;
|
|
private config: CryptoConfig;
|
|
|
|
constructor(
|
|
encryptor?: IEncryptor,
|
|
signer?: ISigner,
|
|
config: CryptoConfig = {},
|
|
axiosConfig?: AxiosRequestConfig
|
|
) {
|
|
this.encryptor = encryptor || null;
|
|
this.signer = signer || null;
|
|
this.config = {
|
|
timestampWindow: 5 * 60 * 1000, // 默认5分钟
|
|
enableTimestamp: true,
|
|
enableSignature: true,
|
|
...config,
|
|
};
|
|
|
|
// 创建axios实例
|
|
this.axiosInstance = axios.create(axiosConfig);
|
|
|
|
// 添加请求拦截器
|
|
this.axiosInstance.interceptors.request.use(
|
|
this.encryptRequestInterceptor.bind(this),
|
|
(error) => Promise.reject(error)
|
|
);
|
|
|
|
// 添加响应拦截器
|
|
this.axiosInstance.interceptors.response.use(
|
|
this.decryptResponseInterceptor.bind(this),
|
|
(error) => Promise.reject(error)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 请求拦截器 - 加密请求数据
|
|
*/
|
|
private async encryptRequestInterceptor(
|
|
config: InternalAxiosRequestConfig
|
|
): Promise<InternalAxiosRequestConfig> {
|
|
// 放行GET和OPTIONS请求
|
|
if (config.method?.toUpperCase() === 'GET' || config.method?.toUpperCase() === 'OPTIONS') {
|
|
return config;
|
|
}
|
|
|
|
// 如果没有配置加密器,直接返回
|
|
if (!this.encryptor) {
|
|
return config;
|
|
}
|
|
|
|
try {
|
|
// 将请求数据转换为JSON字符串
|
|
const plaintext = JSON.stringify(config.data || {});
|
|
const plaintextBytes = stringToUint8Array(plaintext);
|
|
|
|
// 加密数据
|
|
const ciphertext = await this.encryptor.encrypt(plaintextBytes);
|
|
const encryptedData = uint8ArrayToBase64(ciphertext);
|
|
|
|
// 构建加密请求体
|
|
const encryptedRequest: EncryptedRequest = {
|
|
data: encryptedData,
|
|
timestamp: Date.now(),
|
|
request_id: generateUUID(),
|
|
algorithm: this.encryptor.name(),
|
|
};
|
|
|
|
// 生成签名
|
|
if (this.config.enableSignature && this.signer) {
|
|
const signature = await this.signer.sign(plaintextBytes);
|
|
encryptedRequest.signature = uint8ArrayToBase64(signature);
|
|
}
|
|
|
|
// 替换请求数据
|
|
config.data = encryptedRequest;
|
|
config.headers['Content-Type'] = 'application/json';
|
|
|
|
// 保存request_id供响应使用
|
|
config.headers['X-Request-ID'] = encryptedRequest.request_id;
|
|
|
|
} catch (error) {
|
|
console.error('加密请求失败:', error);
|
|
throw error;
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* 响应拦截器 - 解密响应数据
|
|
*/
|
|
private async decryptResponseInterceptor(
|
|
response: AxiosResponse
|
|
): Promise<AxiosResponse> {
|
|
// 如果没有配置加密器或响应不是加密格式,直接返回
|
|
if (!this.encryptor || !response.data || typeof response.data !== 'object') {
|
|
return response;
|
|
}
|
|
|
|
// 检查是否是加密响应
|
|
const encryptedResponse = response.data as EncryptedResponse;
|
|
if (!encryptedResponse.data || !encryptedResponse.request_id) {
|
|
// 不是加密响应,直接返回
|
|
return response;
|
|
}
|
|
|
|
try {
|
|
// 验证时间戳
|
|
if (this.config.enableTimestamp) {
|
|
this.verifyTimestamp(encryptedResponse.timestamp);
|
|
}
|
|
|
|
// 解密数据
|
|
const ciphertext = base64ToUint8Array(encryptedResponse.data);
|
|
const plaintext = await this.encryptor.decrypt(ciphertext);
|
|
|
|
// 验证签名
|
|
if (this.config.enableSignature && this.signer && encryptedResponse.signature) {
|
|
const signature = base64ToUint8Array(encryptedResponse.signature);
|
|
const isValid = await this.signer.verify(plaintext, signature);
|
|
if (!isValid) {
|
|
throw new Error('签名验证失败');
|
|
}
|
|
}
|
|
|
|
// 将解密后的数据转换为JSON对象
|
|
const decryptedData = uint8ArrayToString(plaintext);
|
|
response.data = JSON.parse(decryptedData);
|
|
|
|
} catch (error) {
|
|
console.error('解密响应失败:', error);
|
|
throw error;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* 验证时间戳
|
|
*/
|
|
private verifyTimestamp(timestamp: number): void {
|
|
const now = Date.now();
|
|
const diff = Math.abs(now - timestamp);
|
|
|
|
if (diff > (this.config.timestampWindow || 5 * 60 * 1000)) {
|
|
throw new Error('请求超时');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取axios实例
|
|
*/
|
|
getInstance(): AxiosInstance {
|
|
return this.axiosInstance;
|
|
}
|
|
|
|
/**
|
|
* GET 请求
|
|
*/
|
|
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.get<T>(url, config);
|
|
}
|
|
|
|
/**
|
|
* POST 请求
|
|
*/
|
|
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.post<T>(url, data, config);
|
|
}
|
|
|
|
/**
|
|
* PUT 请求
|
|
*/
|
|
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.put<T>(url, data, config);
|
|
}
|
|
|
|
/**
|
|
* DELETE 请求
|
|
*/
|
|
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.delete<T>(url, config);
|
|
}
|
|
|
|
/**
|
|
* PATCH 请求
|
|
*/
|
|
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
|
return this.axiosInstance.patch<T>(url, data, config);
|
|
}
|
|
}
|