当前位置: 首页 > news >正文

【C++实战(75)】筑牢安全防线,攻克漏洞难题

目录

  • 一、常见 C++ 安全漏洞类型
    • 1.1 缓冲区溢出
    • 1.2 内存泄漏与野指针
    • 1.3 SQL 注入与 XSS
  • 二、漏洞防护的代码实践
    • 2.1 安全的内存操作
    • 2.2 输入验证与过滤
    • 2.3 安全的 API 使用
  • 三、安全编译与工具防护
    • 3.1 编译器安全选项
    • 3.2 静态代码分析工具
    • 3.3 动态漏洞检测工具
  • 四、实战项目:安全的用户登录系统
    • 4.1 项目需求
    • 4.2 安全代码实现与漏洞防护措施
    • 4.3 安全测试


一、常见 C++ 安全漏洞类型

1.1 缓冲区溢出

缓冲区溢出是指程序向缓冲区写入的数据超出了缓冲区的容量,导致数据覆盖了相邻的内存区域。这就好比你有一个杯子,它的容量是 100 毫升,但你却试图往里面倒入 150 毫升的水,那么多余的水就会溢出来,可能会损坏周围的物品。在程序中,缓冲区溢出可能导致程序崩溃、数据损坏,甚至被攻击者利用来执行恶意代码。

造成缓冲区溢出的原因主要有以下几点:一是缺乏输入验证,程序没有对用户输入的数据长度进行检查,攻击者可以输入超长的数据,从而引发缓冲区溢出;二是使用了不安全的函数,如strcpy、strcat等,这些函数在复制字符串时不会检查目标缓冲区的大小,容易导致数据溢出;三是数组下标越界,在访问数组元素时,使用了错误的下标,导致访问到了数组之外的内存区域。

为了防止缓冲区溢出,我们可以采取一系列措施。在输入验证方面,要对所有输入数据进行严格的长度和格式检查,确保输入数据不会超过缓冲区的容量。比如在处理用户输入的用户名时,可以限制用户名的长度不能超过 20 个字符。同时,要使用安全的函数,如strncpy、strncat等,这些函数会在复制字符串时检查目标缓冲区的大小,避免数据溢出。在进行数组操作时,要仔细检查数组下标,确保不会越界访问。还可以使用一些工具,如静态代码分析工具,来检测代码中潜在的缓冲区溢出漏洞。

1.2 内存泄漏与野指针

内存泄漏是指程序在分配内存后,没有及时释放已经不再使用的内存,导致这些内存无法被再次利用,就像你买了很多房子,但住过之后却不卖掉或出租,导致房子闲置浪费。长时间运行的程序如果存在内存泄漏问题,会逐渐耗尽系统内存,导致系统性能下降,甚至崩溃。比如一个网络服务器程序,如果每次处理完客户端请求后都没有释放分配的内存,随着客户端连接的增多,内存泄漏会越来越严重,最终可能导致服务器无法正常工作。

野指针则是指向无效内存地址的指针,它就像一个迷路的指针,不知道自己该指向哪里。野指针的产生原因主要有未初始化的指针,声明指针变量后没有给它赋初值,其值是随机的,可能指向非法内存地址;指针越界,指针操作超出了其所指向的内存区域;指针指向的内存被释放后未置空,成为悬空指针。当程序解引用野指针时,会导致未定义行为,可能引发程序崩溃或数据损坏。

为了检测内存泄漏,可以使用一些工具,如 Valgrind。它能够在程序运行时动态检测内存泄漏和其他内存错误。在代码规范方面,要养成良好的编程习惯,比如在使用指针前一定要先初始化,避免使用未初始化的指针;在释放内存后,要将指针置为nullptr,防止悬空指针的产生;尽量使用智能指针,如std::unique_ptr、std::shared_ptr,它们能够自动管理内存,减少内存泄漏和野指针的风险。

1.3 SQL 注入与 XSS

SQL 注入是一种常见的 Web 应用安全漏洞,攻击者通过在 Web 表单输入或查询字符串中注入恶意 SQL 语句,从而操控数据库。比如一个用户登录系统,正常的登录 SQL 语句可能是SELECT * FROM users WHERE username = ‘username′ANDpassword=′username' AND password = 'usernameANDpassword=password’,如果攻击者在用户名输入框中输入admin’ OR ‘1’='1,那么拼接后的 SQL 语句就变成了SELECT * FROM users WHERE username = ‘admin’ OR ‘1’=‘1’ AND password = ‘$password’,由于1=1恒成立,攻击者就可以绕过密码验证,成功登录系统。SQL 注入可能导致数据泄露、数据篡改、甚至数据库被完全控制。

