Tech-Paul

work hard, play hard

  • 不合理的验证码业务逻辑设计,导致验证码存在安全风险,用户密码存在可爆破风险 *

现有业务设计:

  1. nodejs 环境,引入 svg-captcha, 后端生成验证码
  2. 验证码图片通过 api 返回,同时验证码文本通过 set-cookie 设置到浏览器,验证码有效期 30s
  3. 前端在登录时,需要在参数中用户输入验证码内容,以及请求头中携带包含验证码文本的 cookie
  4. 后端在登录时,需要校验验证码文本是否与 cookie 中的文本一致

问题

验证码复用,导致用户名密码可爆破

  • 调用登录接口时,需要在参数中携带用户名、密码、验证码
  • 使用相同验证码遍历用户名,返回包均相同,证明验证码存在复用
  • 验证码存在复用风险,攻击者可以通过遍历用户名,使用相同验证码登录,获取用户密码

解决方法

  • 规则:每个验证码只允许使用一次
  • 验证码生成后,同时生成一个 captchaId, 作为验证码的唯一标识,唯一标识作为 key、验证码文本作为 value 存储在 redis 中,有效期与验证码有效期一致
  • captchaId 过期后 redis 回自动删除,不会占用内容
  • captchaId 通过 set-cookie 设置到浏览器
  • 登录时,校验验证码文本是否与 redis 中的 captchaId 一致,如果不一致,则返回验证码错误,一致和不一致的情况都删除 redis 中 captchaId

时间格式、最佳实践以及转换方法。

一、核心原则:两条黄金法则

  1. 后端和数据库(权威来源):永远使用 ISO 8601 格式和 UTC 时间。
    ◦ 为什么? UTC 是零时区,没有夏令时等问题,是全球统一的标准时间。用它来存储和计算可以避免因地时区不同而产生的混乱。

  2. 前端(展示层):在最后一刻(UI 层)才转换为用户的本地时间。
    ◦ 为什么? 后端传递 UTC 时间,前端根据用户的浏览器或设备设置的时区,将 UTC 时间转换为本地时间进行显示。提交数据时,再转换回 UTC 格式传给后端。

二、常用的时间格式

  1. ISO 8601 格式 (国际标准)

这是最重要的格式,是前后端交互和数据库存储的首选。
• 格式: YYYY-MM-DDTHH:mm:ss.sssZ

◦   T: 日期和时间的分隔符。

◦   Z: 表示协调世界时(UTC)。如果显示的是本地时间,这里可能是 +08:00(东八区)这样的时区偏移量。

• 示例:

◦   2023-10-27T07:30:25.123Z (UTC时间)

◦   2023-10-27T15:30:25.123+08:00 (北京时间,东八区)
  1. 时间戳 (Timestamp)

表示自 1970 年 1 月 1 日 00:00:00 UTC (Unix 纪元) 以来经过的秒数或毫秒数。
• 优点: 绝对值,没有时区概念,计算方便(比较、加减)。

• 缺点: 人类不可读。

• 示例:

◦   秒级: 1698391825

◦   毫秒级: 1698391825123 (更常见于JavaScript等领域)
  1. 数据库中的日期时间类型

• MySQL:

◦   DATETIME: 不包含时区信息。它就是你存入的那个日期和时间。

◦   TIMESTAMP: 包含时区信息,存入时会根据数据库时区转换为UTC存储,取出时再转换回数据库时区。其实际存储的是时间戳(秒级)。

• PostgreSQL:

◦   TIMESTAMP / TIMESTAMPTZ (Timestamp with time zone): 推荐使用 TIMESTAMPTZ。它会存储UTC时间,并在查询时根据数据库的时区设置或会话时区显示相应时间。

• 建议: 在数据库中,也优先使用能明确时区的类型(如 TIMESTAMPTZ),或者统一用 DATETIME 存储 UTC 时间。

  1. 前端/人类可读格式 (用于显示)

• 格式: 各种各样,取决于地区和语言环境。

• 示例:

◦   2023/10/27 15:30:25

◦   27/10/2023 15:30:25 (欧洲常见)

◦   10/27/2023, 3:30:25 PM (美国常见)

三、相互转换方法(核心实践)

下图清晰地展示了在不同层级之间处理和时间转换的“黄金流程”:
flowchart TD
A[用户输入
本地时间] –>|前端处理| B{转换节点};
B – 最佳路径
JS: toISOString –> C[“UTC 字符串
(ISO 8601 格式)”];
C –>|HTTP 传输| D[后端接收];
D –>|解析并存储| E[“数据库
(UTC 时间)”];

E -->|查询| F["UTC 字符串<br>(ISO 8601格式)"];
F -->|HTTP传输| G[前端接收];
G -->|JS: new Date()<br>+ toLocaleString()| H[UI 显示<br>(本地时间)];

B -- 备用路径<br>拼接字符串 --> I["非标准格式<br>(如: YYYY-MM-DD HH:MM:SS)"];
I --> J[后端接收];
J -->|需指定时区解析| K["潜在问题<br>时区歧义"];

subgraph Legend
    L[最佳实践路径]
    M[不推荐路径]
end

以下是各环节的关键代码实现:

  1. 前端 JavaScript 中的转换

JavaScript 的 Date 对象是处理时间的核心。

• 获取当前时间的 UTC ISO 字符串和时间戳:
const now = new Date(); // 创建 Date 对象,表示当前时刻

// 转换为UTC的ISO格式 (后端交互首选)
const isoString = now.toISOString(); // "2023-10-27T07:30:25.123Z"

// 获取时间戳 (毫秒级)
const timestamp = now.getTime(); // 1698391825123

• 将 UTC ISO 字符串转换为用户的本地时间进行显示:
// 从后端接收到的 UTC 时间
const utcIsoStringFromBackend = ‘2023-10-27T07:30:25.123Z’;

