阅读视图

发现新文章,点击刷新页面。

服务端响应 RST, ACK ,可能是中间安全设备导致的

最近在一个项目中遇到了一个奇怪的问题,项目部署到客户提供的服务器环境中后,从互联网侧访问时好时坏,但是在服务器上访问是正常的,按照常理判断是防火墙的问题,但是客户联系的网络工程师一直没有解决该问题,其华为原厂工程师反馈说是“外网客户端访问时与服务器协商加密协议,服务器没有协商过程直接拒绝了,请再检查一下服务器的配置”,而又把问题抛了回来。

我虽笃定是是客户服务器环境的网络设备导致的,但没有证据也不能服人,所以用 Wireshark 抓包实际测试,这里记录排查过程。

这段抓包记录是一个典型的TLS握手失败的过程,在尝试建立安全连接时出现了问题。在 4835 & 4836 客户端发送了一个 Client Hello 包,这是 TLS 握手的第一步,结果在 4840 服务器突然发送了一个 RST, ACK 包,重置了连接。

由于之前测试过在服务器上是可以正常访问的,所以这这个时候我们需要考虑一个问题:这个 RST, ACK 包真的是服务器回复的吗?

从 RST 报文的 seq 来看确实可以和前序报文对应得上(由于SYN标志位在逻辑上占用1字节序号,所以 RST 报文的序号是第二个报文的序号加1)。

一个很好的判断一条流是否是同一个服务器发送的方法是对比同一个方向的报文的IP头中的 TTL(Time to Live) 值。由于 TCP 对乱序非常敏感,而网络设备逐包转发数据会引入更严重的乱序,因此网络中的设备一般都是逐流转发(按五元组,源目IP、源目端口、协议),所以,大部分情况下,在捕获的数据流中,同一条流的同一个方向的报文总是有相同的 TTL 值。

这里排查上面的抓包记录来看,从客户端到远程服务器的 TTL 值都是 128,服务端返回到客户端的包 TTL 值都为 119。而该 RST, ACK 包的 TTL 为 56,因此断定该包并不是真实服务器返回的,而是中间的网络设备伪造的。

将该问题返回给客户后,客户排查网络链路上的安全设备,在 IPS 中找到对应的日志,IPS 冒充服务器向终端发送 RST 报文拆链:

Oracle Linux 配置 CentOS 软件仓库镜像源

Oracle Linux 是基于 RHEL 的另一个“重构发行版”,和 CentOS 同属一类系统。国内使用该系统最大的一个问题就是没有对应的 Oracle Linux 软件仓库镜像源,并且网上也没有相关的资料,但考虑到是同属 RHEL 系统,并且官方如此的强调和 CentOS 兼容性,就想到尝试使用国内的 CentOS 镜像源仓库替代。

我这里使用 Oracle Linux 7 版本,对应 CentOS 7 版本,其他的大家各自对照略有出入。

# 1. 下载并导入 GPG 公钥
wget -O /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 https://mirrors.aliyun.com/centos/7/os/x86_64/RPM-GPG-KEY-CentOS-7
rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

# 2. 配置仓库镜像源
wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo

# 3. 替换仓库源文件中的环境变量为具体的值
sed -i 's/\$releasever/7/g' /etc/yum.repos.d/CentOS-Base.repo

# 4. 生成缓存
yum makecache

简单说一下第四步操作:因为和 CentOS 系统的部分环境变量不同,所以按照该文件中的用法会导致找不到资源404错误。$releasever 环境变量在 Oracle Linux 下值为 7Server,而在 CentOS 7 系统中为 7,所以直接使用会出错。

EPEL 镜像源配置

# 下载镜像源
wget -O /etc/yum.repos.d/epel.repo https://mirrors.aliyun.com/repo/epel-7.repo

yum install epel-release

OK!现在已经可以正常使用 yum 命令了~

微信小程序防爬虫、黄牛方案设计

近期公司新上了疫苗预约功能,由于目前九价、四价疫苗的火爆程度,很快就吸引了各路牛马扫描爆破接口。因为缺乏防控措施,导致了这些爬虫、脚本对疫苗资源的抢占挤压,更有些不讲武德完全不做频率限制的爬虫脚本,一秒中查询调用11次,给服务产生了不良影响。