XSS(跨站脚本攻击)是指攻击者在网页中注入恶意脚本,当用户访问该网页时,恶意脚本就会在用户的浏览器中执行,从而盗取用户的 cookie、会话令牌等敏感信息,或者进行钓鱼攻击。比如攻击者在一个论坛的帖子中插入一段恶意 JavaScript 脚本,当其他用户浏览该帖子时,脚本就会被执行,攻击者就可以获取这些用户的敏感信息。

在 Web 场景下的 C++ 后端,为了防止 SQL 注入,要使用参数化查询,避免直接将用户输入拼接到 SQL 语句中。比如使用 MySQL 数据库时,可以使用预处理语句和绑定参数的方式来执行 SQL 查询,这样可以确保用户输入的数据不会被解释为 SQL 代码的一部分。同时,要对用户输入进行严格的验证和过滤,去除可能导致 SQL 注入的特殊字符。防止 XSS 攻击则需要对用户输入进行转义处理,将特殊字符转换为 HTML 实体,这样可以避免恶意脚本在浏览器中执行;对输出内容进行编码,确保输出到页面的数据是安全的。

二、漏洞防护的代码实践

2.1 安全的内存操作

在 C++ 编程中,使用std::string替代char能显著提升内存操作的安全性。std::string是 C++ 标准库中的字符串类,它自动管理内存,避免了手动内存管理带来的诸多问题。比如当你使用char时,需要手动分配和释放内存,一旦忘记释放内存,就会导致内存泄漏。而std::string对象在销毁时会自动释放其所占用的内存,无需手动干预。

std::string还提供了丰富的成员函数,方便进行字符串操作,并且能自动处理字符串长度变化时的内存调整。例如,使用append函数追加字符串时,std::string会自动扩展内存以容纳新的内容,而使用char*时,如果不预先分配足够大的缓冲区,就可能导致缓冲区溢出。std::string重载了很多操作符,如+、+=等,使用起来更加简洁直观。

为了避免数组越界,在使用数组时,一定要明确数组的边界范围,确保索引值始终在合法范围内。可以在访问数组元素前,使用if语句对索引进行检查,判断其是否小于数组的大小。比如:

int arr[10];
int index = 15;
if (index >= 0 && index < 10) {arr[index] = 100;
} else {// 处理索引越界的情况,比如记录日志或抛出异常
}

也可以使用std::vector来替代普通数组,std::vector会自动管理内存,并且提供了at函数,该函数在访问元素时会进行边界检查,如果索引越界,会抛出std::out_of_range异常,便于我们及时发现和处理问题。

2.2 输入验证与过滤

对用户输入进行合法性检查是至关重要的,它是防止许多安全漏洞的第一道防线。在接收用户输入时,我们不能盲目信任用户输入的数据,必须对其进行严格的验证,确保输入数据符合预期的格式和范围。比如对于一个要求输入整数的场景,我们需要检查用户输入的是否确实是整数,而不是其他非法字符。可以使用正则表达式来进行复杂的格式验证,比如验证邮箱地址、手机号码等。

对于特殊字符,需要进行妥善处理。在 Web 应用中,一些特殊字符可能会被攻击者利用来进行 SQL 注入或 XSS 攻击。对于可能导致 SQL 注入的特殊字符,如单引号(')、双引号(")、分号(;)等,在将用户输入拼接到 SQL 语句之前,要进行转义处理,或者使用参数化查询的方式来避免 SQL 注入。对于可能导致 XSS 攻击的特殊字符,如尖括号(<、>)、单引号、双引号等,在输出到页面时,要进行 HTML 转义,将这些特殊字符转换为 HTML 实体,如<转换为&lt;,>转换为&gt;,这样可以防止恶意脚本在浏览器中执行。

// 简单的输入验证示例,检查输入是否为正整数
#include <iostream>
#include <string>
#include <regex>bool validatePositiveInteger(const std::string& input) {std::regex pattern("^[1-9]\\d*$");return std::regex_match(input, pattern);
}int main() {std::string input;std::cout << "请输入一个正整数: ";std::cin >> input;if (validatePositiveInteger(input)) {std::cout << "输入合法" << std::endl;} else {std::cout << "输入不合法,请输入一个正整数" << std::endl;}return 0;
}

