学习编程之初就常被告诫:“永远不要相信用户的输入”,但实际编码中,可能因为各种原因而忽略这点,本文尝试以 SQL 注入的角度探寻校验输入的重要性 以下实验均以 SQLI labs 靶场为例 1. 联合注入(Union-Based) 来自:Less-1 这是一个常见的查询页面。http://127.0.0.1/Less-1/?id=1 ,通过 id=1 传递参数。后端常见的 SQL 写法:SELECT * FROM users WHERE id='$id' LIMIT 0,1; 攻击者可以通过构造 id 的参数值,执行任意的 SQL 语句: 其中关键步骤是构造 1' --+: 通过某个具体参数 1 和 单引号 ' 来结束前面的语句:SELECT * FROM users WHERE id=',使其成为合法的 SQL 语句: SELECT * FROM users WHERE id='1' 通过 --+ 来注释后面的 ' LIMIT 0,1"; 基于上面的原理,我们就可以在 1' 和 --+ 之间插入语句了,进行联合注入,具体步骤如下: 通过 order by 测列宽:?id=-1' order by 4 --+,通过不断尝试和错误提示可以得知列宽为 3 判断回显值对应的位置,?id=-1' union select 1,2,3 --+,2 和 3 这两个位置都可供使用 ...
重拾 SSH:从基础到安全加固
安全外壳协议(Secure Shell Protocol,简称SSH)是一种加密的网络传输协议,属于应用层协议。OpenSSH 是最流行的 SSH 实现,它是大量操作系统的默认组件 OpenSSH 套件由以下工具组成: 远程操作使用:ssh, scp 和 sftp 密钥管理:ssh-add, ssh-keysign, ssh-keyscan 和 ssh-keygen 服务端: sshd, sftp-server 和 ssh-agent 使用 SSH 连接服务器 1. 客户端创建公私钥对 密钥类型选择 ed25519 椭圆曲线,它生成的公私钥都要比 RSA 更短,具有较高的安全性和性能 # - a KDF (Key Derivation Function) 的迭代次数 默认:16 ,防止暴力破解 # - t 类型 # Ubuntu 22.04 默认:RSA 3072;Mac OS 默认:ED25519 256 # - C 备注,可以备注上创建年月,定期更换私钥 ssh-keygen -a 256 -t ed25519 -C "Brandon+2025-01@MacBook" # 可以手动指定路径和密码,也可以一路回车 在 ~/.ssh 下会生成公私钥对 . ├── [ 411] id_ed25519 ├── [ 98] id_ed25519.pub # 私钥需要妥善保管,避免暴露 cat id_ed25519 -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACCRtC9cJJBFwvVsp4vV058ci8lSHNrf2qcx8W+umtK7OwAAAKArJx9PKycf... -----END OPENSSH PRIVATE KEY----- # .pub 结尾为公钥 cat id_ed25519.pub ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJG0L1wkkEXC9Wyni9XTnxyLIt/zHxb66a0rs7 Brandon+2025-01@MacBook 2. 在服务器上添加公钥 将上面客户端生成的公钥 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJG0L1wkkEXC9Wyni9XTnxyLIt/zHxb66a0rs7 Brandon+2025-01@MacBook 加入到服务端 ~/.ssh/authorized_keys,每个私钥占据一行 ...
重拾 iptables
iptables 是一个常看常忘的命令,本文试图从应用的角度理解它 iptables 是运行在用户空间的应用软件,通过控制 Linux 内核 netfilter 模块,来管理网络数据包的处理和转发 一些常用的场景 1. 禁止 ip 访问后端 IP 在 192.168.64.6 上增加规则: # -A INPUT: 将规则添加到 INPUT 链,表示处理进入的流量 # -s 192.168.64.7: 指定源 IP 地址,即要阻止的 IP # -d 192.168.64.6: 指定目标 IP 地址,即后端 IP # -j DROP: 表示丢弃匹配的流量 iptables -A INPUT -s 192.168.64.7 -d 192.168.64.6 -j DROP # -j REJECT: 丢弃流量的同时向源 IP 返回一个拒绝消息。请求方直接提示:Connection refused iptables -A INPUT -s 192.168.64.7 -d 192.168.64.6 -j REJECT # -p 指定协议类型为 TCP # --dport 指定目标端口 iptables -A INPUT -s 192.168.64.7 -d 192.168.64.6 -p tcp --dport 80 -j REJECT # 看当前的 iptables 规则 # -L "list",列出当前的规则 # -n "numeric",即使用数字 IP 地址和端口号而不是主机名和服务名 # -v "verbose",显示详细信息 iptables -L -n -v # 列出带编号的规则 iptables -L --line-numbers # 删除 INPUT 链中的第 1 条规则 # 注意!删除成功后序号会改变,需要重新查询序号 iptables -D INPUT 1 # 清除 INPUT 链所有规则 iptables -F INPUT # 清除当前活跃的表(未指定默认是 filter 表)的所有 iptables 规则 # 等同于 iptables -F -t filter iptables -F 2. 端口转发 默认情况下,Linux 系统不会转发目的 IP 地址不是本地网络的 IPv4 数据包。这是出于安全考虑,防止系统意外成为恶意流量的转发表。要启用 IPv4 数据包转发功能,需要修改内核参数 net.ipv4.ip_forward ...
MySQL 时区与 serverTimezone
TL;DR 手动为 MySQL 指定非偏移量的时区,以避免 TIMESTAMP 类型夏令时问题和时区转化性能瓶颈 TIMESTAMP 范围:‘1970-01-01 00:00:01’ UTC to ‘2038-01-19 03:14:07’ 连接 MySQL 数据库时,serverTimezone 参数用于指定数据库服务器的时区,需要设置为与 MySQL 服务端相同的时区 MySQL 时区设置影响 TIMESTAMP 类型数据和部分时间函数 MySQL 会话时区设置会影响 TIMESTAMP 和 时间函数(NOW()、CURDATE()、CURTIME()、CURRENT_TIMESTAMP()) 存储 TIMESTAMP 类型数据时,MySQL 会根据当前会话的时区将时间转换为 UTC 时间,MySQL 实际存储的是 UTC 时间。检索时 MySQL 根据会话的时区将存储的 UTC 时间转换为会话对应时区的时间。而 DATETIME 类型的字段存储的时间值是原始值,不受时区影响 MySQL 默认使用 SYSTEM 时区(即操作系统的时区),每个需要时区计算的 MySQL 函数调用都会调用系统库来确定当前系统时区。此调用可能受到全局互斥体的保护,从而导致争用,建议显式设置时区 查询当前时区 # time_zone:MySQL 使用 SYSTEM 的时区 # system_time_zone:SYSTEM 为 CST 时区 show variables like "%time_zone%"; +------------------+--------+ | Variable_name | Value | +------------------+--------+ | system_time_zone | CST | | time_zone | SYSTEM | +------------------+--------+ 不同会话时区对 时间函数 的影响 # 当前时区 # 查看当前的全球和会话时区值 SELECT @@GLOBAL.time_zone, @@SESSION.time_zone; SELECT NOW(), CURDATE(), CURTIME(), CURRENT_TIMESTAMP(); set time_zone = 'America/New_York'; SELECT NOW(), CURDATE(), CURTIME(), CURRENT_TIMESTAMP(); 不同会话时区对 TIMESTAMP 类型的影响 # UTC +8 set time_zone = 'Asia/Shanghai'; CREATE TABLE events ( id INT AUTO_INCREMENT PRIMARY KEY, event_name VARCHAR(255) NOT NULL, event_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, event_datetime DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO events (event_name, event_timestamp, event_datetime) VALUES ('10.24 15:45:00', '2022-10-24 15:45:00', '2022-10-24 15:45:00'); INSERT INTO events (event_name, event_timestamp, event_datetime) VALUES ('12.24 15:45:00', '2022-12-24 15:45:00', '2022-12-24 15:45:00'); SELECT * FROM events; +----+----------------+---------------------+---------------------+ | id | event_name | event_timestamp | event_datetime | +----+----------------+---------------------+---------------------+ | 1 | 10.24 15:45:00 | 2022-10-24 15:45:00 | 2022-10-24 15:45:00 | | 2 | 12.24 15:45:00 | 2022-12-24 15:45:00 | 2022-12-24 15:45:00 | +----+----------------+---------------------+---------------------+ 2 rows in set (0.00 sec) # 仅修改当前会话的时区 set time_zone = 'America/New_York'; SELECT * FROM events; +----+----------------+---------------------+---------------------+ | id | event_name | event_timestamp | event_datetime | +----+----------------+---------------------+---------------------+ | 1 | 10.24 15:45:00 | 2022-10-24 03:45:00 | 2022-10-24 15:45:00 | <- 夏令时,相差 12 小时 | 2 | 12.24 15:45:00 | 2022-12-24 02:45:00 | 2022-12-24 15:45:00 | <- 平时相差 13 小时 +----+----------------+---------------------+---------------------+ 2 rows in set (0.00 sec) 纽约 UTC 时差通常为 UTC-5(EST),夏令时为 UTC-4(EDT),所以将原本的会话从上海(UTC+8) 转到纽约时,TIMESTAMP 相差了 13 或 12(夏令时) 小时,所以为了自动转换夏令时,指定时区最好使用时区名词 Asia/Shanghai,避免使用偏移量:'+08:00' ...
为什么 AI 时代更应该 Learn in Public
TL;DR Learn in Public 强调将学习到的知识 分享到公共空间,相较于纯输入式的学习有诸多好处。AI 工具极大降低了信息检索、整理、概括的门槛,使得输入信息更容易,但对我们真正掌握知识的帮助仍然有限,所以我们更需践行要像 Learn in Public 这样能提供 有效输出 的学习方式 AI 工具大大降低了信息检索、整理和概括的门槛,使得获取信息更加便捷,但对我们真正掌握知识的帮助仍然有限。因此我们更需要践行像 Learn in Public 这样重视 有效输出 的学习方式 为什么会想到这个话题 偶然间看到 Owen 发的贴子: 看到一个说法:笔记是一种无限游戏,没有结果,只有过程;而博客是一种有限游戏,因为它产出了完成的作品:博文。这说明我们不能当一个完美主义者,只在脑海或草稿箱中保留想法,我们应该尽可能完成作品,公开它,然后不断的练习这个过程。 我更坚定那个想法了:做一个默认设计为公开的笔记软件 看到这个贴子,马上联想到了 Learn in Public 这个概念,开始思考自己的这些年记笔记的方式,意识到了原来的方式可能存在一些问题,于是开始尝试践行 Learn in Public,将自己学习的一些东西写成博客、用学到的知识做一个有意义的产品等等。经过了这段时间的实践,再结合上自己日常使用 AI 的一些感受和想法,很自然地就想到了这个话题 什么是 Learn In Public 在 swyx 发布 Learn In Public 后,这个概念变得更加流行。Learn in Public 强调的是 将学到的东西分享到公共空间 常见的 Learn In Public 的方式,例如: 撰写博客、教程 在会议上发言 在问答社区提问或者回答 制作并发布视频 与之相对的 Learn in Private 侧重的是 消费内容,例如: 个人笔记 阅读书籍 阅读源码 订阅 GitHub 的 Repos 和 Issues,观察其他的人实践 为什么需要 Learn in Public Learn in Public 是一个输出的过程,促进知识的整理、理解、求证,帮助我们拓宽对某些 知识理解的边界 公共空间能提供 反馈,反馈可以产生激励、也可以修正我们努力的方向 有助于 筛选 所要学习的东西,Learn In Public 会花费大量精力,它能促使我们评估即将学习的内容是否值得 对抗完美主义,先有产出,走出第一步再根据反馈 持续迭代,而不是止步不前 很多的知识都来自 Public ,所以没有什么好藏着掖着,大部分的成果可能都是站在巨人的肩膀上 为什么 AI 时代更应该 Learn in Public 在 AI 的推动下,知识获取已经实现了质的飞跃,但对我们真正掌握知识的帮助仍然有限,所以我们更应该 Learn in Public,做更多的 有效输出 ...
理解前端工程化
最初对前端的观感:眼花缭乱,各种各样的工具链以及其对应的配置文件、VS Code 插件,各种技术百家争鸣,选择众多。后来才理解前端不同于后端,后端代码的运行环境相对可控,而前端代码运行在用户设备上,所以需要兼容不同的环境,而很大一部分的工具、配置都是解决兼容性的问题 TL;DR 工程化的目的:降低开发成本提高开发效率 方式:解决前端三大件(HTML, CSS, JS)存在的问题并对其进行增强,JS 通过 Babel 而 CSS 可以通过 Sass, Less, PostCSS 等工具实现进行增强,并且保证输出向后兼容的的 JS 或 CSS 代码 模块化、包管理 分解聚合:拆分复杂任务,降低复杂度(分而治之) 模块化解决 问题:1. 全局污染 2. 依赖管理 等等 JS 模块化标准(常用):1. CommonJS (Node 标准)2. ES modules(JavaScript 官方标准模块化方案) 实现:1. 浏览器只支持 ESM 2. Node 和 构建工具 上面两种标准都支持 包 (package) 管理:npm(Node.js的标准包管理器),还有其他的 pnpm 和 yarn 等 JS 工具链 JS 语言本身一直都在繁荣地发展,经常出现新的 API 和 语言特性,但是用户的运行环境(浏览器、Node等)的版本可能是五花八门的,可能会导致报错、兼容性等的问题,所以最朴素的解决方式就是把 JS 都转换为向后兼容的老版本 JS 代码。前端的运行环境更多的是在用户这边,这点不同于后端能比较自由地调整服务器运行环境,这可能就是前端缝缝补补的原因吧,修补主要有的方式: 新 API:使用 Polyfill(填充物),为其实现缺少的 API,例如 core-js 库,就实现了 Array 的 flatMap 方法,这样在旧版本的 Node 环境,也可使用该方法 新语法:例如 Promise,对于这样的语法糖,就无法直接为其编写方法,需要转换代码,有点类似翻译,通过 regenerator 库可以将含有 Promise 的代码转换为向后兼容的代码 有很多这样的库用于解决某个特定的兼容性问题,一个个导入很麻烦,所以就有了 Babel,可以通过 Babel 的插件整合这些转换代码的库,需要安装对应的 Babel 插件依赖 并在 babel.config.js 中配置 plugins。这样还是很麻烦,所以我们可以直接使用 Babel 的预设,其中最常用的就是:@babel/preset-env,安装依赖以后完成以下的 babel.config.js 配置,即可开箱即用: ...
JavaScript 事件循环 动画演示
在前端代码中很经常看到使用 setTimeout(fn, 0),如下面代码所示,乍一看很多余,但是移除了可能会出现一些奇奇怪怪的问题。要解释这个就需要理解 事件循环(Event Loop),下面会通过一些例子和动画来辅助理解事件循环 setTimeout(() => { // 调用一些方法 }, 0) 为什么使用事件循环 JS 是单线程的(浏览器和 Node则是多线程的),为了避免 渲染主线程 阻塞,需要异步,事件循环 是异步的实现方式 浏览器在一个渲染主线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。为了避免 同步 的执行方式导致渲染主线程阻塞,使得页面卡死,所以浏览器采用异步的方式:渲染主线程将任务交给其他线程去处理,自身 立即结束 任务的执行,转而执行后续代码,当其他线程完成时,将事先传递的回调函数包装成任务,加入到对应的消息队列的末尾排队,等待渲染主线程调度执行 流程: 渲染主线程执行全局 JS,需要异步的任务放到对应的队列,如果是 setTimeout 则会有线程计时,到了指定时间会将任务放入 延时队列(并非立即执行) 渲染主线程为空时,按队列的优先级依次选择队列(最先执行微队列的任务),依次按顺序执行各个队列的任务 任务没有优先级,而消息队列有优先级,不同任务分属于不同队列:参考 W3C 规范。微队列优先级最高,接着是交互队列然后才是延时队列 常见队列: 微队列(microtask):⽤户存放需要最快执⾏的任务,优先级「最⾼」,通过 Promise.resolve().then() ⽴即把⼀个函数添加到微队列 交互队列:⽤于存放⽤户操作后产⽣的事件处理任务,优先级「⾼」 延时队列:⽤于存放计时器到达后的回调任务,优先级「中」 事件循环 下面例子来自于:《WEB前端大师课》,大块的文字描述相对没那么直观,所以用 Keynote 做了 gif 方便理解(如果有更好的做 gif 的方式可以留言告诉我) 1. JS阻碍页面渲染 JS 修改了 DOM 后,并不会马上显示在页面上,需要进行 绘制 后才会显示页面变更 <!DOCTYPE html> <html lang="en"> <head></head> <body> <h1>初始h1</h1> <button>change</button> <script> var h1 = document.querySelector('h1'); var btn = document.querySelector('button'); function delay(duration) { var start = Date.now(); while (Date.now() - start < duration) {} } btn.onclick = function () { h1.textContent = '修改h1 textContent'; delay(3000); }; </script> </body> </html> ...
如何弄懂复杂项目
先跑起来,通过文档和实践熟悉业务流程 这一步可以通过看官方文档开始,要注意的是一些项目是 更新先于文档 的,比如新版本启动方式有变更,但是文档还没更新。跟着文档不一定能把项目跑起来,需要借助 GitHub Issue 或者是 Slack 这样的工具以获取即时的帮助 看测试,通过测试了解流程 如果是开源项目,可以通过 GitHub Action 快速了解需要哪些依赖、如何快速运行测试,便于在本地运行测试,通过这些集成测试可以快速弄懂业务主线 通过 debug 高效快速地梳理流程 通过断点可以一步一步跟踪程序的运行,可以比较直观地看调用栈、变量等等的 对于一些无法本地调试的项目来说,我们可以退而求其次,断点它的测试,这也是一个很有效的方法 画图:降低复杂度 很多项目会使用一些比较优雅的设计或是引入一些抽象层,这样代码读起来就会跳来跳去,层级深的话就很容把人给绕晕了 可以用 draw.io 或者 excalidraw 等工具,根据实际情况画一画 活动图、时序图等 提出具体的问题,带着问题看项目 如果只是盲目地看项目代码,可能看完还是一头雾水,但是如果能提出一个具体问题,或是带着一个需求去看,效果就会好得多 比如我提出问题:“某个任务在集群内是如何完成的?”,我可能会先去找到该任务的创建入口,然后顺藤摸瓜,找到任务的调度逻辑,顺着 happy path 找到下发任务的逻辑,再找到 Woker 的处理逻辑,这样就能弄懂整个调度流程 最后如果能用 一句话 回答提出的问题,那可能能说明你对这个问题涉及的知识已经有了一个比较好的理解 英语很重要 大多数项目的注释、日志等的都是英文,看懂这些能极大提高效率
IDEA (任意 JetBrains IDE)拆分先前 commit
最近在合并上游代码,遇到了一个问题:某个 commit 杂糅了几个不同的特性修改,这可能会导致 rebase 上游代码时需要再对该 commit 进行额外的代码冲突处理 解决方法:合并上游分支前,拆分杂糅的 commit,并将其中不同的特性修改合并(Squash)回相关的 commit。可以直接通过命令行进行操作,可以参考:Break a previous commit into multiple commits。也可以通过 JetBrains 家内置的 Git 进行操作,下面会介绍 IDEA 图形化操作的方法 非先前 commit 的拆分 对于刚提交的 commit,要拆分多个 commit 是非常容易的,因为我们只要 soft reset commit,将 commit 内容撤销回至 暂存区,就可以随意提交 commit 如果对于 soft reset 不太了解,可以参考我之前的博客:Git 中的回退操作:reset 和 revert 先前 commit 的拆分 先前 commit 指的是:在目标 commit 后已经有了若干个 commit。它无法直接通过 soft reset 进行拆分,因为这样会丢失后续的 commit,如下图,我们需要拆分 B commit,我们就无法直接使用 soft reset ,因为这样会丢失 C 和 D commit 的修改 所以我们需要使用 rebase,具体步骤: 在 交互式 (interactive) rebase 中将 B 标记为 edit,这时 B 后面的 commit 会被暂时隐藏起来 使用 soft reset 将 B 撤销回 暂存区 将 B 的修改内容分多个 commit 提交 B1 和 B2 使用 rebase 的 continue 将刚才隐藏的 C 和 D 恢复回来,需要注意的是:因为之前的 commit 记录已经改变了,所以这时的 C 和 D 已经与原来的 commit 记录不相同,故标记为 C' 和 D' ...
SpringBoot 测试实践 - 3:@MockBean、@SpyBean 、提升测试运行速度、Testcontainer
上一节:SpringBoot 测试实践 - 2:单元测试与集成测试 编写测试的时候,我们必须保证外部依赖行为一致,也需要模拟一些边界条件,所以我们需要使用 Mock 来模拟对象的行为。SpringBoot 提供了 @MockBean 和 @SpyBean 注解,可以方便地将模拟对象与 Spring 测试相结合,简化测试代码的编写 @MockBean @MockBean 是 Spring Boot Test提供的注解,用于在 Spring Boot 测试中创建一个模拟的 Bean 实例,并注入到测试类中的依赖项中。使用 Mock 可以控制被 Mock 对象的行为:自定义返回值、抛出指定异常等,模拟各种可能的情况,提高测试的覆盖率 @SpringBootTest @RunWith(SpringRunner.class) public class MyServiceTest { @MockBean private ExternalDependency externalDependency; @Autowired private MyService myService; @Test public void testSomeMethod() { // 定义外部依赖的行为 Mockito.when(externalDependency.someMethod()).thenReturn("Mocked Result"); // 调用被测试类的方法 // 被测方法内部调用了 ExternalDependency 的 someMethod 方法 String result = myService.someMethod(); // 验证外部依赖的方法是否被调用 Mockito.verify(externalDependency).someMethod(); // 断言结果 assertEquals("Mocked Result", result); } } 需要注意的是:使用了 @MockBean,会创建完全模拟的对象,它完全替代了被模拟的 Bean,并且所有方法的调用都被模拟。对于未指定行为的方法,返回值如果是基本类型则返回对应基本类型的默认值,如果是引用类型则返回 null ...