因此,需要设计一套方案来避免黄牛抢占资源,为广大群众创造出更加公平可靠的疫苗预约平台🎉

方案设计

一、请求响应加密

对报文使用加密可以避免被抓包,让攻击者抓包窃取到密文也无法理解。虽然目前小程序都是使用的 HTTPS 协议,但是攻击者依旧可以通过证书劫持的方法进行抓包,因此即使应用层已加密,报文本身是明文的依旧会有风险。

在使用加密时,存在一个问题就是密钥交换。在密钥交换的时候无法确保密钥被安全、无误的传输。如果密钥写到前端代码中,攻击者获取到前端代码后可以轻松窃取到密钥。在浏览微信小程序文档的时候,发现了微信小程序有一个获取加密密钥的API:UserCryptoManager.getLatestUserKey

const userCryptoManager = wx.getUserCryptoManager()
userCryptoManager.getLatestUserKey({
  success: res => {
    const {encryptKey, iv, version, expireTime} = res
    console.log(encryptKey, iv, version, expireTime)
  }
})

对报文请求进行加密,并在接口参数中加上时间戳参数,可以有效的避免被抓包和重放攻击,并且这个过程对于正常用户而言是无感的,完全不会影响使用体验。这个方案的缺点是,无法避免自动化UI脚本在真实的小程序运行环境下模拟操作,使用自动化UI脚本刷号源

二、请求频率限制

正常用户使用程序,会存在一定的行为特征,和机器的一个典型区别就是请求频率。在应用此方案的过程中,需要先统计出用户的使用频率,取一个阈值作为机器人的判断标准,当超出阈值后直接返回错误,一定时间后恢复。

为了最大程度的避免对正常用户的影响,在统计用户使用频率和阈值选取这块需要多加注意,并且可以结合多种频率限制规则,如一秒中允许请求多少3次,一分钟允许请求100次,一小时允许请求3000次。具体的频率限制规则需要按照具体业务来制定,具体的实现可以使用 Nginx 的 ngx_http_limit_req_module 轻松完成。

三、来源限制

目前注意到,脚本刷号的主要来源是PC端,即使用 Windows 端微信打开小程序,结合 Windows 上丰富的自动化UI工具刷号源。对于这种情况可以在代码中直接封死PC端,只允许在移动端操作,这块的选择需要自己权衡影响。

四、验证码

是的,这种最传统的、最影响用户使用体验的方案还是来了,为了区分正常用户和自动化UI脚本,需要使用验证码来区分开这类用户,验证码无疑是针对这个场景最有效的方案了。

当然,不一定非要使用最传统的字母数字验证码,甚至可以使用人脸识别,结合请求频率等规则使用。当用户请求频率到一定程度,需要人脸识别来判断是本人操作。

验证码的具体实现有很多种,这里不展开阐述。

五、机器学习风控系统(猜想原型)

这一步目前还在猜想阶段,因为最近在学习机器学习,意识到这是具有一定可行性的。黄牛党的刷号、预约、取消号源具有特定的行为模式,我们可以通过数据分析出特征,使用机器学习的方式识别出异常行为。

但是机器学习得出结果过程是一个黑盒,当出现用户投诉情况的话,不好核实处理。且存在成本相对较高的问题,所以此方案搁置。

Java 的 hashCode 与 equals

Java 的 hashCode 和 equals 是 java.lang.Object 的两个方法,这意味着所有的 Java 对象都存在这两个方法。当我们在使用 Map、Set 数据结构时,都要或多或少的和此方法打交道。亦或使用 Hibernate 在定义 Entity 实体类时,也需要注意这两个方法的重写。掌握 hashCode 与 equals 方法有利于加深对 Java 数据结构的理解,并且还能帮助我们避免一些低级 Bug。

Kotlin 与 Java 类似,但 Kotlin 的一些语法糖使得背后的类转换变得隐晦起来。例如 Kotlin 的比较操作符 == 实际调用的是类的 equals;Kotlin 可以针对列表进行取差操作,而这个操作的背后需要先把对应数据集合类型转成 HashSet ,然后使用 HashSet 的取差方法。