2.3 安全的 API 使用

在 C++ 编程中,一些传统的函数存在安全风险,应该避免使用,转而使用其安全替代函数。例如,strcpy函数用于复制字符串,但它不会检查目标缓冲区的大小,如果源字符串长度超过目标缓冲区的大小,就会导致缓冲区溢出。而strncpy函数则是strcpy的安全版本,它可以指定最多复制的字符数,从而避免缓冲区溢出。sprintf函数在格式化字符串时也存在类似的问题,可能导致缓冲区溢出,安全的替代函数是snprintf,它可以指定目标缓冲区的大小,确保不会写入过多的数据。

除了字符串操作函数,还有一些其他函数也需要注意安全使用。比如gets函数用于从标准输入读取一行数据,它无法限制读取的字符数,容易导致缓冲区溢出,应该使用fgets函数替代,fgets函数可以指定读取的最大字符数,从而保证安全。在使用这些函数时,一定要仔细阅读文档,了解其参数含义和潜在风险,确保正确使用,以避免安全漏洞的出现。

三、安全编译与工具防护

3.1 编译器安全选项

在 C++ 编程中,利用编译器的安全选项可以有效增强程序的安全性。以 GCC 编译器为例,-fstack-protector选项能够为程序提供堆栈保护机制。当程序被编译时,该选项会在函数的栈帧中插入一个特殊的保护值,也就是我们常说的 “金丝雀值”。在函数返回之前,编译器会自动生成代码来检查这个 “金丝雀值” 是否被篡改。如果 “金丝雀值” 发生了变化,那就意味着堆栈可能遭受了溢出攻击,此时程序会立即终止,从而有效阻止攻击者利用溢出漏洞执行恶意代码。比如在一个网络服务器程序中,很多函数都涉及处理客户端请求数据,如果没有堆栈保护,攻击者可能通过精心构造的请求数据引发堆栈溢出,进而控制程序执行流程。而启用-fstack-protector选项后,这种攻击方式就很难得逞了。

Visual Studio 中的/GS选项同样有着重要作用。它的工作原理是在函数调用发生时,向栈帧内压入一个额外的随机 DWORD,我们将其称为 “Security Cookie”。这个 “Security Cookie” 位于 EBP(帧指针寄存器)之前,同时系统还会在.data内存区域中存放一个 “Security Cookie” 的副本。当栈中发生溢出时,“Security Cookie” 会首先被覆盖,然后才是 EBP 和返回地址。在函数返回之前,系统会执行一个安全验证操作,也就是检查 “Security Cookie” 是否被改变。如果 “Security Cookie” 被修改,就证明程序受到了缓冲区溢出攻击,从而采取相应的防御措施,比如终止程序运行。在默认情况下,目前版本的 Visual Studio 是启用了这个编译选项的,但开发者仍然需要了解其原理和作用,以便在必要时进行合理的配置和调整。

3.2 静态代码分析工具

Clang-Tidy 是一款基于 Clang 的强大 C++ 静态代码分析工具,它能够在不运行程序的情况下,对代码进行全面的检查,发现其中潜在的问题。它提供了丰富的检查器(checks),可以检测多种类型的问题。在代码风格方面,它能检查代码是否符合常见的编码规范,比如缩进是否正确、命名是否合理等;对于潜在的编程错误,像空指针引用、数组越界访问、内存泄漏等问题,它也能敏锐地察觉到;在性能方面,它可以给出一些优化建议,比如某些循环是否可以优化、是否存在不必要的计算等;同时,它还能针对现代 C++ 特性的使用给出建议,帮助开发者更好地利用新特性来提升代码质量和效率。

使用 Clang-Tidy 非常简单,在命令行中,只需指定要检查的文件即可,例如clang-tidy your_file.cpp。它还支持通过配置文件来定制检查规则,我们可以在项目根目录下创建一个.clang-tidy文件,在其中明确指定要启用或禁用的检查器。比如Checks: ‘-,modernize-,cppcoreguidelines-,clang-analyzer-’,这条配置表示禁用所有默认检查,然后启用与现代化改造、C++ 核心准则以及 Clang 静态分析相关的检查器。如果发现问题,Clang-Tidy 还可以提供自动修复建议,使用-fix选项就可以自动应用修复,大大提高了代码修复的效率。

