从阿里滑块_rand参数谈基于CSS动画特性的参数传递机制
前言
阿里滑块一直在跑231.1版本,最近发现有些接口使用231.1生成的x5sec还是会重复出现滑块,于是找朋友搞了一版最新的,其中他的_rand参数的关键部分用了playwright自动化生成,放到docker中并发有点过于占用内存,就研究了下生成机制。
1. 检测代码
<html><head><style>@keyframes ____9125356020{0%{opacity:0.63}100%{opacity:0.63}}@keyframes ____9125356021{0%{opacity:0.86}100%{opacity:0.23}}@keyframes ____9125356022{0%{opacity:0.63}100%{opacity:0.0}}@keyframes ____9125356023{0%{opacity:0.7}100%{opacity:0.3}}@keyframes ____9125356024{0%{opacity:0.73}100%{opacity:0.92}}@keyframes ____9125356025{0%{opacity:0.13}100%{opacity:0.80}}@keyframes ____9125356026{0%{opacity:0.43}100%{opacity:0.95}}@keyframes ____9125356027{0%{opacity:0.59}100%{opacity:0.56}}@supports (font-style:normal) and ((overflow:visible) or ((not (overflow:visible)))){.____166429906111{color:darkviolet;background:navy;border-color:teal;animation:100ms linear 100ms 1 reverse backwards running ____9125356024}.____166429906126{color:red;background:darkgreen;border-color:red;animation:100ms linear 100ms 1 normal backwards running ____9125356025}.____16642990610{color:gray;background:red;border-color:darkred;animation:100ms linear 100ms 1 reverse backwards running ____9125356027}}@media (max-height:17px){.____16642990610{color:darkmagenta;background:teal;border-color:darkorange;animation:100ms linear 100ms 1 normal backwards running ____9125356022}}@media not all{.____166429906112{color:orange;background:darkred;border-color:white;animation:100ms linear 100ms 1 normal backwards running ____9125356022}.____166429906125{color:aqua;background:darkmagenta;border-color:darkcyan;animation:100ms linear 100ms 1 normal forwards running ____9125356022}.____16642990614{color:cyan;background:darkred;border-color:white;animation:100ms linear 100ms 1 normal backwards running ____9125356026}}@supports (font-style:italic) and ((position:fixed) and ((not (border-width:medium)))){.____166429906120{color:darkgoldenrod;background:darkviolet;border-color:yellow;animation:100ms linear 100ms 1 normal forwards running ____9125356026}.____166429906114{color:darksalmon;background:aqua;border-color:teal;animation:100ms linear 100ms 1 reverse forwards running ____9125356027}}@supports (not (position:absolute)){.____166429906125{color:orange;background:darkgray;border-color:gray;animation:100ms linear 100ms 1 normal forwards running ____9125356021}.____166429906130{color:fuchsia;background:darkorchid;border-color:darkblue;animation:100ms linear 100ms 1 normal forwards running ____9125356026}}@supports (not (display:none)){.____166429906118{color:darkorange;background:lightblue;border-color:yellow;animation:100ms linear 100ms 1 normal forwards running ____9125356025}}@supports (not (text-align:right)){.____16642990614{color:darkgreen;background:gray;border-color:darkgoldenrod;animation:100ms linear 100ms 1 reverse backwards running ____9125356022}.____166429906122{color:blue;background:lime;border-color:fuchsia;animation:100ms linear 100ms 1 normal backwards running ____9125356022}.____16642990618{color:gray;background:purple;border-color:white;animation:100ms linear 100ms 1 normal backwards running ____9125356023}}@supports (not (border-width:10px)) and ((color:rgb(169,162%,14)) and ((not (overflow:visible)))){.____16642990615{color:gray;background:brown;border-color:gray;animation:100ms linear 100ms 1 reverse forwards running ____9125356022}.____166429906112{color:darkcyan;background:green;border-color:yellow;animation:100ms linear 100ms 1 reverse forwards running ____9125356023}}@media not print{.____166429906117{color:purple;background:fuchsia;border-color:aqua;animation:100ms linear 100ms 1 normal backwards running ____9125356022}.____166429906111{color:lightgray;background:darkred;border-color:teal;animation:100ms linear 100ms 1 reverse backwards running ____9125356020}.____166429906118{color:silver;background:gray;border-color:red;animation:100ms linear 100ms 1 reverse forwards running ____9125356020}}@media not print{.____166429906120{color:darkgray;background:darkgoldenrod;border-color:maroon;animation:100ms linear 100ms 1 normal backwards running ____9125356020}.____166429906113{color:blue;background:brown;border-color:darksalmon;animation:100ms linear 100ms 1 normal forwards running ____9125356020}}@supports (border-width:10px) and ((not (border-width:thick))){.____166429906114{color:black;background:darkorchid;border-color:red;animation:100ms linear 100ms 1 reverse backwards running ____9125356024}.____166429906129{color:darksalmon;background:maroon;border-color:darkgoldenrod;animation:100ms linear 100ms 1 reverse backwards running ____9125356026}.____166429906129{color:lime;background:darkgray;border-color:fuchsia;animation:100ms linear 100ms 1 reverse forwards running ____9125356021}}@supports (color:yellow) and ((position:relative) and ((border-width:medium) and ((not (overflow:scroll))))){.____166429906116{color:maroon;background:white;border-color:darkgray;animation:100ms linear 100ms 1 normal backwards running ____9125356020}.____166429906131{color:darkorange;background:aqua;border-color:purple;animation:100ms linear 100ms 1 normal backwards running ____9125356024}}@supports (display:block){.____166429906114{color:lightgray;background:darkred;border-color:yellow;animation:100ms linear 100ms 1 reverse backwards running ____9125356024}.____166429906116{color:gray;background:lime;border-color:darkgreen;animation:100ms linear 100ms 1 normal forwards running ____9125356023}}@supports (position:absolute){.____16642990616{color:black;background:purple;border-color:darkgreen;animation:100ms linear 100ms 1 normal backwards running ____9125356024}.____16642990610{color:darkviolet;background:lime;border-color:navy;animation:100ms linear 100ms 1 reverse forwards running ____9125356024}.____16642990610{color:white;background:lightblue;border-color:black;animation:100ms linear 100ms 1 normal backwards running ____9125356023}}@supports (not (color:rgb(255%,246,181%))) or ((position:sticked)){.____16642990617{color:darksalmon;background:darkmagenta;border-color:red;animation:100ms linear 100ms 1 normal backwards running ____9125356020}.____166429906127{color:red;background:darkcyan;border-color:lightgray;animation:100ms linear 100ms 1 reverse forwards running ____9125356021}}@supports (overflow:visible) and ((not (font-style:oblique))){.____166429906131{color:silver;background:darkviolet;border-color:red;animation:100ms linear 100ms 1 reverse backwards running ____9125356022}}@media (max-height:10407px){.____16642990618{color:blue;background:teal;border-color:purple;animation:100ms linear 100ms 1 normal forwards running ____9125356025}}@supports (border-width:10px) or ((font-style:oblique)){.____16642990611{color:maroon;background:gray;border-color:olive;animation:100ms linear 100ms 1 reverse backwards running ____9125356026}}@supports (not (color:#5D0A0)){.____166429906120{color:black;background:darkviolet;border-color:white;animation:100ms linear 100ms 1 normal backwards running ____9125356023}.____166429906127{color:lime;background:darkorange;border-color:yellow;animation:100ms linear 100ms 1 normal backwards running ____9125356025}}@supports (text-align:left) and ((position:absolute)){.____166429906128{color:darkviolet;background:fuchsia;border-color:gray;animation:100ms linear 100ms 1 reverse backwards running ____9125356024}.____166429906127{color:white;background:purple;border-color:teal;animation:100ms linear 100ms 1 normal backwards running ____9125356026}}@media (min-width:7px){.____166429906121{color:teal;background:maroon;border-color:orange;animation:100ms linear 100ms 1 reverse forwards running ____9125356020}}@media (max-width:7px){.____166429906115{color:purple;background:darkviolet;border-color:teal;animation:100ms linear 100ms 1 normal backwards running ____9125356023}.____166429906123{color:red;background:darkmagenta;border-color:lightgray;animation:100ms linear 100ms 1 normal backwards running ____9125356027}}@supports (position:fixed) or ((not (display:block))){.____16642990618{color:teal;background:fuchsia;border-color:aqua;animation:100ms linear 100ms 1 reverse backwards running ____9125356025}}@media not all{.____166429906114{color:darkred;background:yellow;border-color:black;animation:100ms linear 100ms 1 reverse backwards running ____9125356022}}@media not all{.____166429906110{color:darkblue;background:teal;border-color:darkorchid;animation:100ms linear 100ms 1 reverse backwards running ____9125356027}.____166429906121{color:olive;background:olive;border-color:gray;animation:100ms linear 100ms 1 reverse forwards running ____9125356026}}@supports (not (display:none)) and ((position:fixed) and ((display:table-flex) and ((not (overflow:scroll))))){.____166429906127{color:lightgray;background:olive;border-color:green;animation:100ms linear 100ms 1 reverse forwards running ____9125356022}.____16642990619{color:darksalmon;background:olive;border-color:lightgray;animation:100ms linear 100ms 1 reverse backwards running ____9125356023}}@supports (not (display:none)) and ((not (color:#D8A7))){.____166429906117{color:red;background:maroon;border-color:darkgray;animation:100ms linear 100ms 1 reverse backwards running ____9125356024}}@media (max-width:16047px){.____166429906127{color:orange;background:orange;border-color:brown;animation:100ms linear 100ms 1 reverse forwards running ____9125356020}.____166429906113{color:silver;background:darkviolet;border-color:brown;animation:100ms linear 100ms 1 normal backwards running ____9125356022}.____166429906131{color:darkred;background:darkviolet;border-color:darkgoldenrod;animation:100ms linear 100ms 1 reverse backwards running ____9125356021}}@supports (overflow:hidden) and ((color:rgb(141,222,102))){.____166429906120{color:darksalmon;background:brown;border-color:olive;animation:100ms linear 100ms 1 reverse forwards running ____9125356022}}@media (min-width:12676px){.____16642990616{color:white;background:maroon;border-color:purple;animation:100ms linear 100ms 1 reverse forwards running ____9125356023}}@supports (not (position:relative)) and ((overflow:hidden) or ((display:block))){.____166429906112{color:darkred;background:cyan;border-color:navy;animation:100ms linear 100ms 1 normal forwards running ____9125356026}}@supports (not (border-width:thick)) and ((not (text-align:left))){.____16642990618{color:darkviolet;background:navy;border-color:navy;animation:100ms linear 100ms 1 reverse backwards running ____9125356026}.____16642990614{color:yellow;background:darkgreen;border-color:yellow;animation:100ms linear 100ms 1 reverse forwards running ____9125356020}}</style></head><body></body><script>
var lf = []
var A = document.body.appendChild(document.createElement("div"));
A.innerHTML = '<span class="____16642990610" data-x22="10" data-x5="2"></span><span class="____16642990611" data-x22="10" data-x5="7" data-x17="0" data-x29="5" data-x25="0"></span><span class="____16642990612" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="7" data-x6="10"></span><span class="____16642990613" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="7" data-x6="3" data-x21="8" data-x28="12" data-x1="7"></span><span class="____16642990614" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="7" data-x6="3" data-x21="8" data-x28="12" data-x1="7" data-x20="8" data-x10="2" data-x19="5"></span><span class="____16642990615" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="2" data-x6="7" data-x21="8" data-x28="12" data-x1="7" data-x20="8" data-x10="2" data-x19="5"></span><span class="____16642990616" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="2" data-x6="7" data-x21="8" data-x28="3" data-x1="7" data-x20="0" data-x10="2" data-x19="5" data-x18="9"></span><span class="____16642990617" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="2" data-x6="7" data-x21="8" data-x28="3" data-x1="7" data-x20="0" data-x10="2" data-x19="5" data-x18="9" data-x27="13" data-x0="4" data-x12="12"></span><span class="____16642990618" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="2" data-x6="7" data-x21="8" data-x28="3" data-x1="7" data-x20="0" data-x10="2" data-x19="5" data-x18="9" data-x27="13" data-x0="4" data-x12="12" data-x9="6" data-x3="0" data-x15="15"></span><span class="____16642990619" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="2" data-x6="7" data-x21="8" data-x28="4" data-x1="7" data-x20="0" data-x10="7" data-x19="5" data-x18="9" data-x27="12" data-x0="4" data-x12="12" data-x9="6" data-x3="0" data-x15="15" data-x16="1"></span><span class="____166429906110" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="0" data-x6="7" data-x21="8" data-x28="4" data-x1="0" data-x20="0" data-x10="7" data-x19="12" data-x18="9" data-x27="12" data-x0="4" data-x12="12" data-x9="6" data-x3="0" data-x15="15" data-x16="1" data-x11="3"></span><span class="____166429906111" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="0" data-x6="7" data-x21="8" data-x28="1" data-x1="14" data-x20="0" data-x10="7" data-x19="12" data-x18="9" data-x27="2" data-x0="4" data-x12="12" data-x9="6" data-x3="0" data-x15="15" data-x16="1" data-x11="3"></span><span class="____166429906112" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="0" data-x6="7" data-x21="8" data-x28="5" data-x1="14" data-x20="0" data-x10="7" data-x19="12" data-x18="9" data-x27="2" data-x0="4" data-x12="12" data-x9="6" data-x3="0" data-x15="15" data-x16="1" data-x11="3" data-x31="5" data-x8="8" data-x23="15"></span><span class="____166429906113" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="0" data-x6="0" data-x21="8" data-x28="5" data-x1="14" data-x20="0" data-x10="7" data-x19="12" data-x18="9" data-x27="2" data-x0="4" data-x12="12" data-x9="6" data-x3="0" data-x15="15" data-x16="1" data-x11="3" data-x31="5" data-x8="8" data-x23="15" data-x30="5"></span><span class="____166429906114" data-x22="4" data-x5="7" data-x17="0" data-x29="5" data-x25="0" data-x6="0" data-x21="8" data-x28="5" data-x1="14" data-x20="0" data-x10="14" data-x19="12" data-x18="9" data-x27="2" data-x0="4" data-x12="3" data-x9="6" data-x3="1" data-x15="15" data-x16="1" data-x11="3" data-x31="5" data-x8="8" data-x23="15" data-x30="5" data-x7="4"></span><span class="____166429906115" data-x22="3" data-x5="7" data-x17="0" data-x29="5" data-x25="0" data-x6="0" data-x21="8" data-x28="5" data-x1="14" data-x20="0" data-x10="14" data-x19="10" data-x18="9" data-x27="2" data-x0="4" data-x12="15" data-x9="6" data-x3="1" data-x15="15" data-x16="1" data-x11="3" data-x31="5" data-x8="8" data-x23="15" data-x30="5" data-x7="4" data-x2="2"></span>',
A.addEventListener("animationend", (function (r) {
var n = r.target
, i = getComputedStyle(n)
lf.push({
"animationName": r.animationName,
"target": {
"style": {
"opacity": (+i['opacity']).toString(),
"color": i['color'].toString(),
"background-color": i['background-color'],
"border-color": i['border-color'],
}
},
})
}))</script></html>
dynamicHtml = wasmVmFunc(timeStamp, randomStr, NCTOKENSTR);
以上为原js参数生成的核心部分,在虚拟机中根据时间戳、随机数以及NCTOKENSTR生成了一大串控制动画的CSS与各种类名的DOM,然后播放动画并在动画播放完毕后触发animationend事件,获取此时的元素类名、文字色、背景色、边框色等。注意,此时数据已被vm隐写到了animationend元素的类名、color等属性中。
在之前的文章 利用浏览器特性进行数据加密与传输的N种方式 中聊到过通过CSS动画隐写与编码传输可以隐秘传递自己的数据,这里就是类似的原理:
- 使用大量
@supports与@media条件块控制特定 CSS 规则是否生效。 - 将这些规则绑定到定义好的
@keyframes动画,动画只修改opacity。 - 浏览器执行动画后,在动画结束时用 JavaScript 读取元素的
opacity及颜色属性。 - 将读取到的终态样式序列化并嵌入
_rand。
由于动画定义、触发时机及采集方式均由js虚拟机控制,此机制既能验证客户端是否完整执行了CSS动画与条件筛选链路,进而判断运行环境的真实性,又能隐秘的进行数据传输。
自动化通过:
const { chromium } = require('playwright');
async function executeAnimationAndGetResults() {
const browser = await chromium.launch();
const page = await browser.newPage();
// 设置HTML内容
await page.setContent(dynamicHtml);
// 等待所有动画完成并收集结果
const results = await page.evaluate(async () => {
return new Promise((resolve) => {
// 设置检查间隔
const checkInterval = setInterval(() => {
if (window.lf && window.lf.length > 0) {
clearInterval(checkInterval);
resolve([...window.lf]); // 返回副本
}
}, 50);
setTimeout(() => {
clearInterval(checkInterval);
resolve(window.lf || []); // 确保返回数组
}, 5000); // 10秒超时
});
});
await browser.close();
console.log(JSON.stringify(results, null, 2));
}
executeAnimationAndGetResults()
2. 还原思路
在无浏览器环境下复写该过程,主要是以下思路:
- 剥离样式标签:从 HTML 中提取纯 CSS 内容
- 过滤条件规则:根据环境配置过滤
@media和@supports规则 - 解析关键帧:提取所有
@keyframes动画定义 - 解析类规则:提取所有 CSS 类的样式声明
- 构建动画对象:为每个类构建完整的动画配置
- 模拟终态:计算动画结束时的样式状态
目标是输入 HTML 与 CSS,即可输出与真实浏览器一致的结果。不需要实现过多的兼容,只要能跑阿里就可以。
3. 代码实现
3.1 关键帧取样逻辑
sampleKeyframesOpacity 函数用于根据动画关键帧和时间偏移量计算透明度值:
function sampleKeyframesOpacity(frames, offset) {
// 边界情况处理:没有关键帧时返回默认透明度 1
if (!frames || frames.length === 0) return 1;
// offset 在首帧之前,返回首帧透明度
if (offset <= frames[0].offset) return frames[0].opacity;
// offset 在末帧之后,返回末帧透明度
if (offset >= frames[frames.length - 1].offset) return frames[frames.length - 1].opacity;
// 在中间区间:找到对应的关键帧区间进行线性插值
for (let i = 0; i < frames.length - 1; i++) {
const a = frames[i], b = frames[i + 1];
if (offset >= a.offset && offset <= b.offset) {
// 线性插值公式:当前值 = 起始值 + (结束值 - 起始值) * 插值比例
const t = (offset - a.offset) / (b.offset - a.offset);
return a.opacity + (b.opacity - a.opacity) * t;
}
}
return 1;
}
3.2 条件规则过滤函数
3.2.1 @media 规则过滤
function evalMediaCondition(cond) {
const norm = cond.trim().toLowerCase();
// 特殊情况处理
if (norm === 'not all') return false; // 明确排除所有媒体类型
if (norm === 'all') return true; // 匹配所有媒体类型
if (norm === 'not print') return true; // 排除打印媒体(通常在屏幕环境中为真)
// 宽度条件匹配
const mw = /max-width\s*:\s*(\d+)px/i.exec(cond);
if (mw) return ENV.width <= +mw[1]; // 最大宽度限制
const mnw = /min-width\s*:\s*(\d+)px/i.exec(cond);
if (mnw) return ENV.width >= +mnw[1]; // 最小宽度限制
// 高度条件匹配
const mh = /max-height\s*:\s*(\d+)px/i.exec(cond);
if (mh) return ENV.height <= +mh[1]; // 最大高度限制
const mnh = /min-height\s*:\s*(\d+)px/i.exec(cond);
if (mnh) return ENV.height >= +mnh[1]; // 最小高度限制
// 未识别的条件默认通过(宽松策略)
return true;
}
3.2.2 @supports 规则过滤
function evalSupportsCondition(condRaw) {
let cond = condRaw.trim();
// 去除外层多余括号的辅助函数
const strip = s => {
s = s.trim();
while (s.startsWith('(') && s.endsWith(')')) {
let depth = 0, ok = true;
// 检查括号是否完全匹配且包围整个表达式
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') depth++;
else if (s[i] === ')') depth--;
// 如果在中间位置深度为0,说明括号不是完全包围的
if (depth === 0 && i < s.length - 1) {
ok = false;
break;
}
}
if (!ok) break;
s = s.slice(1, -1).trim(); // 去除外层括号
}
return s;
};
cond = strip(cond);
// 处理 NOT 逻辑
if (/^not\b/i.test(cond)) {
const inner = strip(cond.replace(/^not\b/i, ''));
return !evalSupportsCondition(inner); // 递归处理内部条件并取反
}
// 顶层逻辑运算符分割函数
const splitTopLevel = (s, kw) => {
let depth = 0, out = [''];
const KW = kw.toLowerCase();
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (ch === '(') depth++;
else if (ch === ')') depth--;
out[out.length - 1] += ch;
// 只在顶层(depth=0)时分割
if (depth === 0 && s.slice(i + 1).toLowerCase().startsWith(' ' + KW + ' ')) {
out[out.length - 1] = strip(out[out.length - 1]);
out.push('');
i += KW.length + 1; // 跳过关键字
}
}
out[out.length - 1] = strip(out[out.length - 1]);
return out.length > 1 ? out : null;
};
// 处理 AND 逻辑:所有条件都必须为真
const andParts = splitTopLevel(cond, 'and');
if (andParts) return andParts.every(evalSupportsCondition);
// 处理 OR 逻辑:任一条件为真即可
const orParts = splitTopLevel(cond, 'or');
if (orParts) return orParts.some(evalSupportsCondition);
// 处理基本特性检测
const m = /\(?\s*([a-zA-Z-]+:[^)]+?)\s*\)?$/.exec(cond);
if (m) {
const feature = m[1].trim(); // 提取 "属性:值" 格式的特性
return !!ENV.supports[feature]; // 检查环境是否支持此特性
}
return false; // 默认不支持未知特性
}
3.3 CSS 解析函数
3.3.1 声明块解析
function parseDecls(block) {
const out = {};
// 匹配 "属性: 值;" 格式的 CSS 声明
const re = /([a-zA-Z-]+)\s*:\s*([^;]+);?/g;
let m;
while ((m = re.exec(block))) {
// 去除属性名和值的前后空白
out[m[1].trim()] = m[2].trim();
}
return out;
}
3.3.2 类规则解析
function parseClassRules(css) {
const rules = [];
// 匹配 ".类名 { 声明块 }" 格式
const re = /\.([A-Za-z0-9_-]+)\s*\{([^{}]*)\}/g;
let m;
while ((m = re.exec(css))) {
rules.push({
selector: '.' + m[1], // 完整的选择器
decls: parseDecls(m[2] || '') // 解析的声明对象
});
}
return rules;
}
3.3.3 关键帧解析
function parseKeyframesOpacityMap(css) {
const map = Object.create(null); // 创建无原型的纯对象
const kw = '@keyframes';
let i = 0;
while (i < css.length) {
const p = css.indexOf(kw, i);
if (p === -1) break; // 没有更多关键帧定义
// 跳过 @keyframes 关键字
let j = p + kw.length;
while (j < css.length && /\s/.test(css[j])) j++; // 跳过空白
// 提取动画名称
let nameStart = j;
while (j < css.length && !/\s|\{/.test(css[j])) j++;
const name = css.slice(nameStart, j).trim();
// 找到开始的大括号
while (j < css.length && css[j] !== '{') j++;
// 找到匹配的结束大括号(处理嵌套)
let k = j + 1, depth = 1;
while (k < css.length && depth > 0) {
if (css[k] === '{') depth++;
else if (css[k] === '}') depth--;
k++;
}
// 提取关键帧内容
const body = css.slice(j + 1, k - 1);
const frames = [];
// 解析每个关键帧步骤:如 "50% { opacity: 0.5 }"
const stepRe = /(\d+)%\s*\{([^}]*)\}/g;
let m;
while ((m = stepRe.exec(body))) {
const offset = Number(m[1]) / 100; // 转换为 0-1 的比例
const decls = m[2];
// 提取 opacity 值
const opm = /opacity\s*:\s*([0-9]*\.?[0-9]+)/i.exec(decls);
if (opm) {
frames.push({
offset,
opacity: Number(opm[1])
});
}
}
// 按偏移量排序确保时间顺序正确
frames.sort((a, b) => a.offset - b.offset);
map[name] = frames;
i = k;
}
return map;
}
3.4 动画配置解析
3.4.1 动画简写属性解析
function parseAnimationShorthand(s) {
const tokens = s.trim().split(/\s+/).filter(Boolean);
// 默认值设置
let duration = 0, delay = 0, iterations = 1;
let direction = 'normal', fillMode = 'none', timingFunction = 'linear';
let name = tokens[tokens.length - 1]; // 动画名称通常是最后一个标记
// 跳过播放状态关键字
const skip = new Set(['running', 'paused']);
let seenTime = 0; // 时间值计数器
// 遍历除最后一个标记外的所有标记
for (let i = 0; i < tokens.length - 1; i++) {
const t = tokens[i];
if (skip.has(t)) continue;
// 解析时间值(duration 和 delay)
if (t.endsWith('ms') || t.endsWith('s')) {
const val = t.endsWith('ms') ? parseFloat(t) : parseFloat(t) * 1000;
if (seenTime === 0) {
duration = val;
seenTime = 1;
} else {
delay = val;
}
}
// 解析迭代次数
else if (/^\d+$/.test(t)) {
iterations = +t;
}
// 解析动画方向
else if (['normal', 'reverse', 'alternate', 'alternate-reverse'].includes(t)) {
direction = t;
}
// 解析填充模式
else if (['none', 'forwards', 'backwards', 'both'].includes(t)) {
fillMode = t;
}
// 解析时间函数
else if (['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'].includes(t) ||
t.startsWith('cubic-bezier')) {
timingFunction = t;
}
}
return {
name, duration, delay, iterations,
direction, fillMode, timingFunction
};
}
3.4.2 动画结束状态计算
function finalOffsetAtEnd(anim) {
const it = anim.iterations === Infinity ? 1 : anim.iterations;
switch (anim.direction) {
case 'normal':
return 1; // 正向播放,结束在 100%
case 'reverse':
return 0; // 反向播放,结束在 0%
case 'alternate':
// 交替播放:奇数次结束在 100%,偶数次结束在 0%
return (it % 2 === 1) ? 1 : 0;
case 'alternate-reverse':
// 反向交替:奇数次结束在 0%,偶数次结束在 100%
return (it % 2 === 1) ? 0 : 1;
default:
return 1;
}
}
3.5 样式构建与动画模拟
3.5.1 类样式和动画构建
function buildStyleAndAnimForClass(cls, rules, kfMap) {
const sel = '.' + cls;
let tick = 0; // 用于记录属性出现的顺序,实现后写入覆盖逻辑
// 状态追踪对象,记录每个属性的值和出现时间
const state = {
color: { v: undefined, t: -1 }, // 文字颜色
bgc: { v: undefined, t: -1 }, // 背景颜色
bdc: { v: undefined, t: -1 }, // 边框颜色
anim: { v: undefined, t: -1 }, // 动画属性
};
// 遍历所有规则,收集目标类的样式
for (const r of rules) {
if (r.selector !== sel) continue;
// 记录每个属性的最新值和时间戳
if (r.decls['color']) {
state.color.v = r.decls['color'];
state.color.t = ++tick;
}
if (r.decls['background']) {
state.bgc.v = r.decls['background'];
state.bgc.t = ++tick;
}
if (r.decls['background-color']) {
state.bgc.v = r.decls['background-color'];
state.bgc.t = ++tick;
}
if (r.decls['border-color']) {
state.bdc.v = r.decls['border-color'];
state.bdc.t = ++tick;
}
if (r.decls['animation']) {
state.anim.v = r.decls['animation'];
state.anim.t = ++tick;
}
}
// 没有动画属性则跳过
if (!state.anim.v) return null;
// 解析动画配置
const a = parseAnimationShorthand(state.anim.v);
const frames = kfMap[a.name];
// 没有对应的关键帧定义则跳过
if (!frames || !frames.length) return null;
// 返回完整的动画对象
return {
name: a.name,
duration: a.duration,
delay: a.delay,
iterations: a.iterations,
direction: a.direction,
fillMode: a.fillMode,
timingFunction: a.timingFunction,
baseStyle: {
color: state.color.v,
'background-color': state.bgc.v,
'border-color': state.bdc.v
},
properties: { opacity: frames }
};
}
3.5.2 动画结束状态模拟
function simulateAnimationEnd(anim) {
let opacity;
// 根据填充模式决定最终透明度
if (anim.fillMode === 'forwards' || anim.fillMode === 'both') {
// forwards 或 both 模式:保持动画结束时的状态
const endOffset = finalOffsetAtEnd(anim);
opacity = sampleKeyframesOpacity(anim.properties.opacity, endOffset);
} else {
// none 或 backwards 模式:回到初始状态
opacity = 1;
}
// 返回计算后的样式对象
return {
opacity: String(opacity),
color: toRGBString(anim.baseStyle.color),
'background-color': toRGBString(anim.baseStyle['background-color']),
'border-color': toRGBString(anim.baseStyle['border-color'])
};
}
3.6 辅助函数
3.6.1 颜色处理
// 颜色关键字到 RGB 值的映射表
const COLOR_KEYWORDS = {
black: [0, 0, 0], white: [255, 255, 255],
gray: [128, 128, 128], lightgray: [211, 211, 211],
silver: [192, 192, 192],
red: [255, 0, 0], maroon: [128, 0, 0],
darkred: [139, 0, 0], darkorange: [255, 140, 0],
orange: [255, 165, 0],
// ... 更多颜色定义
};
function toRGBString(value) {
if (!value) return value;
// 已经是 RGB 格式则规范化空格
if (/^rgb\(/i.test(value)) {
return value.replace(/\s+/g, '');
}
// 转换颜色关键字为 RGB 格式
const arr = COLOR_KEYWORDS[(value || '').toLowerCase()];
if (arr) {
return `rgb(${arr[0]}, ${arr[1]}, ${arr[2]})`;
}
// 未知颜色保持原样
return value;
}
3.6.2 HTML 类名提取
function extractClassListFromHtmlInner(inner) {
const classes = [];
const re = /class="([^"]+)"/g;
let m;
while ((m = re.exec(inner))) {
// 拆分多个类名并保持 DOM 顺序
classes.push(...m[1].trim().split(/\s+/));
}
return classes;
}
3.7 主流程函数
function main() {
// 1. 预处理:去除 HTML 标签获取纯 CSS
const rawCss = stripStyleTags(cssText);
// 2. 条件过滤:根据环境过滤 @media 和 @supports 规则
const filteredCss = extractActiveCSS(rawCss);
// 3. 解析关键帧:构建动画名称到关键帧的映射
const kfMap = parseKeyframesOpacityMap(filteredCss);
// 4. 解析类规则:提取所有 CSS 类的样式规则
const rules = parseClassRules(filteredCss);
// 5. 提取类名:按 DOM 顺序获取所有类名
const classesInDomOrder = extractClassListFromHtmlInner(htmlInner);
// 调试信息输出
console.log('[DEBUG] rules长度: ', rules.length);
console.log('[DEBUG] keyframes: ', Object.keys(kfMap));
console.log('[DEBUG] dom顺序: ', classesInDomOrder);
// 6. 构建候选动画:为每个类构建动画配置
const candidates = [];
for (const cls of classesInDomOrder) {
const anim = buildStyleAndAnimForClass(cls, rules, kfMap);
if (anim) {
candidates.push({ cls, anim });
}
}
console.log('[DEBUG] 候选类: ', candidates.map(x => x.cls));
console.log('[DEBUG] 候选动画: ', candidates.map(x => x.anim.name));
// 7. 过滤有效动画:只保留真正会触发 animationend 事件的动画
const effective = candidates.filter(c =>
c.anim.iterations > 0 &&
c.anim.properties.opacity.length > 0
);
console.log('[DEBUG] 有效类: ', effective.map(x => x.cls));
// 8. 生成最终结果:模拟每个动画结束时的样式状态
const result = [];
for (const { cls, anim } of effective) {
const style = simulateAnimationEnd(anim);
result.push({
animationName: anim.name,
target: { style }
});
}
// 输出 JSON 格式的结果
console.log(JSON.stringify(result, null, 2));
}
3.8 环境配置
// 模拟浏览器环境的配置对象
const ENV = {
width: 1920, // 视口宽度
height: 1080, // 视口高度
supports: { // CSS 特性支持情况
'font-style:normal': true,
'overflow:visible': true,
'overflow:hidden': true,
'position:absolute': true,
'position:fixed': true,
'position:relative': true,
'display:block': true,
'border-width:10px': true,
'border-width:thick': true,
'color:yellow': true,
'text-align:left': true,
'text-align:right': true,
//...更多支持的特性
// 不支持的特性
'position:sticked': false, // 非标准值
'display:table-flex': false, // 非标准值
'color:#5D0A0': false, // 无效颜色格式
'color:rgb(169,162%,14)': false // 无效 RGB 格式
//...更多不支持的特性
}
};
其中:
width和height用于@media查询的尺寸条件判断supports对象模拟浏览器对 CSS 特性的支持情况
经验证和浏览器渲染结果完全一致。
4. 总结
逆向真tm难,DrissionPage启动。
本文是原创文章,采用 CC 4.0 协议,完整转载请注明来自 http://www.1997.pro/
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果