2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过

news2024/11/15 14:22:33

目录

  • 一、无限debugger的原理与绕过
    • 1.1 案例介绍
    • 1.2 实现原理
    • 1.3 绕过debugger方法
      • 1.3.1 禁用所有断点
      • 1.3.2 禁用局部断点
      • 1.3.3 替换文件
      • 1.3.4 函数置空与hook
  • 二、补充
    • 2.1 改写JavaScript文件
    • 2.2 浏览器开发者工具中出现的VM开头的JS文件是什么?
  • 三、实战

一、无限debugger的原理与绕过

debugger 是 JavaScript 中定义的一个专门用于断点调试的关键字,只要遇到它,JavaScript 的执行便会在此处中断,进入调试模式。有了 debugger 这个关键字,我们就可以非常方便地对 JavaScript 代码进行调试,比如使用 JavaScript Hook 时,我们可以加入 debugger 关键字,使其在关键的位置停下来,以便查找逆向突破口。但有时候 debugger 会被网站开发者利用,使其成为阻挠我们正常调试的拦路虎。本节中,我们介绍一个案例来绕过无限 debugger。

1.1 案例介绍

案例介绍:我们先看一个案例,网址是 http://shanzhi.spbeen.com/,打开这个网站,一般操作和之前的网站没有什么不同。但是,一旦我们打开开发者工具,就发现它立即进入了断点模式,如下图所示:

在这里插入图片描述
我们既没有设置任何断点,也没有执行任何额外的脚本,它就直接进入了断点模式。这时候我们可以点击 Resume script execution(恢复脚本执行)按钮,尝试跳过这个断点继续执行,如下图所示:

请添加图片描述
然而不管我们点击多少次按钮,它仍然一次次地进入断点模式,无限循环下去,我们称这样的情况为无限 debugger。怎么办呢?似乎无法正常添加断点调试了,有什么解决办法吗? 办法当然是有的,本节中我们就来总结一下无限 debugger 的应对方案,在后面部分实战的案例中我们也会遇到无限 debugger。

1.2 实现原理

首先要做的是找到无限 debugger 的源头,上面的案例通过堆栈回溯,查看 debugger 是如何生成的,如下图所示:
在这里插入图片描述
继续往上进行追溯,如下图所示:
在这里插入图片描述
这时点击左下角的格式化按钮:

setInterval(()=>{
    (function(a) {
        return (function(a) {
            return (Function('Function(arguments[0]+"' + a + '")()'))
        }
        )(a)
    }
    )('bugger')('de', 0, 0, (0,
    0));
}
, 1000);

利用 Function 产生 debugger,然后通过 setInterval 循环,每秒执行1次产生 debugger 语句的操作。当然,还有很多类似的实现,比如无限 for 循环、无限 while 循环、无限递归调用等,它们都可以实现这样的效果,原理大同小异。ps:从某种意义上来说,无限 debugger 不会真正的死循环(只不过这个执行次数多到我们本身靠手点难以接受罢了),而是有规律得执行逻辑,一般用定时器。 无限 debugger 产生小结:

  1. 一定会先产生 debugger 关键字,产生 debugger 关键字,可以是明文也可以混淆。

    // ① 明文 直接书写完整的 debugger
    debugger;
    // ② 可以混淆(可轻度混淆) 即eval配合 debugger
    eval('debug' + 'ger;')
    // ③ 可以重度混淆
    // 结合constructor,debugger,call,apply,action 等关键字进行混淆,增加调试的困难
    Function('debugger').call()
    Function('debugger').apply()
    Function('debugger').bind()
    Function.constructor('debugger').call('action')
    funObj.constructor('debugger').call('action')
    (function(){return !![];}['constructor']('debugger')['call']('action'))
    eval('(function (){}["constructor"]("debugger")["call"]("action"));')
    //总结:这些debugger方法,是实现debugger的基础,可以理解为是三元素。基于三种元素,可以形成多种多样的玩法
    
  2. 结合循环,循环的方式可以是:while/for 循环、包含 debugger 的函数调用自身、方法间的循环调用、计时器(setInterval)

1.3 绕过debugger方法

因为 debugger 其实就是对应的一个断点,它相当于用代码显示地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。

1.3.1 禁用所有断点