Cppcheck 也是一款优秀的开源 C++ 静态代码分析工具,它专注于发现编译器通常难以察觉的缺陷。它可以检查单个文件,比如cppcheck foo.c;也可以检查整个文件夹,如cppcheck path。在检查过程中,我们可以通过–enable选项来指定要启用的检查规则,默认情况下是–enable=error,只检查错误。如果想要更全面的检查,可以使用–enable=all,它会启用所有类型的检查,包括错误、警告、风格问题、移植性警告、性能建议以及一些有趣的信息等。还可以通过–enable=unusedFuntion path这样的方式来检查特定类型的问题,比如未使用的函数。我们还能将检查结果保存到文件中,方便后续查看和分析,只需要使用重定向符号>即可,如cppcheck your_file.cpp > result.txt。

3.3 动态漏洞检测工具

Valgrind 是一款功能强大的动态漏洞检测工具,尤其在检测内存相关问题方面表现出色。它可以在程序运行时,实时监测内存的使用情况,帮助我们发现内存泄漏、内存越界访问等严重问题。使用 Valgrind 非常方便,只需要在运行程序时加上相应的参数即可。例如,使用valgrind --leak-check=full ./your-program命令,–leak-check=full表示进行全面的内存泄漏检查,它会详细地报告程序中所有未释放的内存块的信息,包括内存块的大小、分配的位置等,让我们能够准确地定位到内存泄漏的源头。

AddressSanitizer(ASan)是 LLVM Clang 编译器自带的内存调试工具,它同样能够有效地检测内存越界、使用已释放内存等问题。使用 ASan 也很简单,不需要单独安装,只需要在编译代码时加上-fsanitize=address选项即可。例如clang++ -fsanitize=address -o example example.cpp,这样编译出来的程序在运行时,ASan 就会自动检测内存错误。当程序中出现内存越界访问时,ASan 会立即捕获到这个错误,并输出详细的错误信息,包括错误发生的位置、相关的代码行以及内存访问的具体情况等,这对于我们快速定位和解决内存问题非常有帮助。

四、实战项目:安全的用户登录系统

4.1 项目需求

在当今数字化时代,用户登录系统是各类应用程序的关键组成部分,其安全性至关重要。本实战项目旨在打造一个安全可靠的用户登录系统,主要涵盖以下几个核心需求:

  • 用户名密码验证:这是登录系统的基础功能,系统需要准确无误地验证用户输入的用户名和密码是否与数据库中存储的信息一致。只有当两者完全匹配时,才允许用户成功登录,从而确保只有合法用户能够访问系统资源。
  • 密码加密存储:为了防止用户密码在数据库中以明文形式存储而带来的安全风险,需要采用安全的加密算法对用户密码进行加密处理后再存储。这样即使数据库不幸被泄露,攻击者也难以直接获取用户的真实密码,大大增强了用户密码的安全性。
  • 防 SQL 注入:由于登录系统通常需要与数据库进行交互,SQL 注入攻击成为了一个潜在的重大威胁。因此,系统必须采取有效的防护措施,避免攻击者通过在输入框中注入恶意 SQL 语句来获取、修改或删除数据库中的敏感数据,确保数据库的完整性和安全性。
  • 防暴力破解:暴力破解攻击是指攻击者通过不断尝试各种可能的用户名和密码组合来试图登录系统。为了抵御这种攻击,系统需要设置合理的登录尝试次数限制,一旦用户的登录尝试次数超过设定的阈值,就暂时锁定该账户一段时间,同时还可以采用验证码、短信验证等多因素认证方式,增加攻击者破解的难度,保障系统的安全稳定运行。

4.2 安全代码实现与漏洞防护措施

