news 2026/9/30 3:12:13

JS直连MySQL全指南:mysql2选型、连接池调优与SQL安全实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS直连MySQL全指南:mysql2选型、连接池调优与SQL安全实践

简介:在JavaScript中直接访问MySQL数据库,可通过JSDBC(JavaScript DataBase Connector)组件实现。JSDBC为Web前端开发者提供了一条绕过后台服务器直连数据库的路径,省去部署Java运行环境与编写复杂JDBC调用的开销,适合AJAX调试、内部工具页面和快速原型验证等场景。文档内含完整的OCX控件安装与页面引入方式,并给出了connectMySQL()、insertMySQL()、execDMLMySQL()、selectMySQL()、updateMySQL()、deleteMySQL()、closeMySQL()等封装函数的JavaScript源码,覆盖连接、增删改查、错误获取与关闭连接的常用操作;查询结果通过分隔符解析后以数组形式返回,代码中已包含行、字段分隔符的处理细节,可直接复制到项目中使用,或作为基础封装继续扩展。资源共1个Word文档,压缩包仅31KB,内容精炼实用,适合熟悉HTML/JavaScript但缺少后端支持的前端工程师,或需要在本地快速验证MySQL逻辑的调试场景。该资源已有6926人学习,是轻量级MySQL前端访问思路的典型案例参考。

1. 我先说透:JS 直接访问 MySQL,到底是哪一层在“直连”

第一次拿到“JS 直接访问数据MySQL”这个诉求,十有八九是被浏览器里的 JS 卡住了。JS 本身只是语言,真正能连数据库的是运行环境。浏览器里的 JS 没有原始 TCP Socket 能力,也不可能让你在地址栏里访问 3306 端口,所以“直接访问”的真实形态是:在 Node.js 服务端或 Node 脚本里,用 mysql2 这个库建立到 MySQL 的 TCP 连接,然后把 SQL 语句整个发过去。这个方案能解决的问题很实际:小团队不打算引入 ORM 或网关层,想用一段十几行的脚本查库、同步数据,或者给前端页面快速提供数据接口。它适合两类人:一类是手里只有 Node 环境、想绕过命令行写 SQL 的脚本爱好者,另一类是前端转全栈、需要把第一个数据接口跑起来的人。

2. 搭最小可访问链路:用 mysql2 先跑通一条查询

2.1 为什么选 mysql2,而不是老牌的 mysql

如果你去搜“JS 连接 MySQL”,最先看到的可能是 mysql 这个包,但我一般会直接绕开。mysql 包的问题不是不能用,而是它维护节奏慢,对 MySQL 8.0 默认的 caching_sha2_password 认证插件支持得比较别扭,很多人在这一步就翻车。mysql2 在主线上兼容更稳,而且多做了几件值得换过去的事:

  • 底层用的是二进制协议而不是文本协议,解析效率更高;
  • 原生支持 Promise API,不用再手动把它包成 Promise;
  • 查询占位符和预处理语句支持完善,降低 SQL 注入风险;
  • 有更完整的类型映射处理,对时间、小数、JSON 字段的可控性更强。

下面这张表是差异最大、直接影响落地的地方:

对比项mysql 包mysql2
MySQL 8.0 默认认证老版本容易报错直接支持
Promise API需要额外封装引入mysql2/promise即可
预处理语句支持一般较完整
连接池参数较少更细粒度
维护活跃度低高

实际项目里,我见过因为 mysql 包导致ER_NOT_SUPPORTED_AUTH_MODE的报错,把mysql换成mysql2后就恢复了。所以这个选型不是无意义的“先进”,而是为了少踩一个常见坑。

2.2 最小连接代码:一条 SQL 从 Node 跑到 MySQL

先把环境准备好。下面的示例假设你已经装好 MySQL,并且有一个能用的库和账号。代码里用mysql2/promise导入:

const mysql = require('mysql2/promise'); async function main() { const conn = await mysql.createConnection({ host: '127.0.0.1', port: 3306, user: 'app_user', password: 'your_password', database: 'demo_db' }); const [rows] = await conn.query( 'SELECT id, name, status FROM user WHERE status = ?', [1] ); console.log('rows:', rows); await conn.end(); } main().catch(err => { console.error('query failed:', err); process.exit(1); });