全局禁用开关位于 Sources 面板的右上角,叫做 Deactivate breakpoints,如下图所示:
在这里插入图片描述
点击它,该按钮会被激活,变成蓝色,如下图所示:
在这里插入图片描述
这个时候我们再重新点击一下 Resume script execution(恢复脚本执行)按钮,跳过当前断点,页面就不会再进入到无限 debugger 的状态了。但是这种全局禁用其实并不是一个好的方案,因为禁用之后我们也无法在其他位置增加断点进行调试了,所有的断点都失效了!

ps: 解决无限 debugger 名词解释应该为在没有 debugger 干扰的情况下调试,而不是放弃所有的 debugger 调试(也就是说我们自己的调试还得能正常使用),所以此种方式基本不用。

1.3.2 禁用局部断点

取消刚才的 Deactivate breakpoints 模式,页面会重新进入无限 debugger 模式,尝试使用另一种方法来跳过这个无限 debugger。在 debugger 语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,如下图所示:
在这里插入图片描述
这里有一个 Never pause here 选项,意思是从不在此处暂停。选择这个选项,于是页面变成如下图所示的样子:

在这里插入图片描述
当前断点显示为橙色,并且断点前面多了一个 ? 符号,同时 Breakpoints 也出现了刚才添加的断点位置,这时再次点击 Resume script execution(恢复脚本执行)按钮,就可以发现我们不会再进入无限 debugger 模式了。当然,我们也可以选择另外一个选项 Add conditional breakpoint,如下图所示:

这个模式更加高级,我们可以设置进入断点的条件,比如在调试过程中,期望某个变量的值大于某个具体的值的时候才停下来。但在本案例中,由于这里是无限循环,我们没有什么具体的变量可以作为判定依据,因此可以直接写一个简单的表达式来控制。选择 Add conditional breakpoint 选项,直接填入 false 然后回车即可,如下图所示:

在这里插入图片描述

1.3.3 替换文件

利用 Overrides 面板我们可以将远程的 JavaScript 文件替换成本地的 JavaScript 文件,这里我们依然可以使用这个方法来对文件进行替换,替换成什么呢?很简单,我们只需要在新的文件里把 debugger 这个关键字删除。我们将当前的 JavaScript 文件复制到文本编辑器中,删除或者直接注释掉 debugger 这个关键字,修改如下:

setInterval(()=>{
    (function(a) {
        return (function(a) {
            return (Function('Function(arguments[0]+"' + a + '")()'))
        }
        )(a)
    }
    // 直接把参数置空,当然这里也可以把整个文件替换掉(没啥业务逻辑),或者去掉setInterval等都可以
    )('')('', 0, 0, (0,
    0));
}
, 1000);

打开 Sources 面板下的 Overrides 面板,将修改后的完整 JavaScript 文件复制进去。替换完成之后,重新刷新网页,这时候发现不会进入无限 debugger 模式了。如果该操作不熟悉,可以参照下面的 3.1 改写JavaScript文件。

1.3.4 函数置空与hook

ps:一定要在 debugger 进入之前。

无限 debugger 产生的原因是定时器造成的,所以我们可以重写这个函数,使无限 debugger 失效:

// 这里是业务代码和setInterval无关,所以直接置空即可
setInterval = function(){} //定时器置空,置空之后上面的无限debugger消失
function xxx(){} //执行函数置空 xxx

② hook:不同的情况书写不同的 hook 代码,即 hook 不同的函数即可,这里我只以 setInterval 为例,其他类似。

_setInterval = setInterval
setInterval = function (a,b) {
    if(a.toString().indexOf('debugger') == -1){
        return function(){}
    }else{
        _setInterval(a,b)
    }
}

Function.prototype.toString = function () {
    return `function ${this.name}() { [native code] }`
}

小结:

  1. 优先尝试禁用局部断点,即 Never pause here (最方便快捷,但是最卡–深有体会,也最容出问题)

  2. 次优先尝试重写调用函数,缺陷:容易破坏业务逻辑,导致控制流变化。如:

    Function = function(){}
    setInterval = function(){}
    
  3. 文件替换。缺陷:操作稍微有一点点的麻烦,对动态情况的支持不太好,也可能会改变控制流走向

  4. 万一以上三个都难受了怎么办? 别逆向了,反调试都那么难了,那加密不得难上天啊。放弃吧,嗷~