// 1. 创建Date对象(JS会自动将其解析为本地时间)
const dateObj = new Date(utcIsoStringFromBackend);

// 2. 转换为本地时间的可读字符串
const localDateTimeString = dateObj.toLocaleString(); // "2023/10/27 15:30:25" (取决于用户系统设置)
// 或者使用更灵活的Intl.DateTimeFormat
const formatter = new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false // 24小时制
});
const localTime = formatter.format(dateObj); // "2023/10/27 15:30:25"

• 将用户输入的本地时间转换为 UTC ISO 字符串:
// 假设用户输入的是本地时间的字符串(这是一个复杂且容易出错的操作,推荐使用日期选择器组件)
const userInput = “2023-10-27 15:30:25”; // 注意:这个字符串没有时区信息!

// 1. 解析为Date对象(注意:不同浏览器对字符串解析的实现可能不一致!)
// 更安全的方式是手动拆分字符串
const [year, month, day, hour, minute, second] = userInput.split(/[- :]/);
// 创建一个表示“用户这个时间点”的Date对象(JS会使用本地时区)
const userDate = new Date(year, month - 1, day, hour, minute, second);

// 2. 获取这个时间点对应的UTC时间(ISO格式)
const utcIsoForBackend = userDate.toISOString(); // "2023-10-27T07:30:25.000Z"
// 注意:如果用户在北京(东八区),输入15:30,对应的UTC时间就是 15:30 - 8小时 = 07:30
  1. 后端(以 Java Spring Boot 为例)

• 定义字段:在接收和返回对象的实体类中,使用 Instant、LocalDateTime 或 ZonedDateTime 等现代日期时间 API。
// 推荐使用 Instant (代表时间戳)
private Instant createdAt;

// 或者使用 LocalDateTime (需要明确知道它不包含时区信息)
// private LocalDateTime createdAt;

• 接收前端请求(JSON 反序列化):

Spring Boot 默认能很好地处理 ISO 8601 格式。
// @RequestBody 中的JSON字段如 "2023-10-27T07:30:25.123Z" 会自动被反序列化为 Instant 或 LocalDateTime

• 返回给前端(JSON 序列化):

配置Jackson将时间格式序列化为ISO字符串。
// 在application.properties中
spring.jackson.serialization.write-dates-as-timestamps=false // 不使用时间戳,使用ISO格式

这样,返回的JSON中日期字段就会自动变成 "2023-10-27T07:30:25.123Z"。
  1. 数据库(以 MySQL 为例)

• 写入数据库: 你的后端代码应该将 UTC 时间(如 Java 的 Instant)写入数据库的 DATETIME 或 TIMESTAMP 字段。

• 从数据库读取: 从数据库读出的时间,应被映射为后端语言对应的 UTC 时间对象(如 Java 的 Instant)。

示例(MyBatis):

// Java Entity
public class Order {
private Instant createdAt; // 使用 Instant 类型
// getters and setters
}

总结与最佳实践

  1. 统一使用 UTC: 后端、数据库、API 交互全部基于 UTC 时间和 ISO 8601 格式。

  2. 前端负责展示转换: 前端在拿到 UTC 时间后,利用 Date API 或库(如 date-fns, day.js)转换为本地时间显示。

  3. 避免使用纯字符串: 尽量不要手动拼接 “YYYY-MM-DD HH:MM:SS” 这样的字符串进行传递和解析,极易出错。

  4. 利用现代库:
    ◦ 前端: 原生 Date API 有时难用,推荐使用 day.js 或 date-fns,它们更小、更模块化、更安全。

    ◦ 后端 Java: 坚持使用 java.time 包(Java 8+)下的类,如 Instant, LocalDateTime, ZonedDateTime。

    ◦ 后端 Python: 使用 datetime 模块,注意 naive(无时区)和 aware(有时区)的 datetime 对象的区别。

  5. 文档: 在 API 文档中明确注明所有时间字段均为 ISO 8601 格式的 UTC 时间。

问题描述
fastgpt V4.9.3 运行过程中发现每隔一段时间容器发生重启
进一步查看返现内容持续上升,达到设置阀值,触发重启

排查方案:

服务端增加监控,获取 profile 文件,导入插件,分析内存泄漏原因。

配置 docker 开启 debug 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
获取dump文件
#1.复制工程代码
cd ~/snapshots/output
docker cp node_modules fastgpt-2:/tmp/
docker cp take-memory-snapshot.js fastgpt-2:/tmp/
#2.进入容器,开启DEBUG
docker exec -it fastgpt-2 sh
kill -USR1 1
#输出显示:
Debugger listening on ws://127.0.0.1:9229/a1111a16-b8fd-4348-9e7d-01aae1a70fd5
For help, see: https://nodejs.org/en/docs/inspector
#3.进入容器,执行dump脚本
cd /tmp
node take-memory-snapshot.js > heap-$(date +%s).heapsnapshot
#注意:dump脚本执行完后,容器会自动重启,请选择合适时间进行dump!
#4.下载dump文件,通过chrome://inspect 插件分析dump文件
Go to chrome://inspect
Click on “Open dedicated DevTools for Node”
Go to Memory tab
Press load profile button
Load all the heap snapshots you want to compare
工程代码路径:20上/home/albert/snapshots/output

分析内存泄漏原因

下载后会得到 profile-xxxxxxxxxxxxx.heapsnapshot 文件
导入到 chrome://inspect 插件中进行分析
找到内存泄漏的对象,分析泄漏路径,发现是因为工作流中的节点缓存没有清理,导致内存泄漏。

解决办法

对 settimeout 做了清理,继续做测试,内存稳定在 1 个多 G

0%