这段代码里最值得说的是createConnection的返回值不是连接本身,而是一个连接对象,它具备query、execute、end等方法。query返回的是一个数组,第一项才是查询结果rows,第二项是fields字段信息,所以你看到我用const [rows] = await conn.query(...)来解构。?是值占位符,后面[1]会安全地替换上去,不要自己在 SQL 里拼接字符串。

host用127.0.0.1而不是localhost是刻意为之。原因后面排查章节会细说:localhost在部分环境里会触发 MySQL 走 Unix Socket 而不是 TCP,连不上时很容易绕晕。

2.3 查询结果结构:为什么打印出来全是 RowDataPacket

第一次跑通后,很多人会盯着终端里的RowDataPacket发愣。这其实是 mysql2 对查询结果行的封装对象。它看起来像普通对象,能正常读取字段,但不是纯粹的 plain object。看这个例子:

const [rows] = await conn.query('SELECT id, name, created_at FROM user LIMIT 1'); console.log(rows); // [ RowDataPacket { id: 1, name: '张三', created_at: 2025-01-01T10:00:00.000Z } ] console.log(rows[0].name); // '张三' console.log(Object.prototype.toString.call(rows[0])); // [object Object]

这里没有隐藏 bug,但有一个落地的坑:有些序列化库、日志库会对对象原型敏感,导致你JSON.stringify(rows)没问题,但拿去发给前端再回来就有隐患。所以当我们想获得干净数据时,常见做法是手动映射:

const rows = result.map(row => ({ id: row.id, name: row.name, createdAt: row.created_at }));

另一个值得关注的是fields。它包含了每列的元信息,比如字段名、数据类型、表名。你要做动态导出 CSV、拼接查询结果时,可以用fields.map(f => f.name)拿到列名列表。

2.4 先装库,再写代码:用命令行把前置条件压到最低

很多 JS 连接失败的问题,根源根本不在 JS,而是 MySQL 本身没装好、没启动、权限没给。所以我一般先搜一下“mysql 安装教程”,把服务端装好,然后用同样的账号跑一遍 mysql 命令行,确认数据库本身是通的:

mysql -h127.0.0.1 -P3306 -uapp_user -p demo_db -e "SELECT COUNT(*) FROM user;"

这里-e表示执行一条 SQL 后退出。如果这一步成功,说明网络、端口、账号、库权限都没问题;如果这一步就报错,就别急着调试 Node。

常见的情况有两个:

  • ERROR 1045 (28000): Access denied for user ...:账号密码错误,或者账号不允许从当前 IP 登录;
  • ERROR 1044 (42000): Access denied for database ...:账号存在,但对demo_db没有权限。

权限修正可以先登录 root 账号执行:

GRANT SELECT, INSERT, UPDATE, DELETE ON demo_db.* TO 'app_user'@'%'; FLUSH PRIVILEGES;

注意%是允许任何主机,生产环境按需换成具体 IP。命令行验证通过后,再回过来跑 Node 脚本,排错范围会小很多。

提示:生产库里别给应用账号开ALL PRIVILEGES,按操作类型给到最小权限,后面出事时后悔药不好找。

3. 连接池与参数调优:直连脚本里那几个要命的默认值

3.1 为什么不是每次 createConnection,而要用 createPool

用一个脚本跑一条 SQL,createConnection没问题。但如果你在 Web 服务里,每个请求都新建连接、用完再end(),高并发下 MySQL 会看到大量连接频繁建立和销毁,TCP 层也堆积一堆 TIME_WAIT,很快会出现Too many connections。我常用的做法是换成createPool:

const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: '127.0.0.1', user: 'app_user', password: 'your_password', database: 'demo_db', waitForConnections: true, connectionLimit: 10, maxIdle: 10, idleTimeout: 60000, queueLimit: 0, connectTimeout: 10000 });

连接池做的事情是:在内部维护多个连接对象,调用方每次只是“借”一个连接,用完归还;池子里没有空闲连接时,请求会排队等待,而不是立刻新建连接。这样就避免了反复握手的开销。

在实际小项目里,你可以把pool定义在一个独立模块中导出,所有 SQL 都走它。需要注意的是,池不等于无限连接,它不是 MySQL 的max_connections避风港。

3.2 连接池参数怎么调:不要凭感觉给 1000

连接池的参数很多,但真正需要优先关注的是这几个:

参数默认值作用常见设置
connectionLimit10池里最多同时存在的连接数10~50,视并发
maxIdle同connectionLimit最多保留多少个空闲连接不要大于connectionLimit
queueLimit0排队等待的最大请求数,0 表示不限制0 或并发峰值的 2 倍
waitForConnectionstrue池满时是否排队等待一般保持 true
idleTimeout60000空闲连接多久被关闭释放按业务波动定
connectTimeout10000建立连接的超时时间网络差可以调大

connectionLimit并不是越大越好。MySQL 自己有一个max_connections,默认通常在 151 左右。你把 Node 池子调到 200,可能直接把数据库打挂。安全的做法是先用 SQL 看数据库上限:

SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Threads_connected';

把connectionLimit设成max_connections的一半以内,同时保证同一时刻业务并发不超过它。如果峰值高,优先排查慢查询,而不是盲目加连接。

3.3 字符集、时区和小数类型:JS 拿到的不是你以为的值

这是我实践中容易被忽略的一层。第一是字符集。连接配置里必须显式写明charset:

charset: 'utf8mb4'

如果不写,mysql2 有默认值,但和表的字符集不一定一致,很容易出现中文读出来正常,写进去变成乱码的情况。而且utf8mb4和utf8mb4_general_ci是两回事,前者是字符集,后者是排序规则,连接池配置里一般指定charset即可。

第二是时区。MySQL 返回DATETIME时,Node 端如果配置不对,会被本地时区干扰。你可以在连接配置里显式声明:

timezone: '+08:00'

或统一用 UTC:

timezone: 'Z'

第三是小数。DECIMAL类型在 mysql2 里默认以字符串返回,原因是 JS 的Number无法完整表达高精度小数。你如果直接做parseFloat,精度就丢了。宁可让前端拿到字符串,需要计算时统一用Decimal之类的精度库。

3.4 连接泄漏:写完 release 不到位置,迟早爆池

连接池最大的坑不是配置,而是借了不还。下面这段代码就是反面教材:

// 错误示范:手动 getConnection 后没有 release async function queryUserBad(uid) { const conn = await pool.getConnection(); const [rows] = await conn.query('SELECT * FROM user WHERE id = ?', [uid]); return rows; }

第一次调用它很正常,第二次也还行,跑到几十次,池里 10 个连接全部被占满,后续请求开始排队,再之后queueLimit满了直接报超时。正确的写法是拿到连接后立刻进入try...finally:

async function queryUserGood(uid) { const conn = await pool.getConnection(); try { const [rows] = await conn.query('SELECT * FROM user WHERE id = ?', [uid]); return rows; } finally { conn.release(); } }

这里finally保证无论查询成功还是抛异常,连接都会归还。很多人以为失败就不用还,这恰恰是翻车重灾区。如果你用的是pool.query这种快捷方法,它内部会自动获取并释放连接,不需要也不应该再手动getConnection。

4. JS 拼 SQL 的正确姿势:占位符、排序与大小写边界

4.1 占位符:? 和 ?? 的区别必须分清

“JS 直接访问 MySQL”最大的安全隐患,就是把 JS 变量用模板字符串直接写进 SQL。比如:

const sql = `SELECT * FROM user WHERE name = '${name}'`;

这就是给 SQL 注入敞开了门。mysql2 提供了两个占位符,?是值占位符,??是标识符占位符,作用完全不同:

const name = "Alice'; DROP TABLE user;--"; // 正确:值用 ? const rows = await pool.query( 'SELECT * FROM user WHERE name = ?', [name] ); // 正确:表名/列名用 ?? const cols = ['id', 'name']; await pool.query( 'SELECT ??, ?? FROM ?? WHERE id = ?', [...cols, 'user', 1] );

?会被当成字符串值,自动做转义;??会被当成表名或列名。不能混淆:如果把表名用?,会被包成字符串字面量,SQL 直接语法错误;如果把值用??,会被原样展开,注入风险依旧。

在 mysql2 里,query和execute都可以用占位符。execute走更严格的预处理协议,对相同 SQL 重复执行时有性能优势,但批量插入时的表现不一样,我会在后面的批量场景单独说。

4.2 js 判断字符串是否包含,不等于 SQL 的 LIKE

很多前端同事写的习惯是:在 JS 里用includes判断字符串包含某个关键字,然后希望 SQL 也能像 JS 一样模糊匹配。到数据库侧,对应的是LIKE:

const keyword = 'ali'; const [rows] = await pool.query( 'SELECT id, name FROM user WHERE name LIKE CONCAT("%", ?, "%")', [keyword] );

这里用LIKE CONCAT('%', ?, '%'),既安全又避免手动拼%导致漏转义。但 JS 的includes是大小写敏感的,MySQL 默认的utf8mb4_general_ci排序规则是大小写不敏感的。同一个关键字,JS 判断和 SQL 判断可能结论不同。想让 SQL 表现更接近 JS,可以显式加COLLATE:

await pool.query( 'SELECT id, name FROM user WHERE name LIKE CONCAT("%", ?, "%") COLLATE utf8mb4_bin', [keyword] );

utf8mb4_bin按二进制比较,大小写敏感,更贴近 JS 的includes。反过来,如果业务必须忽略大小写,保持默认排序规则即可。

还有个隐藏坑是用户搜索关键字里带着%或_。它们对 LIKE 是通配符,必须转义:

function escapeLike(keyword) { return keyword.replace(/[\\%_]/g, (m) => '\\' + m); }

使用:

await pool.query( 'SELECT id, name FROM user WHERE name LIKE CONCAT("%", ?, "%") ESCAPE "\\"', [escapeLike(keyword)] );

不做这一步,用户搜“10%”会发现所有以 10 开头的字符串都出来了。

4.3 ORDER BY 排序:表字段别直接拼进 SQL

动态排序是最容易忽略注入的地方。很多人在ORDER BY上直接拼字段名,因为?占位符只适合值。实际上??就是为这个准备的,但要配合白名单才安全:

const allowFields = ['id', 'name', 'created_at']; const orderField = allowFields.includes(reqSort) ? reqSort : 'id'; const orderDir = reqDir === 'ASC' || reqDir === 'DESC' ? reqDir : 'DESC'; const [rows] = await pool.query( `SELECT id, name, created_at FROM user ORDER BY ?? ${orderDir}`, [orderField] );

orderDir没有用占位符,因为它本身只有两个固定值,直接用白名单约束。orderField用白名单校验后才交给??,可以在防止注入的同时避免无效字段名。

顺序上还要注意 NULL 的位置。MySQL 排序默认把 NULL 放在最前(ASC),业务里经常要把空值排到最后,可以这样写:

ORDER BY (column IS NULL), column ASC

这条在 JS 里拼接时,列名仍然用??处理。

4.4 批量插入和 ON DUPLICATE KEY UPDATE

mysql2 的query支持一种比较特殊的VALUES ?写法,专门用于批量插入。传参是一个二维数组:

const newUsers = [ ['zhangsan', 1], ['lisi', 2] ]; const [result] = await pool.query( 'INSERT INTO user (name, dept_id) VALUES ?', [newUsers] ); console.log(result.affectedRows); // 插入成功条数 console.log(result.insertId); // 第一条自增 id

注意批量插入时用的是VALUES ?而不是VALUES (?, ?)。这也是 mysql2 文档里比较特别的一处,容易踩。用execute时这个能力可能不同,所以我直接用query。

业务里常要求“存在则更新,不存在则插入”,可以追加ON DUPLICATE KEY UPDATE:

const [result] = await pool.query( 'INSERT INTO user (name, dept_id) VALUES ? ON DUPLICATE KEY UPDATE name = VALUES(name)', [newUsers] );

这里有个值得注意的现象:affectedRows在冲突更新时不是简单相加,MySQL 会按两倍计数(插入 1 行 + 更新 1 行)这类逻辑返回,所以拿affectedRows判断条数时别只看字面量。

5. 直连报错排查:从 2002 socket 到 SSL 与连接池耗尽

5.1 error 2002:Can't connect to local MySQL server through socket

这是“JS 连不上 MySQL”里最有迷惑性的报错。完整报错长这样:

Error: connect ENOENT /tmp/mysql.sock Error: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

现象:命令行 mysql 能连,Node 脚本却报找不到 socket 文件。原因是连接配置里写了host: 'localhost'。在 Node 和 MySQL 客户端的约定里,localhost通常意味着走 Unix Socket,而不是 TCP。而 Linux 上 MySQL 的 socket 文件不总在/tmp/mysql.sock,一旦路径不对,就会出现上面的错误。

解决就两招,选择其一:

// 方案一:强制走 TCP host: '127.0.0.1' // 方案二:显式声明 socket 路径 socketPath: '/var/run/mysqld/mysqld.sock'