二、补充

2.1 改写JavaScript文件

我们知道,一个网页里面的 JavaScript 是从对应服务器上下载下来并在浏览器执行的。有时候,我们可能想要在调试的过程中对 JavaScript 做一些更改,比如说有以下需求:

  1. 发现 JavaScript 文件中包含很多阻挠调试的代码或者无效代码、干扰代码,想要将其删除(如上面的无限 debugger)。
  2. 调试到某处,想要加一行 console.log 输出一些内容,以便观察某个变量或方法在页面加载过程中的调用情况。在某些情况下,这种方法比打断点调试更方便。
  3. 调试过程遇到某个局部变量或方法,想要把它赋值给 window 对象以便全局可以访问或调用。
  4. 在调试的时候,得到的某个变量中可能包含一些关键的结果,想要加一些逻辑将这些结果转发到对应的目标服务器。

这时候我们可以试着在 Sources 面板中对 JavaScript 进行更改,但这种更改并不能长久生效,一旦刷新页面,更改就全都没有了。比如我们在 JavaScript 文件中写入一行 JavaScript 代码,然后保存,如下图所示:

在这里插入图片描述

注意:点击了左下角的格式化按钮后,不能向格式化的文件中添加内容。

这时候我们可以发现 JavaScript 文件名左侧上出现了一个警告标志,提示我们做的更改是不会保存的。这时候重新刷新一下页面,再看一下更改的这个文件,如下图所示:

有什么方法可以修改呢?其实有一些浏览器插件可以实现,比如:ReRes。在插件中,我们可以添加自定义的 JavaScript 文件,并配置 URL 映射规则,这样浏览器在加载某个在线 JavaScript 文件的时候就可以将内容替换成自定义的 JavaScript 文件了。另外,还有一些代理服务器也可以实现,比如 Charles、Fiddler,借助它们可以加载 JavaScript 文件时修改对应 URL 的响应内容,以实现对 JavaScript 文件的修改。其实浏览器的开发者工具已经原生支持这个功能了,即浏览器的 Overrides 功能,它在 Sources 面板左侧,如下图所示:

我们可以在 Overrides 面板上选定一个本地的文件夹,用于保存需要更改的 JavaScript 文件,下面来实际操作一下。切到 Overrides 面板,点击 + 按钮,如下图所示:

这时候浏览器会提示我们选择一个本地文件夹,用于存储要替换的 JavaScript 文件。这里我选定了一个新建的文件夹:FunddbOverrides,注意这时候可能会遇到下图所示的提示,如果没有问题,直接点击 允许 即可。

在这里插入图片描述

这时,在 Overrides 面板下就多了 FunddbOverrides 文件夹,用于存储所有我们想要更改的 JavaScript 文件,如下图所示:

我们可以看到,现在所在的 JavaScript 选项卡是 app.d0a16ab3b7972174cc88.js:formatted,代码已经被格式化了。因为格式化后的代码是无法直接在浏览器中修改的,所以为了方便,我把格式化后的文件复制到了 Notepad++ 中,然后把 window.eval 这行代码注释了,如下图所示:

在这里插入图片描述

接着把修改后的内容替换到原来的 JavaScript 文件中。这里要注意,要切换到 app.d0a16ab3b7972174cc88.js 文件才能修改,直接替换 JavaScript 文件的所有内容即可,如下图所示:

在这里插入图片描述

替换完毕之后 ctrl + s 保存,这时候再切换回 Overrides 面板,就可以发现成功生成了新的 JavaScript 文件,它用于替换原有的 JavaScript 文件,如下图所示:

在这里插入图片描述

替换完成之后,重新刷新网页,正如我们所料,这时候不会再进入无限 debugger 模式了,证明改写 JavaScript 成功!而且刷新页面也不会丢失了,除了注释掉干扰代码外,在一些场景下,我们还可以增加一些 JavaScript 逻辑,比如直接将某个变量的结果通过 API 发送到远程服务器,并通过服务器将数据保存下来,也就完成了直接拦截 Ajax请求并保存数据的过程了,修改 JavaScript 文件有很多用途,此方案可以为我们进行 JavaScript 逆向带来极大便利。

2.2 浏览器开发者工具中出现的VM开头的JS文件是什么?

