Import JC Web IM
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import axios from 'axios';
|
||||
import { appConfig } from '@/config';
|
||||
|
||||
const AES_KEY = 'xczx2026!@#secKe';
|
||||
const ENCRYPTION_EXCLUDE_PATHS = ['/avatar/', '/f/', '/amr2mp3', '/appVersion/check'];
|
||||
const AUTH_STORAGE_KEY = 'qmt-web-im-app-auth-v1';
|
||||
const AUTH_ERROR_CODE = 13;
|
||||
const REFRESH_ERROR_CODE = 20045;
|
||||
|
||||
let importedCryptoKeyPromise = null;
|
||||
let refreshPromise = null;
|
||||
|
||||
function storageScope() {
|
||||
return appConfig.appServer || '';
|
||||
}
|
||||
|
||||
function readAuthStore() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(AUTH_STORAGE_KEY) || '{}');
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeAuthStore(store) {
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(store));
|
||||
}
|
||||
|
||||
function normalizeAuthBundle(bundle = {}) {
|
||||
const accessToken = String(bundle.accessToken || '').trim();
|
||||
const refreshToken = String(bundle.refreshToken || '').trim();
|
||||
if (!accessToken || !refreshToken) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenType: String(bundle.tokenType || 'Bearer').trim() || 'Bearer',
|
||||
accessTokenExpireIn: Number(bundle.accessTokenExpireIn || 0) || 0,
|
||||
accessTokenExpireAt: Number(bundle.accessTokenExpireAt || 0) || 0,
|
||||
refreshTokenExpireIn: Number(bundle.refreshTokenExpireIn || 0) || 0,
|
||||
refreshTokenExpireAt: Number(bundle.refreshTokenExpireAt || 0) || 0
|
||||
};
|
||||
}
|
||||
|
||||
export function getStoredAppAuth() {
|
||||
const store = readAuthStore();
|
||||
return normalizeAuthBundle(store[storageScope()]);
|
||||
}
|
||||
|
||||
export function persistAppAuth(bundle) {
|
||||
const normalized = normalizeAuthBundle(bundle);
|
||||
const store = readAuthStore();
|
||||
if (!normalized) {
|
||||
delete store[storageScope()];
|
||||
writeAuthStore(store);
|
||||
return null;
|
||||
}
|
||||
store[storageScope()] = normalized;
|
||||
writeAuthStore(store);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function clearStoredAppAuth() {
|
||||
persistAppAuth(null);
|
||||
}
|
||||
|
||||
function hasFormData(value) {
|
||||
return typeof FormData !== 'undefined' && value instanceof FormData;
|
||||
}
|
||||
|
||||
function absoluteUrl(url) {
|
||||
return new URL(url, window.location.origin);
|
||||
}
|
||||
|
||||
function shouldEncrypt(url) {
|
||||
const pathname = absoluteUrl(url).pathname;
|
||||
return !ENCRYPTION_EXCLUDE_PATHS.some(path => pathname === path || pathname.startsWith(path));
|
||||
}
|
||||
|
||||
function shouldAttachAuth(config) {
|
||||
return !config.skipAuth;
|
||||
}
|
||||
|
||||
function textEncoder() {
|
||||
return new TextEncoder();
|
||||
}
|
||||
|
||||
function textDecoder() {
|
||||
return new TextDecoder();
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
let binary = '';
|
||||
bytes.forEach(byte => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function base64ToBytes(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function getCryptoKey() {
|
||||
if (!importedCryptoKeyPromise) {
|
||||
importedCryptoKeyPromise = window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
textEncoder().encode(AES_KEY),
|
||||
{ name: 'AES-CBC' },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
return importedCryptoKeyPromise;
|
||||
}
|
||||
|
||||
async function encryptPayload(plainText) {
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await getCryptoKey();
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: 'AES-CBC', iv },
|
||||
key,
|
||||
textEncoder().encode(plainText)
|
||||
);
|
||||
return `${bytesToBase64(iv)}:${bytesToBase64(new Uint8Array(encrypted))}`;
|
||||
}
|
||||
|
||||
async function decryptPayload(cipherText) {
|
||||
const [ivBase64, encryptedBase64] = String(cipherText || '').split(':', 2);
|
||||
if (!ivBase64 || !encryptedBase64) {
|
||||
throw new Error('Encrypted payload format is invalid');
|
||||
}
|
||||
const key = await getCryptoKey();
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv: base64ToBytes(ivBase64) },
|
||||
key,
|
||||
base64ToBytes(encryptedBase64)
|
||||
);
|
||||
return textDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
function normalizeBusinessResponse(payload) {
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: payload
|
||||
};
|
||||
}
|
||||
|
||||
async function decodeResponsePayload(response) {
|
||||
const encrypted = String(response.headers?.['x-encrypted'] || '').toLowerCase() === 'true';
|
||||
if (encrypted && typeof response.data === 'string') {
|
||||
const plainText = await decryptPayload(response.data);
|
||||
response.data = normalizeBusinessResponse(JSON.parse(plainText));
|
||||
return response;
|
||||
}
|
||||
response.data = normalizeBusinessResponse(response.data);
|
||||
return response;
|
||||
}
|
||||
|
||||
const client = axios.create({
|
||||
withCredentials: false
|
||||
});
|
||||
|
||||
client.interceptors.request.use(async config => {
|
||||
const nextConfig = { ...config };
|
||||
const url = absoluteUrl(nextConfig.url || '').toString();
|
||||
|
||||
if (shouldAttachAuth(nextConfig)) {
|
||||
const auth = getStoredAppAuth();
|
||||
if (auth?.accessToken) {
|
||||
nextConfig.headers = {
|
||||
...(nextConfig.headers || {}),
|
||||
Authorization: `${auth.tokenType || 'Bearer'} ${auth.accessToken}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFormData(nextConfig.data) && nextConfig.method?.toLowerCase() !== 'get' && shouldEncrypt(url)) {
|
||||
const plainBody = JSON.stringify(nextConfig.data ?? {});
|
||||
nextConfig.data = await encryptPayload(plainBody);
|
||||
nextConfig.headers = {
|
||||
...(nextConfig.headers || {}),
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
'X-Timestamp': `${Math.floor(Date.now() / 1000)}`
|
||||
};
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
});
|
||||
|
||||
client.interceptors.response.use(async response => decodeResponsePayload(response));
|
||||
|
||||
async function refreshAccessToken() {
|
||||
if (refreshPromise) {
|
||||
return refreshPromise;
|
||||
}
|
||||
const auth = getStoredAppAuth();
|
||||
if (!auth?.refreshToken) {
|
||||
clearStoredAppAuth();
|
||||
throw new Error('登录状态已失效,请重新登录');
|
||||
}
|
||||
|
||||
refreshPromise = client.post(`${appConfig.appServer}/auth/refreshToken`, {
|
||||
refreshToken: auth.refreshToken
|
||||
}, {
|
||||
skipAuth: true
|
||||
}).then(response => {
|
||||
const payload = response.data || {};
|
||||
if (Number(payload.code) !== 0 || !payload.data) {
|
||||
if (Number(payload.code) === REFRESH_ERROR_CODE) {
|
||||
clearStoredAppAuth();
|
||||
}
|
||||
throw new Error(payload.message || '刷新登录状态失败');
|
||||
}
|
||||
persistAppAuth(payload.data);
|
||||
return payload.data;
|
||||
}).finally(() => {
|
||||
refreshPromise = null;
|
||||
});
|
||||
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
async function request(config) {
|
||||
const response = await client.request(config);
|
||||
const payload = response.data || {};
|
||||
if (Number(payload.code) === AUTH_ERROR_CODE && shouldAttachAuth(config) && !config._retriedAfterRefresh) {
|
||||
await refreshAccessToken();
|
||||
return request({
|
||||
...config,
|
||||
_retriedAfterRefresh: true
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function appServerRequest(config) {
|
||||
return request(config);
|
||||
}
|
||||
|
||||
export async function appServerGet(url, config = {}) {
|
||||
return request({
|
||||
...config,
|
||||
method: 'get',
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
export async function appServerPost(url, data = {}, config = {}) {
|
||||
return request({
|
||||
...config,
|
||||
method: 'post',
|
||||
url,
|
||||
data
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user