我倾向方案一,因为它让连接行为和端口一致,调试也简单。如果 MySQL 启动时开启了skip-networking,那就只能走 socket,此时必须用方案二,并且确认 Node 进程对 socket 文件有访问权限。

5.2 mysql ssl 连接错误:该关还是该配

SSL 相关的报错五花八门,常见的是:

Error: Server requires secure connection Error: SSL routines:WRONG_VERSION_NUMBER Error: Client does not support authentication protocol requested by server

第一种现象是 MySQL 服务端配置了require_secure_transport=ON,强制要求所有连接走 TLS。这时候你客户端没开 SSL,自然被拒。解决:要么在连接配置里带上 CA 证书,要么和运维确认后临时放宽数据库侧校验:

// 内网测试环境,数据库已经允许非 SSL 时 const conn = await mysql.createConnection({ host: '127.0.0.1', user: 'app_user', password: 'your_password', database: 'demo_db', ssl: { rejectUnauthorized: false } });

rejectUnauthorized: false只是跳过证书校验,并不代表建立安全连接。这不是推荐做法,它只适合数据库侧本来就不强制 SSL 的临时链路。生产环境应该使用云数据库提供的 CA 证书:

const fs = require('fs'); const conn = await mysql.createConnection({ host: 'your-db.example.com', ssl: { ca: fs.readFileSync('./ca.pem') } });

还有一种更隐蔽的情况:MySQL 8 默认支持 TLS,但服务端没有配置正确证书,导致握手时协议版本对不上。这时候排查重点从 Node 代码挪到数据库服务端的 SSL 配置,不要只在客户端反复试。

注意:如果你不需要加密连接,直接把ssl相关项全部删掉,不要随手写ssl: {},空对象一样会触发加密握手。

5.3 连接池耗尽:ETIMEDOUT 是表象

当请求开始报connect ETIMEDOUT、getConnection timeout,或者pool has been closed,问题往往不在网络,而在连接池。典型现象是服务刚启动一切正常,运行一段时间后接口全部超时。

原因是连接被借出但没有归还。前面提到的getConnection后没release是最常见的一种;另一种是查询本身很慢,池里的连接都被慢查询占着,新请求只能排队,排到超时就是ETIMEDOUT。

排查步骤我固定是这样:

mysql -u root -p

进入 MySQL 后执行:

SHOW FULL PROCESSLIST; SHOW GLOBAL STATUS LIKE 'Threads_connected';

如果看到大量线程处于Sleep或Query状态,并且数量接近connectionLimit,再结合代码,基本能判断是泄漏还是慢查询。临时恢复服务可以把池子调大,但根治必须在代码里补finally,或者给慢 SQL 加索引。

5.4 MySQL 8 认证插件和中文乱码的坑

MySQL 8 默认把账号认证插件改成了caching_sha2_password。如果你用的是较旧的 mysql 包,或者某些可视化工具,会直接报:

Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server

mysql2 对这种插件支持得很好,所以第一反应是升级连接库,而不是改数据库。但如果业务里确实有一些老系统无法升级,临时解决方案是把账号改成旧的认证方式:

ALTER USER 'app_user'@'%' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;

注意这会让账号失去新插件的部分能力,不是长久之计。另一个绕不开的是中文乱码。现象:命令行查询正常,JS 写入后读回来变成???,或读出来是乱码。原因基本是两个地方不一致:连接charset没写成utf8mb4,或者表和库的排序规则不是utf8mb4。先统一三层:

ALTER DATABASE demo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

然后在 Node 连接配置里也写上:

charset: 'utf8mb4'

三层统一后,中文乱码的坑一般就填平了。

6. 把直连封装成业务可用的 async 接口:事务与重试的最后一公里

6.1 一个简短的 db.js 封装

不管你是写脚本还是做接口,我建议把直连代码收敛成一个模块。下面是常用的最小封装,核心是query和withTransaction:

const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: '127.0.0.1', user: 'app_user', password: 'your_password', database: 'demo_db', waitForConnections: true, connectionLimit: 10 }); async function query(sql, params) { const conn = await pool.getConnection(); try { const [rows] = await conn.query(sql, params); return rows; } finally { conn.release(); } } async function withTransaction(fn) { const conn = await pool.getConnection(); await conn.beginTransaction(); try { const result = await fn(conn); await conn.commit(); return result; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } } module.exports = { pool, query, withTransaction };