在 Chrome 的开发者工具中,你可能会看到一些以 VM 开头的 JavaScript 文件(如 VM1057)。
在这里插入图片描述
VM 表示的是 Virtual Machine(虚拟机),这些文件通常表示由浏览器生成和执行的虚拟机脚本环境中的临时脚本。这些脚本并不是项目源代码的一部分,也不是实际存在的物理文件,它们在浏览器的内存中创建并执行。 比如说,当你在调试一个网页时,如果在某些动态生成并执行的 JS 代码上设定了断点,Chrome 调试器会在一个以 VM 开头的文件中显示这些代码,例如 VM1057。这个 VM 文件的存在只是为了调试目的,它并不存在于服务器端,也不会被存储在本地,而是存在于浏览器内存中。一般情况下,这类文件的出现是因为浏览器对 JavaScript 代码的处理方式,如动态编译或者 JavaScript 堆栈跟踪。出现的原因:

  1. 动态执行的 JavaScript 代码。比如通过 eval 函数或者 new Function 方法,Chrome 浏览器会创建一个 VM 文件来展示这段临时执行的代码。比如某个网页因为反爬虫,动态生成了 debugger,这些断点并没有直接写在服务器上的原始 JavaScript 文件中,而是在某些 JavaScript 代码的执行过程中被生成,并因此触发 debugger。这些代码也会在执行时被浏览器视为临时的 VM 脚本,并在执行到 debugger 时暂停执行,从而造成所谓的 无限 debugger 循环
  2. 来自执行栈的代码。有时候,当 JavaScript 引擎处理异步操作(例如 Promise、setTimeout 等)中的错误时,错误堆栈可能包含到 VM 脚本的引用,这是因为内部错误回调函数是在虚拟环境中执行的。

三、实战

F12 打开调试工具,打上 script 断点,如下图所示:
在这里插入图片描述
接着一直单击 Resume script execution(恢复脚本执行)按钮,直到左边 Page 下方 top 中,出现我们要分析网站的域名为止(因为有时候会受浏览器等插件的影响,不是在我们要分析的网站下断住的),如下图所示:

接着放开 script 断点,再次点击 Resume script execution(恢复脚本执行)按钮,跳到如下位置:

过这个 debugger 很简单了,使用改写 JavaScript 文件的方式即可,在 1.3.3 替换文件 已经详细讲解过,这里就不再进行赘述。替换完成之后,我们重新刷新一下网页,又出现一个 debugger,如下图所示:

优先使用禁用局部断点的方式,即鼠标放在 debugger 行号处,单击鼠标右键,选择 Never pause here,发现是可以过掉的,这里也可以采用其他方式,在产生 debugger 之前,选择堆栈 K 的时机执行如下代码:

// 因为这里产生debugger的逻辑比较复杂,我就不去分析了,写起来很累
_Function = Function
Function.prototype.constructor = function () {
    if (arguments[0].indexOf('debugger') !== -1) {
        return _Function('')
    }
    return _Function(arguments[0])
}

我这里为了简便就直接采用禁用局部断点的方式了,接着单击 Resume script execution(恢复脚本执行)按钮,发现又出现了新的 debugger,如下图所示:



发现这里在调用 document 的 appendChild 方法,目的是向 n.head 中添加元素,控制台看一下小 o,如下所示:
在这里插入图片描述
到这里我们就知道此处产生 debugger 的原因了,就是使用 appendChild 方法往页面动态插入 script 标签,然后 script 标签对中包含 debugger 关键字,故此处采取的方案,直接将 appendChild 方法重写即可,重写代码如下:

let oldAppendChild = Node.prototype.appendChild
Node.prototype.appendChild = function () {
    if (arguments[0].innerHTML && arguments[0].innerHTML.indexOf('debugger') !== -1) {
        arguments[0].innerHTML = ''
    }
    return oldAppendChild.apply(this, arguments)
}

在 debugger 出现之前(即在堆栈处于 b 或者 le 时都可以,或者更早之前都行)执行此代码,发现是可以过掉 debugger 的,然后调试到了如下位置:
在这里插入图片描述
这个就很简单了,写死的 debugger + 固定的循环次数,也没有相关的业务逻辑代码,故直接使用文件替换的方式直接替换即可,我是将 for 循环整体进行了替换,替换之后,同样,重新刷新一下网页,发现又到了 appendChild 那里,采取上面重写 appendChild 方法的方案即可,接着单击 Resume script execution(恢复脚本执行)按钮,进入到一个新的 debugger 调试,如下:

优先使用禁用局部断点的方式,即鼠标放在 debugger 行号处,单击鼠标右键,选择 Never pause here,如下图所示:

发现是可以过掉的,这里也可以采用其他方式,追溯到 debugger 产生的源头:


可以看到此处的 debugger 为 eval 结合 debugger 关键字实现,并且观察该代码中无业务逻辑代码,故直接重写 eval 函数即可,重写代码如下:

old_eval = eval
eval = function () {
    if (arguments[0].indexOf('debugger') !== -1) {
        return function () {
        }
    } else {
        return old_eval(arguments[0])
    }
}

Function.prototype.toString = function () {
    return `function ${this.name}() { [native code] }`
}

至此此题目的所有涉及到的 debugger 调试我们都已经过掉,可以正常地进行接口分析了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java构造方法详解

在Java方法内部定义一个局部变量时,必须要初始化,否则就会编译失败,如下: 要让上述代码通过编译,只需在使用a之前给a赋一个初始值即可 如果是对象:下面用一个日期类演示 我们没有给年月日赋值,…

laravel8 导入 excel常见问题

上传xls 或 xlsx 文件后,文件解析为 zip 格式,输入正常情况,不影响解析 里面的内容 遇到解析内容,解析为空的情况,可能是 因为excel 存在多个 Sheet1 造成,服务器不能解析一个 Sheet1 的情况&#xff0…

阿里开源编程大模型 CodeQwen1.5:64K92编程语言,Code和SQL编程,评测接近GPT-4-Turbo

前言 阿里巴巴最近发布的CodeQwen1.5模型标志着其在编程语言模型领域的一次重大突破。这款开源模型不仅支持高达92种编程语言和64K的上下文长度,而且在多项性能评测中显示出接近或超过当前行业领导者GPT-4-Turbo的能力。 Huggingface模型下载:https://h…

# ERROR: node with name “rabbit“ already running on “MS-ITALIJUXHAMJ“ 解决方案

ERROR: node with name “rabbit” already running on “MS-ITALIJUXHAMJ” 解决方案 一、问题描述: 1、启动 rabbitmq-server.bat 服务时,出错 Error 2、查询 rabbitmqctl status 状态时,出错 Error 3、停止 rabbitmqctl stop 服务时&a…

如何使用CertCrunchy从SSL证书中发现和识别潜在的主机名称

关于CertCrunchy CertCrunchy是一款功能强大的网络侦查工具,该工具基于纯Python开发,广大研究人员可以利用该工具轻松从SSL证书中发现和识别潜在的主机信息。 支持的在线源 该工具支持从在线源或给定IP地址范围获取SSL证书的相关数据,并检索…

A Dexterous Hand-Arm Teleoperation System

A Dexterous Hand-Arm Teleoperation System Based on Hand Pose Estimation and Active Vision解读 摘要1. 简介2.相关工作2.1 机器人遥操作2.2 主动视觉(Active Vision) 3. 硬件设置4. 基于视觉的机器人手部姿态估计4.1 Transteleop4.2 Dataset 5. 主动…

【基础绘图】 09.小提琴图

效果图: 主要步骤: 1. 数据准备:生成随机数组 2. 数据处理:计算四分位数、中位数、均值、最大最小值 3. 图像绘制:绘制小提琴图 详细代码:着急的直接拖到最后有完整代码 步骤一:导入库包及…

使用C#和NMODBUS快速搭建MODBUS从站模拟器

MODBUS是使用广泛的协议,通讯测试时进行有使用。Modbus通讯分为主站和从站,使用RS485通讯时同一个网络内只能有一个主站,多个从站。使用TCP通讯时没有这方面的限制,可以同时支持多个主站的通讯读写。 开发测试时有各种复杂的需求&…

Milvus中那些年重要的基本概念

Milvus是一款开源的云原生向量数据库,专为海量向量数据的存储、检索和管理而设计。它支持实时的向量相似度搜索,适用于各种AI和机器学习应用场景。以下是Milvus的一些基本概念: 非结构化数据 非结构化数据是指那些不遵循特定数据模型或格式、…

npm无法安装node-sass 的问题

