某音网页端 X-Bogus 参数

逆向目标
目标:某音网页端用户信息接口 X-Bogus 参数

接口:aHR0cHM6Ly93d3cuZG91eWluLmNvbS9hd2VtZS92MS93ZWIvdXNlci9wcm9maWxlL290aGVyLw==

什么是 JSVMP?
JSVMP 全称 Virtual Machine based code Protection for JavaScript,即 JS 代码虚拟化保护方案。

JSVMP 的概念最早应该是由西北大学2015级硕士研究生匡开圆,在其2018年的学位论文中提出的,论文标题为:《基于 WebAssembly 的 JavaScript 代码虚拟化保护方法研究与实现》,同年还申请了国家专利,专利名称:《一种基于前端字节码技术的 JavaScript 虚拟化保护方法》,网上可以直接搜到.
抓包情况

随便来到某个博主主页,抓包后搜索可发现一个接口,返回的是 JSON 数据,里面包含了博主某音号,认证信息、签名,关注、粉丝、获赞等,请求 Query String Parameters 里包含了一个 X-Bogus 参数,每次请求会改变,此外还有 sec_user_id 是博主主页 URL 后面那一串,webid 直接请求主页返回内容里就有,msToken 与 cookie 有关,清除 cookie 访问,就没这个参数了,实测该接口不验证 webid 和 msToken,直接置空即可。

逆向分析
这条请求是 XHR 请求,所以直接下个 XHR 断点,当 URL 中包含 X-Bogus 参数时就断下:

往前跟栈,来到一个叫 webmssdk.js 的 JS 文件,这里就是生成参数的主要 JS 逻辑了,也就是 JSVMP,整体上做了一个混淆,这里可以使用 AST 来解混淆,K哥以前同样也写过 AST 的文章,这里还原混淆不是重点,咱们直接使用 V 佬的插件 v_jstools[3] 来还原:

还原后使用浏览器的 Overrides 替换功能将 webmssdk.js 替换掉,往上跟栈,如下图所示,到 W 这里就已经生成了 X-Bogus 了,this.openArgs[1] 就是携带了 X-Bogus 的完整 URL,仔细观察这段代码,有很多三元表达式,当 M 的值为 15 时,就会走到这段逻辑,U 的值生成之后,有一个 S[C] = U 的操作。

再往上看代码,S 是一个数组,单步调试的话会发现代码会一直走这个 if-else 的逻辑,几乎每一步都有 S 数组的参与,不断往里面增删改查值,for 循环里面的 I 值,决定着后续 if 语句的走向,这里也就是插桩的关键所在,如下图所示:

插桩分析
大的 for 循环和 if-else 逻辑有两个地方,为了保证最后的日志更加详细完整,在这两个地方都下个日志断点(右键 Add logpoint),断点内容为:

"位置 1", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {if (value == window) {return undefined} return value})"位置 2", "索引I", I, "索引A", A, "值S: ", JSON.stringify(S, function(key, value) {if (value == window) {return undefined} return value})

插桩输出 S 的时候为什么要写这么长一串呢?首先 JSON.stringify() 方法的作用是将 JavaScript 值转换为 JSON 字符串,基础语法是 JSON.stringify(value[, replacer [, space]]),如果不将其转换成 JSON,那么 S 的值,输出可能是这样的:[empty, Array(26), 1, Array(0)],你看不到 Array 数组里面具体的值,该方法有个可选参数 replacer,如果 replacer 为函数,则 JSON.stringify 将调用该函数,并传入每个成员的键和值,在函数中可以对成员进行处理,最后返回处理后的值,如果此函数返回 undefined,则排除该成员,举个例子:

var obj1 = {key1: 'value1', key2: 'value2'}function changeValue(key, value) { if (value == 'value2') { return '公众号:K哥爬虫' } return value}var obj2 = JSON.stringify(obj1, changeValue)console.log(obj2)// 输出:{"key1":"value1","key2":"公众号:K哥爬虫"}
上面的代码中 JSON.stringify 传入了一个函数,当 value 为 value2 的时候就将其替换成字符串 公众号:K哥爬虫,接下来我们演示一下当 value 为 window 时,会发生什么:

根据报错我们可以看到这里由于循环引用导致异常,要知道在插桩的时候,如果插桩内容有报错,就会导致不能正常输出日志,这样就会缺失一部分日志,这种情况我们就可以加个函数处理一下,让 value 为 window 的时候,JSON 处理的时候函数返回 undefined,排除该成员,其他成员正常输出,如下图所示:

以上就是日志断点为什么要这样写的原因,下好日志断点后,注意前面我们下的 XHR 断点不要取消,然后刷新网页,控制台就开始打印日志了,因为有很多 XHR 请求都包含了 X-Bogus,如果你 XHR 断点取消了,日志就会一直打印直到卡死。日志输出完毕后,大约有8千多条,搜索就能看到最后一条日志 X-Bogus 已经生成了:

28个字符生成逻辑
直接在打印的日志页面右键 save as..,将日志导出到本地进行分析。X-Bogus 由28个字符组成,现在要做的就是看 DFSzswVOAATANH89SMHZqF9WX7n6 这28个字符是怎么来的,在日志里搜索这个字符串,找到第一次出现的地方,观察一下可以发现,他是逐个字符依次生成的,如下图红框所示:

在上图中,第8511行,X-Bogus 字符串的下一个元素是 null,到了第8512行,就生成数字6了,那么在这两步之间就是数字6的生成逻辑,这个时候我们看第8511行的日志断点是 位置 2 索引I 16 索引A 738,那么我们回到原网页,在位置2,下一个条件断点(右键 Add conditional breakpoint),当 I == 16 && A == 738 && S[7] && S[7] == 21 时就断下。之所以要加 S[7] 是因为 索引I 16 索引A 738 的位置有很多,在日志里搜一下大概有40多个,多加个限制条件就可以缩小范围,当然有可能加了多个条件仍然有多个位置都满足,这就需要你细心观察了,通过断点断下的时候看看控制台前面输出的日志来判断是不是我们想要的位置。这也是一个小细节,一定要找准位置,千万别搞混了。(提示一下,像我这样下断点的话,一般情况下会断下两次,第二次是满足要求的)

(注意:本文描述的日志的多少行、断点的具体位置、变量的具体值,可能会有所变化,以你的实际情况为准,但思路是一样的)

刷新网页,断下之后开始单步跟,来到下图所示的地方:

到这里之后,就不要下一步了,再下一步有可能整个语句就执行完毕了,其中的细节你看不到,所以这里我们在控制台挨个输入看看:

可以看到实际上的逻辑就是返回指定位置的字符,y 的值就是 S[5],m 的值就是 S[4],经过多次调试发现 m 的值是固定的,M 就是 charAt() 方法,我们再看看我们本地的日志,S[5] 的值为 [20],charAt() 取值出来就是6,逻辑完全正确。

现在我们还需要知道这个20是怎么来的,继续往上看,找到20第一次出现的地方,在第8510行,那么我们就要使其在上一步断下,也就是第8509行,如下图所示:

第8509行的索引信息为 位置 2 索引I 47 索引A 730,同样的下条件断点观察怎么生成的:

可以看到逻辑是 S[5] & S[6],再看我们本地 S[5] = 5647508、S[6] = 63,5647508 & 63 = 20,逻辑正确,20就是这么来的。接下来又开始找 5647508 和 63 是怎么生成的,同样在生成的上一步,也就是8508行下个条件断点,这行的索引为 位置 2 索引I 72 索引A 726。

可以看到 63 是直接 q[A] 生成的,q 是一个大数组,A 就是索引为 726,q 这个大数组怎么来的先不用管,而 5647508 这个大数字,搜索一下,发现有很多,咱们也先放着,到这里咱们可以总结一下最后一个字符的生成步骤如下:

short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="q[726] = 635647508 & 63 = 20short_str.charAt(20) = '6'
然后接日志着往上看,看倒数第二个字母是怎么来的,方法也和前面演示的一样,不断往前下条件断点,这里就不再逐步演示了,当你找完四个数字后,就可以开始看 5647508 这个大数字怎么来的了,搜索这个数字,同样的找到第一次出现的地方,在其前一步下条件断点,步骤捋出来会发现有一个乱码字符串经过 charCodeAt() 操作,再加上一些位运算得到的,乱码字符串类似下图所示:

至于这个乱码字符串怎么来的,我们后面再讲,到这里先总结一下,首先我们的 X-Bogus = DFSz swVO AATA NH89 SMHZ qF9W X7n6,将其看成每四个为一组,之所以这么分组,是因为你经过分析后会发现,每一组的每一个字符生成流程都是一样的,这里以最后两组为例,流程大致如下:

short_str = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="X-Bogus = DFSz swVO AATA NH89 SMHZ qF9W X7n6============== 第6组【qF9W】=============="\u0002-%.*yê^s6V,".charCodeAt(15) = 158q[342] = 16158 << 16 = 10354688"\u0002-%.*yê^s6V,".charCodeAt(16) = 253q[408] = 8253 << 8 = 6476810354688 | 64768 = 10419456"\u0002-%.*yê^s6V,".charCodeAt(17) = 156156 | 10419456 = 10419612q[520] = 1651507210419612 & 16515072 = 10223616q[532] = 1810223616 >> 18 = 39short_str.charAt(39) = 'q'q[590]= 25804810419612 & 258048 = 192512q[602] = 12192512 >> 12 = 47short_str.charAt(47) = 'F'q[660] = 403210419612 & 4032 = 3456q[668] = 63456 >> 6 = 54short_str.charAt(54) = '9'q[726] = 6310419612 & 63 = 28short_str.charAt(28) = 'W'============== 第7组【X7n6】=============="\u0002-%.*yê^s6V,".charCodeAt(18) = 86q[342] = 1686 << 16 = 5636096"\u0002-%.*yê^s6V,".charCodeAt(19) = 44q[408] = 844 << 8 = 112645636096 | 11264 = 5647360"\u0002-%.*yê^s6V,".charCodeAt(20) = 148148 | 5647360 = 5647508q[520] = 165150725647508 & 16515072 = 5505024q[532] = 185505024 >> 18 = 21short_str.charAt(21) = 'X'q[590] = 2580485647508 & 258048 = 139264q[602] = 12139264 >> 12 = 34short_str.charAt(34) = '7'q[660] = 40325647508 & 4032 = 3200q[668] = 63200 >> 6 = 50short_str.charAt(50) = 'n'q[726] = 635647508 & 63 = 20short_str.charAt(20) = '6'
将流程对比一下就可以发现,每个步骤 q 里面的取值都是一样的,这个可以直接写死,不同之处就在于最开始的 charCodeAt() 操作,也就是返回乱码字符串指定位置字符的 Unicode 编码,第7组依次是 18、19、20,第6组依次是15、16、17,以此类推,第1组刚好是0、1、2,如下图所示:

每一组的逻辑都是一样的,我们就可以写个通用方法,依次生成七组字符串,最后拼接成完整的 X-Bogus,代码如下:(乱码字符串的生成后文会讲)