withTransaction的回调参数是同一个连接,事务里的所有查询必须走这个连接,不要再去pool.query,否则它们不在同一个事务里。这个细节我吃过亏:并发场景下两段逻辑各拿一个连接,commit 和 rollback 根本没有共同事务上下文,数据就乱了。

6.2 断线重试:哪些查询能重试,哪些不能

连接偶发断开在实际运维里太常见。网络抖动、MySQL 重启、连接空闲被杀,都会让应用抛ECONNRESET或PROTOCOL_CONNECTION_LOST。给查询加一层小重试很实用:

async function queryWithRetry(sql, params, retries = 2) { for (let i = 0; i <= retries; i++) { try { return await query(sql, params); } catch (err) { const retryable = ['ECONNRESET', 'PROTOCOL_CONNECTION_LOST', 'ETIMEDOUT'].includes(err.code); if (!retryable || i === retries) throw err; await new Promise(resolve => setTimeout(resolve, 200 * (i + 1))); } } }

读查询重试很安全,但写操作要小心。INSERT在连接断开后可能已经被服务端执行,客户端却因为断线没有收到结果,这时重试会造成重复写入。我的习惯是只给读和幂等更新做自动重试,写操作则把幂等键落到数据库里,靠唯一索引兜底。这也是我用事务时要特别确认的一点:不要在事务内部盲加重试,否则rollback和commit的状态会被重试逻辑打乱。

最后说我自己的经验:一开始图省事,所有getConnection都不写finally,觉得脚本跑完进程退出自然会释放。后来线上接口一压测就超时,半夜翻SHOW PROCESSLIST,全是被占住的连接。从那天起,我把每个getConnection都配一个try...finally作为默认写法,成本只是两行代码,收益是少熬好几个通宵。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:11:56

SDH备考精讲:STM-1帧结构、复用路线与指针定位

简介&#xff1a;HCIA-Transmission V2.0培训课程PDF文档是华为官方面向传输领域工程师与认证考生的系统学习材料&#xff0c;适合初学者入门和从业者查漏补缺&#xff0c;重点解决从光传送网演进到主流传输体制的技术认知问题。资源为单个PDF文件&#xff0c;压缩包约40.99MB&…

作者头像 李华
网站建设 2026/9/30 3:11:36

RediShell攻击揭秘:Redis Lua脚本如何沦为RCE突破口

最近在看一次内网安全评估报告时又碰到那串熟悉的关键字&#xff1a;6379 端口暴露、CONFIG SET dir、EVAL脚本&#xff0c;最后落盘写了一个 crontab。群里管这套打法叫 RediShell&#xff0c;听起来挺酷&#xff0c;本质上是借着 Redis Lua 脚本引擎的能力做了一次 RCE&#…

作者头像 李华
网站建设 2026/9/30 3:11:34

WiFi大师专业版4.0.5独立部署与流量主广告接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 3:10:21

HTTPS下GET与POST的实战区别:从协议语义到工具验证

先抛个引子&#xff1a;上个月有个做前端的同事跑来问我&#xff0c;为什么他用fetch发的请求&#xff0c;明明写了method: POST&#xff0c;后端收到的却是GET。我让他把代码里的大小写拍给我看&#xff0c;果然是写成了Method: get。这还不是最离谱的&#xff0c;另一个搞测试…

作者头像 李华
网站建设 2026/9/30 3:09:56

系统对接接口方案全解析:从设计原则到API落地的避坑指南

简介&#xff1a;《软件系统平台对接接口方案文档》面向系统集成、软件开发及平台对接人员&#xff0c;系统阐述了不同软件系统间高效、稳定、安全对接的技术路径&#xff0c;覆盖接口设计原则、接口分类、设计模式与API实现方式等核心内容。文档强调高内聚、低耦合与SOA组件化…

作者头像 李华
网站建设 2026/9/30 3:09:38

SpringBoot+Vue精准扶贫管理系统设计与实现全流程解析

这几年找我聊计算机毕业设计的人&#xff0c;问得最多的一种题目就是“SpringBootVue精准扶贫管理系统”。说实话&#xff0c;这类题看起来不复杂&#xff0c;但真正能跑起来、能写进论文、能顺利通过答辩的版本并不太多。很多人卡在前后端联调、权限控制、报表统计这些真实工程…

作者头像 李华