安装 node-sass 的问题呈现:4.9.0版本无法下载 Downloading binary from https://github.com/sass/node-sass/releases/download/v4.9.0/win32-x64-72_binding.node Cannot download "https://github.com/sass/node-sass/releases/download/v4.9.0/win32-x64-…

Linux服务器常用巡检命令,查看日志

查看日志 3.1 通过journalctl命令查看系统日志 命令&#xff1a;journalctl 3.2 通过tail查看系统日志 查看日志文件多少行代码&#xff1a;tail -n [行数] [日志文件] 4. 服务状态 4.1 查看指定服务的状态 命令&#xff1a;systemctl status <service> 比如查看防火墙…

React 第三十四章 React 渲染流程

现代前端框架都可以总结为一个公式&#xff1a;UI f&#xff08;state&#xff09; 上面的公式还可以进行一个拆分&#xff1a; 根据自变量&#xff08;state&#xff09;的变化计算出 UI 的变化根据 UI 变化执行具体的宿主环境的 API 对应的公式&#xff1a; const state…

Axure PR 10 制作顶部下拉三级菜单和侧边三级菜单教程和源码

在线预览地址&#xff1a;Untitled Document 2.侧边三级下拉菜单 在线预览地址&#xff1a;Untitled Document 文件包和教程下载地址&#xff1a;https://pan.quark.cn/s/77e55945bfa4 程序员必备资源网站&#xff1a;天梦星服务平台 (tmxkj.top)

事件代理 浅谈

事件代理是一种将事件处理委托给父元素或祖先元素来管理的技术。当子元素触发特定事件时&#xff0c;该事件不会直接在子元素上进行处理&#xff0c;而是会冒泡到父元素或祖先元素&#xff0c;并在那里进行处理。这样做的好处是可以减少事件处理函数的数量&#xff0c;提高性能…

PyCharm 集成 Git

目录 1、配置 Git 忽略文件 2、定位Git 3、使用pycharm本地提交 3.1、初始化本地库 3.2、添加到暂存区 3.3、提交到本地库 3.4、切换版本 4、分支操作 4.1、创建分支 4.2、切换分支 4.3、合并分支 5、解决冲突 1、配置 Git 忽略文件 作用&#xff1a;与项目的实际…

浅谈运维数据安全

在数字化日益深入的今天&#xff0c;运维数据安全已经成为企业信息安全体系中的核心要素。运维工作涉及到企业信息系统的各个方面&#xff0c;从硬件维护到软件升级&#xff0c;从网络配置到数据备份&#xff0c;无一不需要严谨的数据安全保障措施。本文将从运维数据安全的重要…

Ubuntu 22.04: VS Code 配置 C++ 编译及 CMake

一、VS Code 安装以及 C 编译环境配置 1. 在 Ubuntu 中安装 VS Code 笔者直接在 Ubuntu Software 中心安装 VS Code。也可以从VS Code官网下载 deb&#xff0c;解压 dpkg -i 安装。 2. VS Code 中配置 g/gcc 1) 安装 C/C 扩展 &#xff08;CtrlShiftX&#xff09; 2&#x…

HR人才测评,表达能力与岗位胜任力素质测评

什么是表达能力&#xff1f; 表达能力指的就是在语言能力基础之上发展形成的一种语用能力&#xff0c;可以结合自己所掌握的语言来实现交际的目的&#xff0c;能正确且灵活的把语言材料组合成为语言并且表达出想要表达的内容。 在百度百科中有如此定义&#xff0c;表达能力…

go-zero整合asynq实现分布式定时任务

本教程基于go-zero微服务入门教程&#xff0c;项目工程结构同上一个教程。 go-zero微服务入门教程&#xff08;点击进入&#xff09; 本教程主要实现go-zero整合asynq实现分布式定时任务。 本文源码&#xff1a;https://gitee.com/songfayuan/go-zero-demo &#xff08;教程源…

超详细的胎教级Stable Diffusion使用教程(五)

这套课程分为五节课&#xff0c;会系统性的介绍sd的全部功能和实操案例&#xff0c;让你打下坚实牢靠的基础 一、为什么要学Stable Diffusion&#xff0c;它究竟有多强大&#xff1f; 二、三分钟教你装好Stable Diffusion 三、小白快速上手Stable Diffusion 四、Stable dif…