function getXBogus(originalString){ // 生成乱码字符串 var garbledString = getGarbledString(originalString); var XBogus = ""; // 依次生成七组字符串 for (var i = 0; i <= 20; i += 3) { var charCodeAtNum0 = garbledString.charCodeAt(i); var charCodeAtNum1 = garbledString.charCodeAt(i + 1); var charCodeAtNum2 = garbledString.charCodeAt(i + 2); var baseNum = charCodeAtNum2 | charCodeAtNum1 << 8 | charCodeAtNum0 << 16; // 依次生成四个字符 var str1 = short_str[(baseNum & 16515072) >> 18]; var str2 = short_str[(baseNum & 258048) >> 12]; var str3 = short_str[(baseNum & 4032) >> 6]; var str4 = short_str[baseNum & 63]; XBogus += str1 + str2 + str3 + str4; } return XBogus;}
乱码字符串生成逻辑
在进行下一步之前,我们要注意两点:

文章演示有些变量前后不对应,因为每次插桩的值都是会变的,看流程就行了,流程是正确的;

我们日志输出是经过 JSON.stringify 处理了的,有些步骤是向某个函数传入乱码字符串进行处理,你会发现处理后的结果和日志不一致,这是正常的。

乱码字符串的生成相对来说稍微复杂一点,但思路仍然一样,这里就不一一截图展示了,直接用日志描述一下关键步骤,注意以下日志是正向的步骤,就不逆着推了,建议自己先逆着把流程走一走,再来看这个步骤就看得懂了。

Step1:首先对 URL 后面的参数,也就是 Query String Parameters 进行两次 MD5、两次转 Uint8Array 处理,最后得到的 Uint8Array 对象在后面的步骤中用得到,步骤如下:

位置 1 索引I 4 索引A 134:将 URL 后面的参数进行 MD5 加密得到字符串位置 1 索引I 16 索引A 460:将上一步的字符串转换为 Uint8Array 对象位置 1 索引I 4 索引A 134:将上一步的 Uint8Array 对象进行 MD5 加密,得到字符串位置 1 索引I 29 索引A 472:将上一步的字符串转换为 Uint8Array 对象
上述步骤中,我们将最终得到的结果命名为 uint8Array,关键代码实现如下:

var md5 = require("md5");// 字符串转换为 Uint8Array 对象,缺失的变量自行补齐_0x5960a2 = function(a) { for (var c = a.length >> 1, e = c << 1, b = new Uint8Array(c), d = 0, f = 0; f < e; ) { b[d++] = _0x511f86[a.charCodeAt(f++)] << 4 | _0x511f86[a.charCodeAt(f++)]; } return b;}// originalString: URL 后面的原始参数var uint8Array = _0x5960a2(md5(_0x5960a2(md5(originalString))));
Step2:生成两个大数,一个是时间戳,我们称之为 fixedString1,另一个调用某个方法生成,我们称之为 fixedString2。

fixedString1位置 1 索引I 43 索引A 806:1663385262240 / 1000 = 1663385262.24fixedString2位置 1 索引I 16 索引A 834:M.apply(null, []) = 536919696
上述步骤中,M 对应以下方法,缺失的方法自行补齐(其中 _0x229792 是创建 canvas):

function _0x2996f8() { try { return _0x4b3b53 || (_0xb55f3e.perf ? -1 : (_0x4b3b53 = _0x229792(3735928559), _0x4b3b53)); } catch (a) { return -1; }}
Step3:先后生成两个数组,我们称之为 array1、array2,array2 就是由 array1 的元素位置变换后得来的,严格来讲,array1 不是一个完整的数组,而是一个个数字,这一点可以在日志中体现出来,为了方便我们就直接将其视为一个数组,两个数组都有19个元素,步骤如下:

array1[0] 至 array1[3] 为定值array1[4]位置 1 索引I 25 索引A 946:uint8Array[14]array1[5]位置 1 索引I 25 索引A 970:uint8Array[15]array1[6] 至 array1[7] 为定值,8、9 与 ua 有关array1[10]位置 1 索引I 52 索引A 1090:fixedString1 >> 24 = 99位置 1 索引I 47 索引A 1098:99 & 255 = 99array1[11]位置 1 索引I 52 索引A 1122:fixedString1 >> 16 = 25417位置 1 索引I 47 索引A 1130:25417 & 255 = 73array1[12]位置 1 索引I 52 索引A 1154:fixedString1 >> 8 = 6506755位置 1 索引I 47 索引A 1162:6506755 & 255 = 3array1[13]位置 1 索引I 52 索引A 1186:fixedString1 >> 0 = 241位置 1 索引I 47 索引A 1194:241 & 255 = 241array1[14]位置 1 索引I 52 索引A 1218:fixedString2 >> 24 = 32位置 1 索引I 47 索引A 1226:32 & 255 = 32array1[15]位置 1 索引I 52 索引A 1250:fixedString2 >> 16 = 8192位置 1 索引I 47 索引A 1258:8192 & 255 = 0array1[16]位置 1 索引I 52 索引A 1282:fixedString2 >> 8 = 2097342位置 1 索引I 47 索引A 1290:2097342 & 255 = 190array1[17]位置 1 索引I 52 索引A 1314:fixedString2 >> 0 = 536919696位置 1 索引I 47 索引A 1322:536919696 & 255 = 144array1[18]位置 1 索引I 27 索引A 1352:array1.reduce(function(a, b) { return a ^ b; }); = 100array1 完整值如下位置 1 索引I 27 索引A 1538:64,1.00390625,1,8,9,185,69,63,74,125,99,73,3,241,32,0,190,144,100array2 由 array1 元素交换位置而来:array2 = [array1[0], array1[2], array1[4], array1[6], array1[8], array1[10], array1[12], array1[14], array1[16], array1[18], array1[1], array1[3], array1[5], array1[7], array1[9], array1[11], array1[13], array1[15], array1[17]]array2 完整值如下array2 = [64,1,9,69,74,99,3,32,190,100,1.00390625,8,185,63,125,73,241,0,144]
Step4:将 Step3 得到的 array2 经过转换得到乱码字符串,步骤如下:

位置 1 索引I 16 索引A 1706:_0x2f2740.apply(null, array2) = "@\u0000\u0001\u000eíxE?\u0016c%> \u0000ó"位置 1 索引I 16 索引A 1760:_0x46fa4c.apply(null, ["", "@\u0000\u0001\u000e\tE?J}cI\u0003 \u0000d"]) = "\u0002-%.*yê^s6V,"位置 1 索引I 16 索引A 1812:_0x2b6720.apply(null, [2, 255, "\u0002-%.*yê^s6V,"]) = "\u0002-%.*yê^s6V,"
其中用到的函数:

function _0x2f2740(a, c, e, b, d, f, t, n, o, i, r, _, x, u, s, l, v, h, g) { let w = new Uint8Array(19); return w[0] = a, w[1] = r, w[2] = c, w[3] = _, w[4] = e, w[5] = x, w[6] = b, w[7] = u, w[8] = d, w[9] = s, w[10] = f, w[11] = l, w[12] = t, w[13] = v, w[14] = n, w[15] = h, w[16] = o, w[17] = g, w[18] = i, String.fromCharCode.apply(null, w);}function _0x46fa4c(a, c) { let e, b = [], d = 0, f = ""; for (let a = 0; a < 256; a++) { b[a] = a; } for (let c = 0; c < 256; c++) { d = (d + b[c] + a.charCodeAt(c % a.length)) % 256, e = b[c], b[c] = b[d], b[d] = e; } let t = 0; d = 0; for (let a = 0; a < c.length; a++) { t = (t + 1) % 256, d = (d + b[t]) % 256, e = b[t], b[t] = b[d], b[d] = e, f += String.fromCharCode(c.charCodeAt(a) ^ b[(b[t] + b[d]) % 256]); } return f;}function _0x583250(a) { return String.fromCharCode(a);}function _0x2b6720(a, c, e) { return _0x583250(a) + _0x583250(c) + e;}
自此,整个流程就走完了。可以用 JavaScript 来实现整个算法,用 Python 也可以。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/142915.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【网络八股】TCP八股

网络八股 请简述TCP/IP模型中每层的作用&#xff0c;典型协议和典型设备介绍一下三次握手的过程介绍一下四次挥手的过程必须三次握手吗&#xff0c;两次不行吗&#xff1f;为什么ACK数据包消耗TCP的序号吗三次握手中可以携带应用层数据吗四次挥手时&#xff0c;可以携带应用层数…

crypto:丢失的MD5

题目 得到一个md5.py 运行一下&#xff0c;发现报错&#xff0c;修改一下 运行之后又报错 报错原因是算法之前编码 正确的代码为 import hashlib for i in range(32,127):for j in range(32,127):for k in range(32,127):mhashlib.md5()m.update((TASC chr(i) O3RJMV c…

【Java SE】Lambda表达式

目录 ♫什么是Lambda表达式 ♫Lambda表达式的语法 ♫函数式接口 ♫Lambda表达式的使用 ♫变量捕获 ♫ Lambda表达式在集合中的使用 ♪Collection的foreach()&#xff1a; ♪List的sort()&#xff1a; ♪Map的foreach() ♫什么是Lambda表达式 Lambda 表达式是 Java SE 8中一个…

SpringMVC 学习(二)Hello SpringMVC

3. Hello SpringMVC (1) 新建 maven 模块 springmvc-02-hellomvc (2) 确认依赖的导入 (3) 配置 web.xml <!--web/WEB-INF/web.xml--> <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmlns.jcp.org/xml/ns/javaee…

通用CI/CD软件平台TeamCity推出代理终端功能,谁能从中获益?

JetBrains官方在TeamCity中推出代理终端&#xff1a;这项新功能专门用于帮助用户轻松查看代理上的系统日志、检查已安装的软件&#xff0c;以及直接从 TeamCity 的 UI 调试特定代理问题。 TeamCity是一个通用的 CI/CD 软件平台&#xff0c;可以实现灵活的工作流、协作和开发做…

抖音短视频seo矩阵系统源代码开发系统架构及功能解析

短视频seo源码&#xff0c;短视频seo矩阵系统底层框架上支持了从ai视频混剪&#xff0c;视频批量原创产出&#xff0c;云存储批量视频制作&#xff0c;账号矩阵&#xff0c;视频一键分发&#xff0c;站内实现关键词、短视频批量搜索排名&#xff0c;数据统计分类多功能细节深度…

深入MySQL数据库进阶实战:性能优化、高可用性与安全性

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 MySQL是世界上最流行的开…

ruoyi-nbcio增加websocket与测试页面

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 为了后面流程发起等消息推送&#xff0c;所以需要集成websocket。 1、后端增加websoket支持 首先在framework模块里的pom.xml增加websocket <dependency…

Element UI搭建首页导航和左侧菜单以及Mock.js和(组件通信)总线的运用

目录 前言 一、Mock.js简介及使用 1.Mock.js简介 1.1.什么是Mock.js 1.2.Mock.js的两大特性 1.3.Mock.js使用的优势 1.4.Mock.js的基本用法 1.5.Mock.js与前端框架的集成 2.Mock.js的使用 2.1安装Mock.js 2.2.引入mockjs 2.3.mockjs使用 2.3.1.定义测试数据文件 2…

如何优化网站排名(百度SEO指南与优化布局方法)

百度SEO指南介绍&#xff1a;蘑菇号-www.mooogu.cn 首先&#xff0c;为了提高网站的搜索引擎优化排名&#xff0c;需要遵循百度SEO指南的规则和标准。这包括使用符合规范的网站结构、页面内容的质量和与目标用户相关的关键词。避免使用非法技术和黑帽SEO的方法。 增加百度SEO…

Python——— 异常机制

&#xff08;一&#xff09;异常 工作中&#xff0c;程序遇到的情况不可能完美。比如&#xff1a;程序要打开某个文件&#xff0c;这个文件可能不存在或者文件格式不对&#xff1b;程序在运行着&#xff0c;但是内存或硬盘可能满了等等。 软件程序在运行过程中&#xff0c;非常…

【计算机网络】网络层和数据链路层

文章目录 IP协议网段划分分类划分法CIDR 方案路由NAT网络地址转换技术IP报文的另外三个参数mac帧ARP协议交换机ICMP代理服务器 IP协议 TCP有将数据 可靠、高效 发给对方的 策略&#xff0c;而IP具有发送的能力&#xff0c;即将数据从A主机送到B主机的 能力 。 用户要的是100%…

程序员不得不知道的排序算法-上

目录 前言 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.快速排序 6.归并排序 总结 前言 今天给大家讲一下常用的排序算法 1.冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的排序算法&#xff0c;它重复地从待排序的元素中比较相邻的两个元素&a…

vue event bus 事件总线

vue event bus 事件总线 创建 工程&#xff1a; H:\java_work\java_springboot\vue_study ctrl按住不放 右键 悬着 powershell H:\java_work\java_springboot\js_study\Vue2_3入门到实战-配套资料\01-随堂代码素材\day04\准备代码\08-事件总线-扩展 vue --version vue crea…

【C语言】文件操作(一)

前言 本篇博客讲解对文件的操作&#xff0c;包括打开&#xff0c;关闭操作。在下篇博客将讲解文件的读写。 文章目录 一、 什么是文件&#xff1f;1.1 用于存储数据1.2 文件类型1.3 文件名1.4 二进制文件和文本文件 二、文件的打开和关闭2.1 流和标准流2.2 文件指针2.3文件的打…

你的周末和你一起失去了价值(打工人篇)

花儿在绽放盛开之前&#xff0c;会在无人的清晨吸收甘露&#xff0c;然后赶上第一趟的朝阳&#xff0c;才换来路人赞许 一言指南北 选择你的职业&#xff0c;确认你的方向&#xff0c;没有方向&#xff0c;就无法体验时间感 如果你是打工人&#xff0c;那么请接着往下看 如果是…

React项目中如何实现一个简单的锚点目录定位

小册 这是我整理的学习资料&#xff0c;非常系统和完善&#xff0c;欢迎一起学习 现代JavaScript高级小册 深入浅出Dart 现代TypeScript高级小册 linwu的算法笔记&#x1f4d2; 前言 锚点目录定位功能在长页面和文档类网站中非常常见,它可以让用户快速定位到页面中的某个…

GLTF编辑器也可以转换GLB模型

1、GLB模型介绍 GLB&#xff08;GLTF Binary&#xff09;是一种用于表示三维模型和场景的文件格式。GLTF是"GL Transmission Format"的缩写&#xff0c;是一种开放的、跨平台的标准&#xff0c;旨在在各种3D图形应用程序和引擎之间进行交换和共享。 GLB文件是GLTF文件…

Java之线程的详细解析一

实现多线程 简单了解多线程【理解】 是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程&#xff0c;提升性能。 并发和并行【理解】 并行&#xff1a;在同一时刻&#xff0c;有多个指令在多个CPU上同时执行…

【excel密码】如何给excel设置带有密码的只读模式

大家提起只读模式&#xff0c;应该都不会联想到密码&#xff0c;想起excel密码可能会想到打开密码或者工作表保护。今天给大家分享如何设置带有密码的只读模式。 打开excel文件&#xff0c;将文件进行【另存为】设置&#xff0c;然后停留在保存路径的界面中&#xff0c;我们点…