unicorn-rpa
Version:
🦄 Unicorn RPA - 强大的跨域iframe自动化控制工具
426 lines (397 loc) • 14.3 kB
JavaScript
/**
* Unicorn RPA 安全策略模块
*
* 提供来源域名白名单验证和消息频率限制功能,确保只有来自可信域名的消息才能执行自动化操作,
* 并防止消息发送过于频繁。
* 支持精确匹配和通配符匹配两种模式。
*
* @module security
* @version 1.0.20
*/
const CryptoJS = require('crypto-js');
/**
* 安全配置对象
*
* 定义允许的来源域名白名单和消息频率限制配置。
* 只有来自这些域名的消息才会被接受,且每个来源每10秒只能发送一次消息。
* 支持通配符匹配,例如 'https://*.example.com' 可以匹配所有子域名。
*
* @type {Object}
* @property {string[]} ALLOWED_ORIGINS - 允许的来源域名列表
* @property {Object} RATE_LIMIT - 频率限制配置
* @property {number} RATE_LIMIT.INTERVAL - 消息发送间隔(毫秒),默认10秒
*/
const SECURITY_CONFIG = {
ALLOWED_ORIGINS: [
'http://localhost:5000', // Vite 默认端口
'http://localhost:5173', // Vite 默认端口
'http://localhost:8080', // 常用开发端口
'http://127.0.0.1:5000', // 本地IP地址
'http://127.0.0.1:5173', // 本地IP地址
'http://127.0.0.1:8080' // 本地IP地址
],
RATE_LIMIT: {
INTERVAL: 10000 // 10秒间隔(毫秒)
}
};
/**
* 消息发送记录存储
*
* 用于记录每个来源域名的最后发送时间,实现频率限制功能。
* 使用 Map 结构存储,key 为来源域名,value 为最后发送时间戳。
*
* @type {Map<string, number>}
*/
const messageHistory = new Map();
/**
* 支持的动作类型列表
*
* 定义所有支持的动作类型,用于验证接收到的动作是否可以被处理。
* 如果包含不支持的动作类型,整个请求将被拒绝。
*
* @type {string[]}
*/
const SUPPORTED_ACTION_TYPES = [
'fill', // 文本输入
'select', // 下拉选择
'click', // 点击
'doubleClick', // 双击
'rightClick', // 右键点击
'hover', // 悬停
'scroll', // 滚动
'screenshot', // 截图
'waitFor', // 等待元素出现
'wait', // 等待固定时长
'treeSelect', // 树形选择
'checkAllCheckboxes', // 批量勾选复选框
'getText', // 获取文本
'getValue', // 获取值
'timeRange', // 时间范围选择
'antCalendar', // ant-calendar 时间范围选择
'interfaceReturn', // 接口返回拦截
'waitForNext' // 智能等待下一个元素
];
/**
* 清理过期的消息记录
*
* 定期清理超过限制时间的消息记录,防止内存泄漏。
* 清理间隔为频率限制时间的2倍。
*/
function cleanupExpiredRecords() {
const now = Date.now();
const cutoffTime = now - SECURITY_CONFIG.RATE_LIMIT.INTERVAL;
const entries = messageHistory.entries();
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const origin = entry[0];
const lastTime = entry[1];
if (lastTime < cutoffTime) {
messageHistory.delete(origin);
}
}
}
// 启动定期清理任务
setInterval(cleanupExpiredRecords, SECURITY_CONFIG.RATE_LIMIT.INTERVAL * 2);
/**
* 验证动作类型是否支持
*
* 检查动作数组中的所有动作类型是否都在支持列表中。
* 如果有一个动作类型不支持,整个请求将被拒绝。
*
* @param {Array} actions - 动作数组
* @returns {Object} 验证结果
* @returns {boolean} returns.valid - 验证是否通过
* @returns {string} [returns.error] - 验证失败时的错误信息
* @returns {string} [returns.unsupportedType] - 不支持的动作类型
*
* @example
* // 所有动作类型都支持
* validateActionTypes([
* { type: 'click', selector: '.btn' },
* { type: 'fill', selector: '.input', value: 'test' }
* ]) // { valid: true }
*
* // 包含不支持的动作类型
* validateActionTypes([
* { type: 'click', selector: '.btn' },
* { type: 'unsupported', selector: '.element' }
* ]) // { valid: false, error: '不支持的动作类型', unsupportedType: 'unsupported' }
*/
function validateActionTypes(actions) {
// 检查动作数组是否存在且为数组
if (!Array.isArray(actions)) {
return {
valid: false,
error: 'actions必须是数组'
};
}
// 检查每个动作的类型
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
// 检查动作对象是否有效
if (!action || typeof action !== 'object') {
return {
valid: false,
error: `第${i + 1}个动作无效`
};
}
// 检查动作类型是否存在
if (!action.type || typeof action.type !== 'string') {
return {
valid: false,
error: `第${i + 1}个动作缺少type属性`
};
}
// 检查动作类型是否支持
if (!SUPPORTED_ACTION_TYPES.includes(action.type)) {
return {
valid: false,
error: '不支持的动作类型',
unsupportedType: action.type
};
}
}
return { valid: true };
}
/**
* 检查消息发送频率是否超过限制
*
* 验证指定来源是否在允许的时间间隔内发送过消息。
* 如果距离上次发送时间不足配置的间隔,则拒绝当前消息。
*
* @param {string} origin - 消息来源域名
* @returns {boolean} 如果允许发送返回 true,否则返回 false
*
* @example
* // 首次发送
* checkRateLimit('http://localhost:3000') // true
*
* // 5秒后再次发送(间隔不足10秒)
* checkRateLimit('http://localhost:3000') // false
*
* // 15秒后再次发送(间隔超过10秒)
* checkRateLimit('http://localhost:3000') // true
*/
function checkRateLimit(origin) {
const now = Date.now();
const lastTime = messageHistory.get(origin);
// 如果没有发送记录或距离上次发送已超过限制时间
if (!lastTime || (now - lastTime) >= SECURITY_CONFIG.RATE_LIMIT.INTERVAL) {
// 更新发送时间并允许发送
messageHistory.set(origin, now);
return true;
}
// 发送过于频繁,拒绝发送
const remainingTime = SECURITY_CONFIG.RATE_LIMIT.INTERVAL - (now - lastTime);
console.warn(`[安全警告] 消息发送过于频繁: ${origin},请等待 ${Math.ceil(remainingTime / 1000)} 秒后重试`);
return false;
}
/**
* 验证消息来源是否在白名单中
*
* 检查 postMessage 事件的来源域名是否在允许的白名单中。
* 支持精确匹配和通配符匹配(以 * 结尾的域名)。
*
* @param {MessageEvent} event - postMessage 事件对象
* @param {string} event.origin - 消息来源的完整域名(协议+域名+端口)
* @returns {boolean} 如果来源在白名单中返回 true,否则返回 false
*
* @example
* // 精确匹配
* validateOrigin({ origin: 'http://localhost:3000' }) // true
*
* // 通配符匹配
* validateOrigin({ origin: 'https://app.example.com' }) // 如果配置了 'https://*.example.com'
*
* // 不匹配
* validateOrigin({ origin: 'http://malicious.com' }) // false
*/
function validateOrigin(event) {
// 检查来源是否在白名单中,支持精确匹配和通配符匹配
let isOriginAllowed = false;
for (let i = 0; i < SECURITY_CONFIG.ALLOWED_ORIGINS.length; i++) {
const allowedOrigin = SECURITY_CONFIG.ALLOWED_ORIGINS[i];
if (event.origin === allowedOrigin || // 精确匹配
(allowedOrigin.endsWith('*') && event.origin.startsWith(allowedOrigin.slice(0, -1)))) { // 通配符匹配
isOriginAllowed = true;
break;
}
}
// 如果来源不在白名单中,记录警告并返回 false
if (!isOriginAllowed) {
console.warn(`[安全警告] 拒绝来自未授权来源的消息: ${event.origin}`);
return false;
}
return true;
}
/**
* 生成标准化的安全响应对象
*
* 创建统一格式的响应对象,包含成功状态、会话ID、时间戳等信息。
* 支持成功响应和错误响应两种模式。
*
* @param {boolean} success - 操作是否成功
* @param {string} sessionId - 会话标识符,用于追踪请求
* @param {*} [data=null] - 成功时的响应数据,可选
* @param {string} [error=null] - 失败时的错误信息,可选
* @returns {Object} 标准化的响应对象
*
* @example
* // 成功响应
* createSecureResponse(true, 'session123', { result: 'success' })
* // 返回: { type: 'automationResult', success: true, sessionId: 'session123', ... }
*
* // 错误响应
* createSecureResponse(false, 'session123', null, '验证失败')
* // 返回: { type: 'automationResult', success: false, sessionId: 'session123', error: '验证失败', ... }
*/
function createSecureResponse(success, sessionId, data = null, error = null) {
const response = {
type: 'automationResult', // 标识响应类型
success, // 操作结果
sessionId, // 会话ID
timestamp: new Date().toISOString() // ISO格式的时间戳
};
// 成功时包含结果数据
if (data) {
response.results = data;
}
// 失败时包含错误信息
if (error) {
response.error = error;
}
return response;
}
// AUTH校验相关配置
const AUTH_AES_KEY = 'unicorn-rpa-2024-very-secret-key-1234';
const AUTH_HASH = 'cedf55c3046fc919047fcc7b4f4ada8b823195d5e97e1ebda0bff3c11d313f7a';
/**
* 解密auth字段
* @param {string} encrypted - 加密后的auth字符串
* @returns {string} 解密后的明文
*/
function decryptAuth(encrypted) {
try {
const bytes = CryptoJS.AES.decrypt(encrypted, AUTH_AES_KEY);
return bytes.toString(CryptoJS.enc.Utf8);
} catch (e) {
return '';
}
}
/**
* 校验auth字段
* @param {string} auth - 加密后的auth字符串
* @returns {boolean} 是否通过校验
*/
function validateAuth(auth) {
if (!auth) return false;
const decrypted = decryptAuth(auth);
if (!decrypted) return false;
const hash = CryptoJS.SHA256(decrypted).toString();
return hash === AUTH_HASH;
}
/**
* 执行完整的安全验证流程
*
* 对接收到的 postMessage 事件进行安全验证,包括来源域名验证、频率限制检查和动作类型验证。
* 返回验证结果对象,包含验证状态和错误信息。
*
* @param {MessageEvent} event - postMessage 事件对象
* @param {Array} [actions] - 动作数组,用于验证动作类型
* @returns {Object} 验证结果对象
* @returns {boolean} returns.valid - 验证是否通过
* @returns {string} [returns.error] - 验证失败时的错误信息
* @returns {string} [returns.unsupportedType] - 不支持的动作类型(如果有)
*
* @example
* const result = performSecurityValidation(event, actions);
* if (result.valid) {
* // 验证通过,继续处理
* } else {
* // 验证失败,处理错误
* console.error(result.error);
* if (result.unsupportedType) {
* console.error('不支持的动作类型:', result.unsupportedType);
* }
* }
*/
function performSecurityValidation(event, actions = null) {
// 1. 先校验auth字段
const auth = event.data.auth;
if (!validateAuth(auth)) {
return { valid: false, error: 'auth校验失败' };
}
// 2. 来源白名单 - 已关闭
// if (!validateOrigin(event)) {
// return { valid: false, error: '来源验证失败' };
// }
// 3. 频率限制 - 已关闭
// if (!checkRateLimit(event.origin)) {
// return { valid: false, error: '消息发送过于频繁' };
// }
// 4. 动作类型
if (actions) {
const actionValidation = validateActionTypes(actions);
if (!actionValidation.valid) {
return {
valid: false,
error: actionValidation.error,
unsupportedType: actionValidation.unsupportedType
};
}
}
// 5. 所有验证通过
return { valid: true };
}
/**
* 获取安全统计信息
*
* 返回当前的安全状态统计信息,包括活跃的来源数量、配置信息和支持的动作类型。
*
* @returns {Object} 安全统计信息
* @returns {number} returns.activeOrigins - 当前活跃的来源数量
* @returns {number} returns.allowedOrigins - 允许的来源总数
* @returns {number} returns.rateLimitInterval - 频率限制间隔(秒)
* @returns {Array} returns.supportedActionTypes - 支持的动作类型列表
* @returns {number} returns.supportedActionTypesCount - 支持的动作类型数量
*
* @example
* const stats = getSecurityStats();
* console.log(`活跃来源: ${stats.activeOrigins}/${stats.allowedOrigins}`);
* console.log(`频率限制: ${stats.rateLimitInterval}秒`);
* console.log(`支持的动作类型: ${stats.supportedActionTypesCount}种`);
*/
function getSecurityStats() {
return {
activeOrigins: messageHistory.size,
allowedOrigins: SECURITY_CONFIG.ALLOWED_ORIGINS.length,
rateLimitInterval: SECURITY_CONFIG.RATE_LIMIT.INTERVAL / 1000,
supportedActionTypes: SUPPORTED_ACTION_TYPES,
supportedActionTypesCount: SUPPORTED_ACTION_TYPES.length
};
}
/**
* 模块导出
*
* 导出安全模块的公共接口,供其他模块使用。
*
* @exports {Object} 安全模块的公共接口
* @exports {Function} validateOrigin - 来源验证函数
* @exports {Function} checkRateLimit - 频率限制检查函数
* @exports {Function} validateActionTypes - 动作类型验证函数
* @exports {Function} performSecurityValidation - 安全验证流程函数
* @exports {Function} createSecureResponse - 响应生成函数
* @exports {Function} getSecurityStats - 安全统计函数
* @exports {Object} SECURITY_CONFIG - 安全配置对象
* @exports {Array} SUPPORTED_ACTION_TYPES - 支持的动作类型列表
*/
module.exports = {
validateOrigin, // 来源验证函数
checkRateLimit, // 频率限制检查函数
validateActionTypes, // 动作类型验证函数
performSecurityValidation, // 安全验证流程函数
createSecureResponse, // 响应生成函数
getSecurityStats, // 安全统计函数
SECURITY_CONFIG, // 安全配置对象
SUPPORTED_ACTION_TYPES // 支持的动作类型列表
};