#include <iostream>
#include <string>
#include <mysql/mysql.h>
#include <openssl/sha.h>
#include <regex>// 数据库连接配置
const std::string DB_HOST = "localhost";
const std::string DB_USER = "root";
const std::string DB_PASS = "password";
const std::string DB_NAME = "your_database";// 加密密码
std::string encryptPassword(const std::string& password) {unsigned char hash[SHA256_DIGEST_LENGTH];SHA256_CTX sha256;SHA256_Init(&sha256);SHA256_Update(&sha256, password.c_str(), password.size());SHA256_Final(hash, &sha256);char hex_chars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};std::string encrypted_password = "";for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {encrypted_password += hex_chars[(hash[i] & 0xF0) >> 4];encrypted_password += hex_chars[hash[i] & 0x0F];}return encrypted_password;
}// 验证用户名和密码
bool validateUser(const std::string& username, const std::string& password) {MYSQL* conn = mysql_init(nullptr);if (!mysql_real_connect(conn, DB_HOST.c_str(), DB_USER.c_str(), DB_PASS.c_str(), DB_NAME.c_str(), 0, nullptr, 0)) {std::cerr << "数据库连接失败: " << mysql_error(conn) << std::endl;mysql_close(conn);return false;}// 使用参数化查询防止SQL注入std::string query = "SELECT password FROM users WHERE username = ?";MYSQL_STMT* stmt = mysql_stmt_init(conn);if (!stmt) {std::cerr << "初始化语句失败: " << mysql_error(conn) << std::endl;mysql_close(conn);return false;}if (mysql_stmt_prepare(stmt, query.c_str(), query.length())) {std::cerr << "准备查询失败: " << mysql_error(conn) << std::endl;mysql_stmt_close(stmt);mysql_close(conn);return false;}MYSQL_BIND bind[1];memset(bind, 0, sizeof(bind));std::string username_copy = username;bind[0].buffer_type = MYSQL_TYPE_STRING;bind[0].buffer = (char*)username_copy.c_str();bind[0].is_null = 0;bind[0].length = 0;if (mysql_stmt_bind_param(stmt, bind)) {std::cerr << "绑定参数失败: " << mysql_error(conn) << std::endl;mysql_stmt_close(stmt);mysql_close(conn);return false;}if (mysql_stmt_execute(stmt)) {std::cerr << "执行查询失败: " << mysql_error(conn) << std::endl;mysql_stmt_close(stmt);mysql_close(conn);return false;}MYSQL_RES* result = mysql_stmt_result_metadata(stmt);if (!result) {std::cerr << "获取结果元数据失败: " << mysql_error(conn) << std::endl;mysql_stmt_close(stmt);mysql_close(conn);return false;}int num_fields = mysql_num_fields(result);MYSQL_BIND* result_bind = new MYSQL_BIND[num_fields];memset(result_bind, 0, sizeof(MYSQL_BIND) * num_fields);std::string stored_password;result_bind[0].buffer_type = MYSQL_TYPE_STRING;result_bind[0].buffer = (char*)stored_password.data();result_bind[0].is_null = 0;result_bind[0].length = 0;if (mysql_stmt_bind_result(stmt, result_bind)) {std::cerr << "绑定结果失败: " << mysql_error(conn) << std::endl;mysql_stmt_close(stmt);mysql_close(conn);delete[] result_bind;return false;}if (mysql_stmt_fetch(stmt) == 0) {std::string encrypted_password = encryptPassword(password);if (encrypted_password == stored_password) {mysql_stmt_close(stmt);mysql_close(conn);delete[] result_bind;return true;}}mysql_stmt_close(stmt);mysql_close(conn);delete[] result_bind;return false;
}int main() {std::string username, password;std::cout << "请输入用户名: ";std::cin >> username;std::cout << "请输入密码: ";std::cin >> password;// 简单的输入验证,检查用户名是否只包含字母和数字if (!std::regex_match(username, std::regex("^[A-Za-z0-9]+$"))) {std::cerr << "用户名只能包含字母和数字" << std::endl;return 1;}if (validateUser(username, password)) {std::cout << "登录成功" << std::endl;} else {std::cout << "用户名或密码错误" << std::endl;}return 0;
}

在上述代码中,encryptPassword函数利用openssl/sha.h库提供的SHA256算法对用户输入的密码进行加密处理。在validateUser函数中,通过mysql_real_connect函数与 MySQL 数据库建立连接,然后使用参数化查询来执行 SQL 语句,避免了直接将用户输入拼接到 SQL 语句中,有效防止了 SQL 注入攻击。在执行查询前,先使用mysql_stmt_prepare函数对 SQL 语句进行预处理,再通过mysql_stmt_bind_param函数将用户输入的用户名绑定到查询语句中,最后使用mysql_stmt_execute函数执行查询。从数据库中获取到存储的密码后,将用户输入的密码进行加密,与存储的加密密码进行比对,从而验证用户身份。同时,在main函数中,使用正则表达式对用户输入的用户名进行简单验证,确保用户名只包含字母和数字,进一步增强了系统的安全性。

