初始提交
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
const TokenKey = "App-Token";
|
||||
|
||||
export function getToken() {
|
||||
return uni.getStorageSync(TokenKey);
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
return uni.setStorageSync(TokenKey, token);
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return uni.removeStorageSync(TokenKey);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 显示消息提示框
|
||||
* @param content 提示的标题
|
||||
*/
|
||||
export function toast(content) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: content,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模态弹窗
|
||||
* @param content 提示的标题
|
||||
*/
|
||||
export function showConfirm(content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: content,
|
||||
cancelText: "取消",
|
||||
confirmText: "确定",
|
||||
success: function (res) {
|
||||
resolve(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数处理
|
||||
* @param params 参数
|
||||
*/
|
||||
export function tansParams(params) {
|
||||
let result = "";
|
||||
for (const propName of Object.keys(params)) {
|
||||
const value = params[propName];
|
||||
var part = encodeURIComponent(propName) + "=";
|
||||
if (value !== null && value !== "" && typeof value !== "undefined") {
|
||||
if (typeof value === "object") {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (
|
||||
value[key] !== null &&
|
||||
value[key] !== "" &&
|
||||
typeof value[key] !== "undefined"
|
||||
) {
|
||||
let params = propName + "[" + key + "]";
|
||||
var subPart = encodeURIComponent(params) + "=";
|
||||
result += subPart + encodeURIComponent(value[key]) + "&";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result += part + encodeURIComponent(value) + "&";
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const constant = {
|
||||
avatar: "user_avatar",
|
||||
id: "user_id",
|
||||
name: "user_name",
|
||||
roles: "user_roles",
|
||||
permissions: "user_permissions",
|
||||
};
|
||||
|
||||
export default constant;
|
||||
@@ -0,0 +1,30 @@
|
||||
import useDictStore from "@/store/modules/dict";
|
||||
import { getDicts } from "@/api/system/dict/data";
|
||||
import { ref, toRefs } from "vue";
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
*/
|
||||
export function useDict(...args) {
|
||||
const res = ref({});
|
||||
return (() => {
|
||||
args.forEach((dictType, index) => {
|
||||
res.value[dictType] = [];
|
||||
const dicts = useDictStore().getDict(dictType);
|
||||
if (dicts) {
|
||||
res.value[dictType] = dicts;
|
||||
} else {
|
||||
getDicts(dictType).then((resp) => {
|
||||
res.value[dictType] = resp.data.map((p) => ({
|
||||
label: p.dictLabel,
|
||||
value: p.dictValue,
|
||||
elTagType: p.listClass,
|
||||
elTagClass: p.cssClass,
|
||||
}));
|
||||
useDictStore().setDict(dictType, res.value[dictType]);
|
||||
});
|
||||
}
|
||||
});
|
||||
return toRefs(res.value);
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
401: "认证失败,无法访问系统资源",
|
||||
403: "当前操作没有权限",
|
||||
404: "访问资源不存在",
|
||||
default: "系统未知错误,请反馈给管理员",
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import store from "@/store";
|
||||
|
||||
/**
|
||||
* 字符权限校验
|
||||
* @param {Array} value 校验值
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function checkPermi(value) {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissions = store.getters && store.getters.permissions;
|
||||
const permissionDatas = value;
|
||||
const all_permission = "*:*:*";
|
||||
|
||||
const hasPermission = permissions.some((permission) => {
|
||||
return (
|
||||
all_permission === permission || permissionDatas.includes(permission)
|
||||
);
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(
|
||||
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限校验
|
||||
* @param {Array} value 校验值
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function checkRole(value) {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const roles = store.getters && store.getters.roles;
|
||||
const permissionRoles = value;
|
||||
const super_admin = "admin";
|
||||
|
||||
const hasRole = roles.some((role) => {
|
||||
return super_admin === role || permissionRoles.includes(role);
|
||||
});
|
||||
|
||||
if (!hasRole) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(`need roles! Like checkRole="['admin','editor']"`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import config from "@/config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import errorCode from "@/utils/errorCode";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { toast, showConfirm, tansParams } from "@/utils/common";
|
||||
import {
|
||||
decryptTransportErrorResponse,
|
||||
decryptTransportResponse,
|
||||
encryptTransportRequest,
|
||||
invalidateTransportKeyMeta,
|
||||
resetTransportRequestConfig,
|
||||
shouldRetryTransportWithFreshKey,
|
||||
} from "@/utils/transportCrypto";
|
||||
|
||||
let timeout = 10000;
|
||||
const baseUrl = config.baseUrl;
|
||||
|
||||
const request = async (config) => {
|
||||
// 是否需要设置 token
|
||||
const isToken = (config.headers || {}).isToken === false;
|
||||
config.header = config.header || {};
|
||||
config.headers = config.headers || {};
|
||||
if (getToken() && !isToken) {
|
||||
config.header["Authorization"] = "Bearer " + getToken();
|
||||
}
|
||||
|
||||
try {
|
||||
config = await encryptTransportRequest(config);
|
||||
|
||||
// get请求映射params参数
|
||||
if (config.params) {
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
config.url = url;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
method: config.method || "get",
|
||||
timeout: config.timeout || timeout,
|
||||
url: config.baseUrl || baseUrl + config.url,
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
dataType: "json",
|
||||
success: async (response) => {
|
||||
try {
|
||||
const res = await decryptTransportResponse(response, config);
|
||||
|
||||
if (shouldRetryTransportWithFreshKey(res) && !config.__transportRetried) {
|
||||
invalidateTransportKeyMeta();
|
||||
config.__transportRetried = true;
|
||||
config.headers.repeatSubmit = false;
|
||||
resetTransportRequestConfig(config);
|
||||
resolve(await request(config));
|
||||
return;
|
||||
}
|
||||
|
||||
const code = res.data.code || 200;
|
||||
const msg = errorCode[code] || res.data.msg || errorCode["default"];
|
||||
if (code === 401) {
|
||||
showConfirm(
|
||||
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
|
||||
).then((res) => {
|
||||
if (res.confirm) {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then(() => {
|
||||
uni.reLaunch({ url: "/pages/login" });
|
||||
});
|
||||
}
|
||||
});
|
||||
const error = new Error("无效的会话,或者会话已过期,请重新登录。");
|
||||
error.response = res;
|
||||
reject(error);
|
||||
} else if (code === 500) {
|
||||
const error = new Error(msg);
|
||||
error.response = res;
|
||||
reject(error);
|
||||
} else if (code !== 200) {
|
||||
const error = new Error(msg);
|
||||
error.response = res;
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
error = await decryptTransportErrorResponse(error, config);
|
||||
if (shouldRetryTransportWithFreshKey(error) && !config.__transportRetried) {
|
||||
invalidateTransportKeyMeta();
|
||||
config.__transportRetried = true;
|
||||
config.headers.repeatSubmit = false;
|
||||
resetTransportRequestConfig(config);
|
||||
return request(config);
|
||||
}
|
||||
|
||||
const response = error.response;
|
||||
const responseStatus = response?.status ?? response?.statusCode;
|
||||
const responseCode = response?.data?.code;
|
||||
const responseMsg = response?.data?.msg;
|
||||
if (responseMsg) {
|
||||
uni.showToast({
|
||||
title: responseMsg,
|
||||
icon: "none",
|
||||
duration: responseStatus === 429 || responseCode === 429 ? 5000 : 3000,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
let { message } = error;
|
||||
if (message === "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message && message.includes("timeout")) {
|
||||
message = "系统接口请求超时";
|
||||
} else if (message && message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
}
|
||||
if (message) {
|
||||
toast(message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default request;
|
||||
@@ -0,0 +1,38 @@
|
||||
import constant from "./constant";
|
||||
|
||||
// 存储变量名
|
||||
let storageKey = "storage_data";
|
||||
|
||||
// 存储节点变量名
|
||||
let storageNodeKeys = [
|
||||
constant.avatar,
|
||||
constant.id,
|
||||
constant.name,
|
||||
constant.roles,
|
||||
constant.permissions,
|
||||
];
|
||||
|
||||
const storage = {
|
||||
set: function (key, value) {
|
||||
if (storageNodeKeys.indexOf(key) != -1) {
|
||||
let tmp = uni.getStorageSync(storageKey);
|
||||
tmp = tmp ? tmp : {};
|
||||
tmp[key] = value;
|
||||
uni.setStorageSync(storageKey, tmp);
|
||||
}
|
||||
},
|
||||
get: function (key) {
|
||||
let storageData = uni.getStorageSync(storageKey) || {};
|
||||
return storageData[key] || "";
|
||||
},
|
||||
remove: function (key) {
|
||||
let storageData = uni.getStorageSync(storageKey) || {};
|
||||
delete storageData[key];
|
||||
uni.setStorageSync(storageKey, storageData);
|
||||
},
|
||||
clean: function () {
|
||||
uni.removeStorageSync(storageKey);
|
||||
},
|
||||
};
|
||||
|
||||
export default storage;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,436 @@
|
||||
import config from "@/config";
|
||||
|
||||
const TRANSPORT_BASE_URL = config.baseUrl;
|
||||
const EXCLUDED_URL_PATTERNS = [
|
||||
"/transport/crypto/frontend-config",
|
||||
"/transport/crypto/public-key",
|
||||
"/common/download",
|
||||
"/common/download/resource",
|
||||
];
|
||||
const TRANSPORT_FRONTEND_CONFIG_CACHE_KEY = "transportCryptoFrontendConfig";
|
||||
const TRANSPORT_FRONTEND_CONFIG_URL = "/transport/crypto/frontend-config";
|
||||
const TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS = 60;
|
||||
const DEFAULT_TRANSPORT_ENVELOPE_VERSION = "1";
|
||||
const DEFAULT_REQUEST_ENVELOPE_ALGORITHM = "RSA_OAEP_AES_256_GCM";
|
||||
const DEFAULT_RESPONSE_ENVELOPE_ALGORITHM = "AES_256_GCM";
|
||||
const DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH = 4096;
|
||||
|
||||
let cachedTransportPolicy = null;
|
||||
let inflightTransportPolicyPromise = null;
|
||||
|
||||
/**
|
||||
* 获取当前 Unix 秒级时间戳。
|
||||
*
|
||||
* @returns {number} 当前时间戳
|
||||
*/
|
||||
function getNowTimestamp() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求地址是否命中固定排除名单。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @returns {boolean} 是否命中排除规则
|
||||
*/
|
||||
function matchExcludedUrl(url = "") {
|
||||
return EXCLUDED_URL_PATTERNS.some((pattern) => url.includes(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求路径是否命中路径前缀列表。
|
||||
*
|
||||
* @param {string} path 待匹配路径
|
||||
* @param {string[]} pathPatterns 路径前缀集合
|
||||
* @returns {boolean} 是否匹配成功
|
||||
*/
|
||||
function matchPathPrefix(path = "", pathPatterns = []) {
|
||||
return pathPatterns.some(
|
||||
(pattern) => path === pattern || path.startsWith(`${pattern}/`),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从绝对地址中提取 pathname,或直接返回相对地址的路径部分。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @returns {string} 标准化后的路径
|
||||
*/
|
||||
function parseAbsoluteUrlPath(url = "") {
|
||||
const normalizedUrl = String(url || "");
|
||||
if (!normalizedUrl) {
|
||||
return "/";
|
||||
}
|
||||
if (
|
||||
!normalizedUrl.startsWith("http://") &&
|
||||
!normalizedUrl.startsWith("https://")
|
||||
) {
|
||||
return normalizedUrl.split("?")[0] || "/";
|
||||
}
|
||||
const pathMatch = normalizedUrl.match(/^https?:\/\/[^/]+(\/[^?#]*)?/i);
|
||||
return pathMatch?.[1] || "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析基础 API 地址对应的路径前缀。
|
||||
*
|
||||
* @param {string} baseUrl 基础 API 地址
|
||||
* @returns {string} 基础路径前缀
|
||||
*/
|
||||
function getBaseApiPath(baseUrl = TRANSPORT_BASE_URL) {
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
const baseApiPath = parseAbsoluteUrlPath(baseUrl);
|
||||
return baseApiPath === "/" ? "" : baseApiPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算后端用于 AAD 与策略匹配的标准请求路径。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @param {string} baseUrl 基础 API 地址
|
||||
* @returns {string} 标准化请求路径
|
||||
*/
|
||||
function getRequestPath(url = "", baseUrl = TRANSPORT_BASE_URL) {
|
||||
const baseApiPath = getBaseApiPath(baseUrl);
|
||||
const pathname = parseAbsoluteUrlPath(url);
|
||||
if (baseApiPath && pathname.startsWith(baseApiPath)) {
|
||||
const normalizedPath = pathname.slice(baseApiPath.length);
|
||||
return normalizedPath || "/";
|
||||
}
|
||||
return pathname || "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化后端下发的路径列表配置。
|
||||
*
|
||||
* @param {Array} paths 原始路径集合
|
||||
* @returns {string[]} 标准化后的路径数组
|
||||
*/
|
||||
function normalizePaths(paths) {
|
||||
if (!Array.isArray(paths)) {
|
||||
return [];
|
||||
}
|
||||
return paths.map((path) => String(path || "").trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头对象中读取指定字段。
|
||||
*
|
||||
* @param {Object} headers 请求头对象
|
||||
* @param {string} name 请求头名称
|
||||
* @returns {*} 请求头值
|
||||
*/
|
||||
function getHeaderValue(headers, name) {
|
||||
if (!headers) {
|
||||
return undefined;
|
||||
}
|
||||
return headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端配置响应转换为前端统一使用的策略对象。
|
||||
*
|
||||
* @param {Object} payload 后端返回的配置数据
|
||||
* @returns {Object} 标准化后的传输加密策略
|
||||
*/
|
||||
function normalizeTransportPolicy(payload) {
|
||||
return {
|
||||
transportCryptoEnabled: Boolean(payload?.transportCryptoEnabled),
|
||||
transportCryptoMode: String(payload?.transportCryptoMode || "off"),
|
||||
transportCryptoActive: Boolean(payload?.transportCryptoActive),
|
||||
envelopeVersion: String(
|
||||
payload?.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION,
|
||||
),
|
||||
publicKeyUrl: String(payload?.publicKeyUrl || "/transport/crypto/public-key"),
|
||||
requestEnvelopeAlgorithm: String(
|
||||
payload?.requestEnvelopeAlgorithm || DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
|
||||
),
|
||||
responseEnvelopeAlgorithm: String(
|
||||
payload?.responseEnvelopeAlgorithm || DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
|
||||
),
|
||||
enabledPaths: normalizePaths(payload?.enabledPaths),
|
||||
requiredPaths: normalizePaths(payload?.requiredPaths),
|
||||
excludePaths: normalizePaths(payload?.excludePaths),
|
||||
maxEncryptedGetUrlLength: Number(
|
||||
payload?.maxEncryptedGetUrlLength || DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
|
||||
),
|
||||
configExpireAt: Number(payload?.configExpireAt || 0),
|
||||
retryAt: Number(payload?.retryAt || payload?.configExpireAt || 0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建不可用场景下的本地兜底策略。
|
||||
*
|
||||
* @returns {Object} 明文回退策略
|
||||
*/
|
||||
function buildFallbackTransportPolicy() {
|
||||
const nowTimestamp = getNowTimestamp();
|
||||
return {
|
||||
transportCryptoEnabled: false,
|
||||
transportCryptoMode: "off",
|
||||
transportCryptoActive: false,
|
||||
envelopeVersion: DEFAULT_TRANSPORT_ENVELOPE_VERSION,
|
||||
publicKeyUrl: "/transport/crypto/public-key",
|
||||
requestEnvelopeAlgorithm: DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
|
||||
responseEnvelopeAlgorithm: DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
|
||||
enabledPaths: [],
|
||||
requiredPaths: [],
|
||||
excludePaths: [...EXCLUDED_URL_PATTERNS],
|
||||
maxEncryptedGetUrlLength: DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
|
||||
configExpireAt:
|
||||
nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
|
||||
retryAt: nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于旧策略生成短期可重试的缓存策略。
|
||||
*
|
||||
* @param {Object} policy 旧的策略对象
|
||||
* @returns {Object} 可重试策略
|
||||
*/
|
||||
function buildRetryableTransportPolicy(policy) {
|
||||
const normalizedPolicy = normalizeTransportPolicy(policy);
|
||||
return {
|
||||
...normalizedPolicy,
|
||||
retryAt: getNowTimestamp() + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前策略是否仍在可用期内。
|
||||
*
|
||||
* @param {Object} policy 待校验策略
|
||||
* @returns {boolean} 是否可继续使用
|
||||
*/
|
||||
function isUsableTransportPolicy(policy) {
|
||||
if (!policy || !policy.publicKeyUrl || !policy.retryAt) {
|
||||
return false;
|
||||
}
|
||||
return policy.retryAt > getNowTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地缓存加载最近一次持久化的策略。
|
||||
*
|
||||
* @returns {Object|null} 缓存策略
|
||||
*/
|
||||
function loadPersistedTransportPolicy() {
|
||||
const persistedTransportPolicy = uni.getStorageSync(
|
||||
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
|
||||
);
|
||||
if (!persistedTransportPolicy) {
|
||||
return null;
|
||||
}
|
||||
return normalizeTransportPolicy(persistedTransportPolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求后端传输加密前端配置。
|
||||
* 这里直接使用原始 uni.request,避免策略初始化阶段反向依赖统一 request
|
||||
* 包装器,导致“是否加密尚未判定时又要先走加密请求”的循环依赖。
|
||||
*
|
||||
* @returns {Promise<Object>} Uni 请求响应结果
|
||||
*/
|
||||
function requestFrontendConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${TRANSPORT_BASE_URL}${TRANSPORT_FRONTEND_CONFIG_URL}`,
|
||||
method: "GET",
|
||||
timeout: 10000,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求加密使用的标准路径。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @param {string} baseUrl 基础 API 地址
|
||||
* @returns {string} 标准请求路径
|
||||
*/
|
||||
export function getTransportRequestPath(
|
||||
url = "",
|
||||
baseUrl = TRANSPORT_BASE_URL,
|
||||
) {
|
||||
return getRequestPath(url, baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生效的传输加密策略。
|
||||
*
|
||||
* @returns {Object} 当前策略对象
|
||||
*/
|
||||
export function getTransportCryptoPolicy() {
|
||||
return cachedTransportPolicy || buildFallbackTransportPolicy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前策略缓存与持久化数据。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function invalidateTransportCryptoPolicy() {
|
||||
cachedTransportPolicy = null;
|
||||
inflightTransportPolicyPromise = null;
|
||||
uni.removeStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保本地已加载一份可用的传输加密策略。
|
||||
*
|
||||
* @param {boolean} forceRefresh 是否强制从后端刷新
|
||||
* @returns {Promise<Object>} 当前可用策略
|
||||
*/
|
||||
export async function ensureTransportCryptoPolicyLoaded(forceRefresh = false) {
|
||||
if (!forceRefresh && !cachedTransportPolicy) {
|
||||
const persistedTransportPolicy = loadPersistedTransportPolicy();
|
||||
if (isUsableTransportPolicy(persistedTransportPolicy)) {
|
||||
cachedTransportPolicy = persistedTransportPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
if (!forceRefresh && isUsableTransportPolicy(cachedTransportPolicy)) {
|
||||
return cachedTransportPolicy;
|
||||
}
|
||||
|
||||
if (inflightTransportPolicyPromise) {
|
||||
return inflightTransportPolicyPromise;
|
||||
}
|
||||
|
||||
inflightTransportPolicyPromise = requestFrontendConfig()
|
||||
.then((response) => {
|
||||
const payload = normalizeTransportPolicy(response?.data?.data || {});
|
||||
cachedTransportPolicy = payload;
|
||||
uni.setStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY, payload);
|
||||
inflightTransportPolicyPromise = null;
|
||||
return cachedTransportPolicy;
|
||||
})
|
||||
.catch((error) => {
|
||||
const staleTransportPolicy =
|
||||
cachedTransportPolicy || loadPersistedTransportPolicy();
|
||||
inflightTransportPolicyPromise = null;
|
||||
cachedTransportPolicy = staleTransportPolicy
|
||||
? buildRetryableTransportPolicy(staleTransportPolicy)
|
||||
: buildFallbackTransportPolicy();
|
||||
uni.setStorageSync(
|
||||
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
|
||||
cachedTransportPolicy,
|
||||
);
|
||||
if (staleTransportPolicy) {
|
||||
console.warn(
|
||||
"加载传输加密前端配置失败,当前继续沿用最近一次后端策略",
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
console.warn("加载传输加密前端配置失败,当前回退为明文请求策略", error);
|
||||
}
|
||||
return cachedTransportPolicy;
|
||||
});
|
||||
|
||||
return inflightTransportPolicyPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否需要执行请求体加密。
|
||||
*
|
||||
* @param {Object} requestConfig 请求配置
|
||||
* @param {Object} transportPolicy 传输加密策略
|
||||
* @returns {boolean} 是否启用请求加密
|
||||
*/
|
||||
export function shouldEncryptRequest(
|
||||
requestConfig,
|
||||
transportPolicy = getTransportCryptoPolicy(),
|
||||
) {
|
||||
if (!transportPolicy.transportCryptoActive) {
|
||||
return false;
|
||||
}
|
||||
const requestPath = getRequestPath(
|
||||
requestConfig.url,
|
||||
requestConfig.baseUrl || TRANSPORT_BASE_URL,
|
||||
);
|
||||
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(transportPolicy.enabledPaths || []).length &&
|
||||
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ((requestConfig.headers || {}).encrypt === false) {
|
||||
return false;
|
||||
}
|
||||
if (matchExcludedUrl(requestConfig.url)) {
|
||||
return false;
|
||||
}
|
||||
const contentType =
|
||||
getHeaderValue(requestConfig.header, "Content-Type") ||
|
||||
getHeaderValue(requestConfig.headers, "Content-Type") ||
|
||||
"";
|
||||
if (String(contentType).includes("multipart/form-data")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前响应是否需要执行自动解密。
|
||||
*
|
||||
* @param {Object} requestConfig 请求配置
|
||||
* @param {Object} transportPolicy 传输加密策略
|
||||
* @returns {boolean} 是否启用响应解密
|
||||
*/
|
||||
export function shouldEncryptResponse(
|
||||
requestConfig,
|
||||
transportPolicy = getTransportCryptoPolicy(),
|
||||
) {
|
||||
const requestPath = getRequestPath(
|
||||
requestConfig.url,
|
||||
requestConfig.baseUrl || TRANSPORT_BASE_URL,
|
||||
);
|
||||
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(transportPolicy.enabledPaths || []).length &&
|
||||
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ((requestConfig.headers || {}).encryptResponse === false) {
|
||||
return false;
|
||||
}
|
||||
if (matchExcludedUrl(requestConfig.url)) {
|
||||
return false;
|
||||
}
|
||||
if (requestConfig.__transportCryptoEnabledForRequest === true) {
|
||||
return true;
|
||||
}
|
||||
if (requestConfig.__transportCryptoEnabledForRequest === false) {
|
||||
return false;
|
||||
}
|
||||
return transportPolicy.transportCryptoActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断查询参数是否需要封装为加密信封。
|
||||
*
|
||||
* @param {Object} requestConfig 请求配置
|
||||
* @param {Object} transportPolicy 传输加密策略
|
||||
* @returns {boolean} 是否启用查询参数加密
|
||||
*/
|
||||
export function shouldEncryptQuery(
|
||||
requestConfig,
|
||||
transportPolicy = getTransportCryptoPolicy(),
|
||||
) {
|
||||
if ((requestConfig.headers || {}).encryptQuery === false) {
|
||||
return false;
|
||||
}
|
||||
return shouldEncryptRequest(requestConfig, transportPolicy);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import forge from "node-forge/lib/forge";
|
||||
import "node-forge/lib/util";
|
||||
import "node-forge/lib/asn1";
|
||||
import "node-forge/lib/oids";
|
||||
import "node-forge/lib/cipher";
|
||||
import "node-forge/lib/cipherModes";
|
||||
import "node-forge/lib/aes";
|
||||
import "node-forge/lib/jsbn";
|
||||
import "node-forge/lib/pkcs1";
|
||||
import "node-forge/lib/prime";
|
||||
import "node-forge/lib/random";
|
||||
import "node-forge/lib/md";
|
||||
import "node-forge/lib/sha256";
|
||||
import "node-forge/lib/mgf1";
|
||||
import "node-forge/lib/pem";
|
||||
import "node-forge/lib/pki";
|
||||
|
||||
let randomBytePool = "";
|
||||
const FORGE_RANDOM_REFILL_BYTES = 4096;
|
||||
|
||||
/**
|
||||
* 将 Uint8Array 转为 Forge 兼容的二进制字符串。
|
||||
*
|
||||
* @param {Uint8Array} uint8Array 字节数组
|
||||
* @returns {string} 二进制字符串
|
||||
*/
|
||||
function uint8ArrayToBytes(uint8Array) {
|
||||
return Array.from(uint8Array, (item) => String.fromCharCode(item)).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运行时的全局对象引用。
|
||||
*
|
||||
* @returns {Object|undefined} 全局对象
|
||||
*/
|
||||
function getRuntimeGlobal() {
|
||||
if (typeof globalThis !== "undefined") {
|
||||
return globalThis;
|
||||
}
|
||||
if (typeof self !== "undefined") {
|
||||
return self;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
return window;
|
||||
}
|
||||
if (typeof global !== "undefined") {
|
||||
return global;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Web Crypto 同步获取随机字节。
|
||||
*
|
||||
* @param {number} length 需要的字节长度
|
||||
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
|
||||
*/
|
||||
function getWebCryptoRandomBytes(length) {
|
||||
const runtimeCrypto = getRuntimeGlobal()?.crypto;
|
||||
if (!runtimeCrypto?.getRandomValues) {
|
||||
return null;
|
||||
}
|
||||
const bytes = new Uint8Array(length);
|
||||
runtimeCrypto.getRandomValues(bytes);
|
||||
return uint8ArrayToBytes(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 app-plus iOS 端通过原生 NSUUID 获取随机字节兜底。
|
||||
*
|
||||
* @param {number} length 需要的字节长度
|
||||
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
|
||||
*/
|
||||
function getAppPlusIosRandomBytes(length) {
|
||||
// #ifdef APP-PLUS
|
||||
if (typeof plus === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const osName = String(plus.os?.name || "").toLowerCase();
|
||||
if (osName !== "ios" || typeof plus.ios?.importClass !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const uuidClass = plus.ios.importClass("NSUUID");
|
||||
let randomBytes = "";
|
||||
while (randomBytes.length < length) {
|
||||
const uuidObject = plus.ios.invoke(uuidClass, "UUID");
|
||||
const uuidText = String(plus.ios.invoke(uuidObject, "UUIDString") || "")
|
||||
.replace(/-/g, "")
|
||||
.toLowerCase();
|
||||
plus.ios.deleteObject(uuidObject);
|
||||
if (!uuidText) {
|
||||
return null;
|
||||
}
|
||||
for (
|
||||
let index = 0;
|
||||
index < uuidText.length && randomBytes.length < length;
|
||||
index += 2
|
||||
) {
|
||||
const byteValue = parseInt(uuidText.slice(index, index + 2), 16);
|
||||
if (isNaN(byteValue)) {
|
||||
return null;
|
||||
}
|
||||
randomBytes += String.fromCharCode(byteValue);
|
||||
}
|
||||
}
|
||||
return randomBytes;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 app-plus Android 端通过原生 SecureRandom 获取随机字节兜底。
|
||||
*
|
||||
* @param {number} length 需要的字节长度
|
||||
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
|
||||
*/
|
||||
function getAppPlusAndroidRandomBytes(length) {
|
||||
// #ifdef APP-PLUS
|
||||
if (typeof plus === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const osName = String(plus.os?.name || "").toLowerCase();
|
||||
if (osName !== "android" || typeof plus.android?.importClass !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
plus.android.importClass("java.security.SecureRandom");
|
||||
const secureRandom =
|
||||
typeof plus.android.newObject === "function"
|
||||
? plus.android.newObject("java.security.SecureRandom")
|
||||
: null;
|
||||
if (!secureRandom) {
|
||||
return null;
|
||||
}
|
||||
plus.android.importClass(secureRandom);
|
||||
let randomBytes = "";
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
randomBytes += String.fromCharCode(secureRandom.nextInt(256));
|
||||
}
|
||||
if (typeof plus.android.autoCollection === "function") {
|
||||
plus.android.autoCollection(secureRandom);
|
||||
}
|
||||
return randomBytes;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在随机池不足时尝试同步补充一段新的随机字节。
|
||||
*
|
||||
* @param {number} length 当前至少需要的字节长度
|
||||
* @returns {void}
|
||||
*/
|
||||
function refillRandomBytePool(length) {
|
||||
const refillLength = Math.max(length, FORGE_RANDOM_REFILL_BYTES);
|
||||
const randomBytes =
|
||||
getWebCryptoRandomBytes(refillLength) ||
|
||||
getAppPlusAndroidRandomBytes(refillLength) ||
|
||||
getAppPlusIosRandomBytes(refillLength);
|
||||
if (randomBytes) {
|
||||
randomBytePool += randomBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从随机数池中按需提取字节串。
|
||||
*
|
||||
* @param {number} length 需要提取的字节长度
|
||||
* @returns {string} Forge 使用的二进制字符串
|
||||
*/
|
||||
function consumeRandomBytes(length) {
|
||||
if (randomBytePool.length < length) {
|
||||
refillRandomBytePool(length);
|
||||
}
|
||||
if (randomBytePool.length < length) {
|
||||
throw new Error("传输加密随机数池不足,且当前运行环境无法同步补充安全随机数");
|
||||
}
|
||||
const bytes = randomBytePool.slice(0, length);
|
||||
randomBytePool = randomBytePool.slice(length);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
forge.random.getBytesSync = function getBytesSync(length) {
|
||||
return consumeRandomBytes(length);
|
||||
};
|
||||
|
||||
forge.random.getBytes = function getBytes(length, callback) {
|
||||
const bytes = consumeRandomBytes(length);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, bytes);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 向 Forge 随机数池预填充平台侧生成的随机字节。
|
||||
*
|
||||
* @param {string} bytes 待注入的二进制字符串
|
||||
* @returns {void}
|
||||
*/
|
||||
export function primeForgeRandomBytes(bytes) {
|
||||
randomBytePool += String(bytes || "");
|
||||
}
|
||||
|
||||
export default forge;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useUserStore } from "@/store";
|
||||
import config from "@/config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import errorCode from "@/utils/errorCode";
|
||||
import { toast, showConfirm, tansParams } from "@/utils/common";
|
||||
|
||||
let timeout = 10000;
|
||||
const baseUrl = config.baseUrl;
|
||||
|
||||
export default function upload(config) {
|
||||
// 是否需要设置 token
|
||||
const isToken = (config.headers || {}).isToken === false;
|
||||
config.header = config.header || {};
|
||||
if (getToken() && !isToken) {
|
||||
config.header["Authorization"] = "Bearer " + getToken();
|
||||
}
|
||||
// get请求映射params参数
|
||||
if (config.params) {
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
config.url = url;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
timeout: config.timeout || timeout,
|
||||
url: baseUrl + config.url,
|
||||
filePath: config.filePath,
|
||||
name: config.name || "file",
|
||||
header: config.header,
|
||||
formData: config.formData,
|
||||
success: (res) => {
|
||||
let result = JSON.parse(res.data);
|
||||
const code = result.code || 200;
|
||||
const msg = errorCode[code] || result.msg || errorCode["default"];
|
||||
if (code === 200) {
|
||||
resolve(result);
|
||||
} else if (code == 401) {
|
||||
showConfirm(
|
||||
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
|
||||
).then((res) => {
|
||||
if (res.confirm) {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then((res) => {
|
||||
uni.reLaunch({ url: "/pages/login/login" });
|
||||
});
|
||||
}
|
||||
});
|
||||
reject("无效的会话,或者会话已过期,请重新登录。");
|
||||
} else if (code === 500) {
|
||||
toast(msg);
|
||||
reject("500");
|
||||
} else if (code !== 200) {
|
||||
toast(msg);
|
||||
reject(code);
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
let { message } = error;
|
||||
if (message == "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message.includes("timeout")) {
|
||||
message = "系统接口请求超时";
|
||||
} else if (message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
}
|
||||
toast(message);
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 路径匹配器
|
||||
* @param {string} pattern
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isPathMatch(pattern, path) {
|
||||
const regexPattern = pattern
|
||||
.replace(/\//g, "\\/")
|
||||
.replace(/\*\*/g, ".*")
|
||||
.replace(/\*/g, "[^\\/]*");
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断value字符串是否为空
|
||||
* @param {string} value
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isEmpty(value) {
|
||||
if (
|
||||
value == null ||
|
||||
value == "" ||
|
||||
value == undefined ||
|
||||
value == "undefined"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断url是否是http或https
|
||||
* @param {string} url
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isHttp(url) {
|
||||
return url.indexOf("http://") !== -1 || url.indexOf("https://") !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断path是否为外链
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUsername(str) {
|
||||
const valid_map = ["admin", "editor"];
|
||||
return valid_map.indexOf(str.trim()) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validURL(url) {
|
||||
const reg =
|
||||
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
|
||||
return reg.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validLowerCase(str) {
|
||||
const reg = /^[a-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validUpperCase(str) {
|
||||
const reg = /^[A-Z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validAlphabets(str) {
|
||||
const reg = /^[A-Za-z]+$/;
|
||||
return reg.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function validEmail(email) {
|
||||
const reg =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return reg.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isString(str) {
|
||||
return typeof str === "string" || str instanceof String;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} arg
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isArray(arg) {
|
||||
if (typeof Array.isArray === "undefined") {
|
||||
return Object.prototype.toString.call(arg) === "[object Array]";
|
||||
}
|
||||
return Array.isArray(arg);
|
||||
}
|
||||
Reference in New Issue
Block a user