hashCode

hashCode 方法是由 Java 的哈希算法生成的 int 类型哈希值,既然是哈希值,这就意味着两个不同的对象也可能拥有相同的哈希值

哈希算法是把任意长度的输入转换成定长输出。

在 JavaDoc 中有说明此方法主要用于需要哈希表结构的数据结构,如 java.util.HashMapjava.util.HashSet ,hashCode 方法受限于以下三个法则:

  • 在程序运行过程中,hashCode 结果是不变的。
  • 如果两个对象 equals 相等,则 hashCode 方法必须相等。
  • 不要求两个 hashCode 相等的对象,equals 也相等。

这三个法则浓缩成一句话:

在程序运行阶段时,两个 equals 相等的对象,hashCode 结果值必须相等。

为什么这么要求呢?因为在使用哈希结构查询数据时,如使用 containsgetremove 等操作时,都会先使用哈希值匹配对应的 bucket,当多个对象出现哈希冲突时,在一个 bucket 中会存在以链表方式连结的对象列表,然后逐个使用 equals 方法进行匹配,以提高查询效率。

任何对象都有 hashCode 方法,如果没有手动重写,Object 的原生实现则会在某种程度上使用内存地址。

不同的 JVM,对 hashCode 的具体实现是不一样的。

equals

equals 方法相对而言就单纯了一些,这个方法就是用来比较两个对象的逻辑相等。类是编程思想中用于对现实世界建模抽象的工具,对应现实生活中的一类事物,而对象则是对应现实生活中的一个实体。现实生活中的实体都是可区分的,具有可标识性;在面向对象编程中使用对象映射现实生活中的实体,要保证对象的标识性,则是使用 equals 方法进行比对。

所以当我们要重写 equals 方法时,需要遵循的原则就是要让对象具有可区分性,能够和现实实体相对应。

开发中需要注意什么

平时开发过程中,大多数类都会使用内置的 hashCode 和 equals 方法,这对日常的开发过程没有任何影响,这常常会让我们忽略了它的重要性,这会在某些情况下造成难以察觉的Bug。

所以需要加深对此的理解,尤其是使用 ORM 或集合操作时,一定要注意 hashCode 和 equals 方法的重写。只要遵循上面提到的法则,就能够避免很多问题了。

Also See

Java中Date存储到MySQl字段时间不一致问题

问题情景

在 Java 代码中使用 new Date() 赋值日期,存储到数据库 DATETIME 字段后,在数据库中多了一秒,经过排查是数据库对Java Date 值进行了四舍五入,导致此问题。

问题剖析

Java 的 java.util.Date 类时间精确度到毫秒(milliseconds),MySQl 中的日期时间类 TIMEDATETIMETIMESTAMP 最大可以支持 6 位,即精确到毫秒(microseconds),以格式 type_name(fsp) 格式进行定义,如 DATETIME(3) 为定义精确到秒后 3 位(毫秒)。当不指定 fsp 的时候,默认值为 0,即最大精确度保留到秒。

此问题场景下,MySQL 的 DATETIME 未指定精确度,最大精确度为秒,而 java.util.Date 精确度为毫秒,MySQl会对末尾的时间进行四舍五入,当毫秒值大于 500 时,便会进 1 位增加一秒,造成了此问题。

从 MySQl 8 开始,可以通过启用 TIME_TRUNCATE_FRACTIONAL 参数使用截断模式,即直接移除末尾小数,不进行四舍五入。

测试代码

DROP TABLE IF EXISTS fractest;  
CREATE TABLE fractest( c1 TIME(2), c2 DATETIME(2), c3 TIMESTAMP(2) ); 

INSERT INTO fractest VALUES  
    ('17:51:04.777', '2018-09-08 17:51:04.777', '2018-09-08 17:51:04.777');  
INSERT INTO fractest VALUES  
    ('17:51:04.555', '2018-09-08 17:51:04.555', '2018-09-08 17:51:04.555');  
INSERT INTO fractest VALUES  
    ('17:51:04.444', '2018-09-08 17:51:04.444', '2018-09-08 17:51:04.444');  
SELECT * FROM fractest;

Also see

❌