4.3 安全测试

  • 暴力破解模拟:可以使用工具如 Hydra 来模拟暴力破解攻击。Hydra 是一款功能强大的开源密码破解工具,它支持多种协议的密码破解,包括 HTTP、FTP、SMTP 等。在对用户登录系统进行暴力破解模拟时,首先需要确定目标系统的登录 URL、用户名和密码输入字段的名称等信息。然后,使用 Hydra 的命令行参数来配置破解任务,例如指定破解的协议为 HTTP,目标 URL 为登录页面的地址,用户名文件为包含可能用户名的文件,密码文件为包含可能密码的字典文件等。通过运行 Hydra 工具,它会自动尝试使用字典文件中的用户名和密码组合进行登录,观察系统的反应,检测系统是否能够有效抵御暴力破解攻击。如果系统在设定的登录尝试次数后成功锁定账户,或者能够有效识别并阻止来自同一 IP 地址的大量登录尝试,说明系统在防暴力破解方面表现良好。
  • SQL 注入测试:利用 SQLMap 工具进行 SQL 注入测试。SQLMap 是一款专门用于检测和利用 SQL 注入漏洞的强大工具,它支持多种数据库管理系统,如 MySQL、Oracle、PostgreSQL 等。使用 SQLMap 时,只需将目标系统的登录页面 URL 作为参数输入,SQLMap 会自动检测该 URL 是否存在 SQL 注入漏洞。它会尝试发送各种精心构造的包含恶意 SQL 语句的请求,例如在用户名或密码输入框中注入’ OR ‘1’='1等典型的 SQL 注入语句,观察系统的响应。如果系统存在 SQL 注入漏洞,SQLMap 能够获取到数据库中的敏感信息,如用户名、密码等。通过这种方式,可以验证之前采取的防止 SQL 注入措施是否有效,若系统能够正确处理这些恶意输入,不返回任何敏感信息,且没有执行恶意 SQL 语句,说明系统在防 SQL 注入方面是安全可靠的。
http://www.dtcms.com/a/449221.html

相关文章:

  • 西安网站托管排名网站建设技术支持有什么
  • 【OTA升级】英飞凌TC397实现刷写失败回滚(A/B分区)
  • 揭开 C++ vector 底层面纱:从三指针模型到手写完整实现
  • 【嵌入式】【GIT】终端中文乱码修复
  • LabVIEW 并行 For 循环应用与对比
  • 广东石油化工建设集团网站设计工作室发展前景
  • (五)Vue.js 高级语法②
  • Ansible核心架构深度剖析:从源码看IT自动化的“简单“哲学
  • 深圳建站公司价格内网访问wordpress很慢
  • 网站制作咨网站备案账号密码
  • 继续补js
  • set authentication password cipher 概念及题目
  • 【高级版】沃德政务招商系统源码+uniapp小程序
  • 什么时候需要close()释放资源
  • 小迪安全v2023学习笔记(九十七天)—— 云原生篇KubernetesK8s安全APIKubelet未授权访问容器执行
  • 网站建设itcask单页网站seo
  • 设计模式学习[20]---桥接模式
  • 【第三次全国土壤普查】-土壤类型图报告编制及评分标准
  • python爬虫(一) ---- 静态html数据抓取
  • 《剑指Offer:单链表操作入门——从“头删”开始破解面试》
  • 网站备案号怎么查询做设计私活的网站
  • 微信小程序入门学习教程,从入门到精通,WXS语法详解(10)
  • 深圳做网站公司哪家好在线绘画网站
  • CodeX CLI安装+MCP适配与VSCode部署(Win)
  • 手写MyBatis第95弹:异常断点精准捕获MyBatis深层BUG
  • 网站的结构是什么样的鹰潭律师网站建设
  • Rust多线程详解
  • tcp 服务器的设计思路
  • 基础架构安全和云原生安全的融合~K8S安全和传统安全~K8S和安全融合~综合安全大饼
  • Python全栈(基础篇)——Day05:后端内容(dict与set+while循环+for循环+实战演示+每日一题)