您的当前位置:首页正文

接口安全设计之防篡改和防重放

2024-11-24 来源:个人技术集锦

概述

背景

之所以想跟大家分享怎么做好基础的接口安全,一方面是工作需要,之前这部分的工作是我在负责,所以正好有这部分的经验;二是我之前也接触过很多做后端的同学,对于这一块大部分涉及不深或不太了解,也不知道怎么样做才算相对安全。所以把我的想法写出来,供大家参考和讨论,共同学习和进步。

当然了,我分享的也只是我的个人经验,自然有很多的不足之处,也并不是说我后续阐述的一些观点和实现就一定是安全的,大家要加入自己的判断,并且混入一些定制化的东西

适用范围

本章节内容,主要说的HTTP接口的安全设计,涉及内容包括防窃听、防篡改、防重放、密钥传输安全、密钥存储安全、敏感数据处理等。适用于负责后端、测试或信息接口安全的同学

防篡改

数据加签

为了解决这类问题,前端可以给数据加上一个签名,比如采用MD5最简单的签名方式,可以把请求参数当作待签名的数据,计算一个MD5值,然后传给服务端,服务端同样用参数生成MD5值进行比对,发现不一样,那说明数据被篡改过了。

数据加签的机制是没有问题的,并且确实可以解决防篡改的问题,但是算法和机制要选好。

无论是MD5、HMAC、还是其它类型的摘要算法,都有一个问题,就是篡改者要是知道了算法,那还是可以篡改。所以要选用非对称算法来做数据的加签与验签,这样就算篡改者知道了算法,但是没有私钥也篡改不了,因为非对称的机制就是用私钥签名,公钥验签公钥可以公开出去。

比如客户端用客户端的私钥对请求参数进行签名,并把公钥给到服务端,服务端在收到请求后,用客户端的公钥进行验签。就算篡改者知道了客户端的公钥,也无法对请求数据进行篡改,此时客户端的重心就转移到了,如何保护它的私钥问题上了,而不用担心数据会被别人篡改

双签(双向传输加密)

同样的,服务端把自己的公钥给到客户端,服务端在响应数据的时候,进行签名,客户端进行验签,这就是双签。即客户请求时,使用客户端的私钥签名,服务端验,服务端响应时,使用服务端的私钥签名,客户端验。来确保数据交互时请求和响应都不会被篡改。

常用算法

常用的算法有RSA1024、RSA2048、SM2,当然还有一些其它ECC类算法。早期大家用的基本是RSA1024,现在大部分都用2048或更长的密钥来生成签名了。当然也有用国密的,不过用的少,像我们公共交通行业用的多,还有就是国企。我现在设计系统,基本是全国密体系了,除了一些要给第三方调用的,会做RSA+SM2,就是任选其一。

SM2有个好处就是生成的签名是64字节的,RSA的话,密钥越长,签名值就越长,计算复杂度也越高,系统负载当然也会跟着上去。国密应用这么多年了,安全性还是不用担心的。

接下来,我们就用国密来实现数据的加签,实战一波。

加签数据设计

GET请求

对于GET类的请求,我们可以将参数先按字母进行一个排序,然后把他们拼装起来,比如:

curl -v 'http://127.0.0.1:8080/testGet?p=testp&ab=123&x=456'

那么参数排序,并拼装后变成:

ab=123&p=testp&x=456

POST请求

POST如果是FORMDATA形式,我们也可以像GET请求一样,先将参数进行排序,再拼装,比如:

curl -v -H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' --data-binary "p=test1&ab=123&x=456" 'http://127.0.0.1:8080/testPostFromdata'

同时按照GET的参数拼装方式,拼装后变成:

ab=123&p=test1&x=456

POST BODY

对于传输的数据是BODY形式的,我们可以约定一种拼装方式,把BODY原封不动的作为entity的值来拼接,如:

curl -v -H 'Content-Type: application/json' --data-binary '{
>     "pInt": 1,
>     "pBoolean": false,
>     "pString": "ssss"
> }' 'http://127.0.0.1:8080/testPostBody'

拼接后变成:

entity={"pInt": 1,"pBoolean": false,"pString": "ssss"}

这里entity的值就是BODY的内容,原封不动,有换行也是要体现,这里为了方便大家查看,换行符我就去掉了。

问题一

有了这个规则之后呢,要签名的数据是有了,但大家有没发现一个问题,如果请求参数是空的,咋办,没有数据可签肯定也不行。

问题二

如果两个不同的方法参数个数是一样的,参数名也一样,那是不是有了签名值,别的老六就可以作文章了。

问题解决方案

curl -v 'http://127.0.0.1:8080/testGet?p=testp&ab=123&x=456'
ab=123&p=testp&x=456&path=/testGet

这样是不是即解决了参数可能为空,又避免了别人用同样的参数和签名值去访问其它接口的问题。

签名值存放

关于签名值放置的位置,可以根据大家的需要,放请求头Header里或公共参数都可以。

关于算法

关于SM2签名算法有几点跟大家提一提。

集群部署

通常为满足集群部署,水平扩展的要求,我们需要把用户用于验签相关的内容缓存到内存中(比如Redis),以减少数据库这方面的查询压力。

注意事项

  1. 对于GET类的请求,通常的参数的值可能需要经过encodeURI处理,这里指的是拼装待签名数据的时候,请求传参时不需要
  2. 对于POST类请求,在写拦截器的时候,要定制InputStream,因为拦截验签的时候,会先把BODY读出来,所以处理完要写回去,或采用其它类似方法处理
  3. 这类非功能性需求,如果条件允许,可以将缓存用户公钥这类服务独立出来,增加服务的可用性。
  4. 前端和后端加密的参数一定要保证每个参数的数据类型一致,否则最后加密的结果可能不同(博主亲自踩的坑)

降级处理

针对Redis不可用的降级处理呢,我个人想法是这样的,可以事先根据用户的公钥,使用服务端的私钥生成一个证书,下发给客户端,比如证书内容为:

序号字段名字段类型长度(单位字节)备注
1用户标识ANSIN用户唯一标识
2证书签发时间HEX4UTC时间戳,单位秒
3证书失效时间HEX4UTC时间戳,单位秒
4密钥索引HEX1使用服务端的哪个私钥签发的
5客户端公钥HEX33压缩SM2公钥
6数字签名HEX64使用指定索引下的私钥对1-5进行签名

一旦发生Redis不可用,防篡改这块业务需要降级处理的话,就让客户端在请求时,将证书也发过来,服务端先验证书,证书有效的情况下,用证书里的公钥验客户端签名。

这种方法呢,会增加客户端的流量、服务端流量以及服务器的负载

防重放一

上一章节,为了防篡改,我们给第一个请求增加了签名和验签,这样就可以解决数据防篡改问题。你觉得这样就安全了吗?我们看,同一接口,同样的参数,同样的签名值,是不是每次调用,都是可以成功的。如果被恶意利用,非法用户疯狂的调用这一个接口呢,服务器资源是不是就浪费了,如果这个接口业务比较复杂,还要操作数据库,正好操作数据库也耗时,那是不是有可能给搞的当机了。

那有没有办法解决这个问题呢,当然有。比如再给每一个请求加上一个时间戳并且时间戳也加入签名防篡改,是不是可以先验证这个请求是什么时候发起的,有没有超过指定时间(比如3分钟),超过就直接丢弃。

比如这个接口:

curl -v -H 'X-TimeStamp: 1680503150' 'http://127.0.0.1:8080/testGet?p=testp&ab=123&x=456'

我们在Header里,多传了一个X-TimeStamp,值为距离1970-01-01的UTC秒数,为了防止别人篡改呢,也需要将其作为签名的数据,比如固定参数名为timestamp,放置在path后面,也不参与排序,那么待签名数据就变成了:

ab=123&p=testp&x=456&path=/testGet&timestamp=1680503150

遗留问题

  • 加入时间戳后,可以有效的防止重放,但有效期内请求仍然有效,所以需要加入其它方案来增强,比如限流。又比如后续讲到的nonce(随机串)

  • 时间戳有一个客户端与服务器时间可能会不同步的问题,所以多少分钟内有效,要根据业务需要来定。另外也可以在服务器加入获取服务器当前时间的接口,来同步客户端与服务端的时间。

  • 如果加了获取服务器时间这种无状态的接口,也要考虑加入签名及验签机制,或者限流机制,防止攻击。

防重放二

上面讲到了,为了防止接口重放,我们给每一个请求加上一个时间戳,但是这样并没有完全地解决防重放问题,有效期内还是可以多次调用

所以为了解决这一问题呢,我们可以在每一个请求上,再加上一个随机串nonce,客户端每个请求的nonce我都记录下来,那么下一次再次请求的时候,就先根据nonce查一下,有没有请求过,请求过就丢弃,没有就正常处理。

比如这个接口:

curl -v -H 'X-TimeStamp: 1680503150' -H 'X-Nonce: wX5krzyVHuaYS8Ta' 'http://127.0.0.1:8080/testGet?p=testp&ab=123&x=456'

我们在Header里,多传了一个X-Nonce,值为wX5krzyVHuaYS8Ta(随机生成的),为了防止别人篡改呢,也需要将其作为签名的数据,比如固定参数名为nonce,放置在timestamp后面,也不参与排序,那么待签名数据就变成了:

ab=123&p=testp&x=456&path=/testGet&timestamp=1680503150&nonce=wX5krzyVHuaYS8Ta

注意事项

  • 如果客户端超时重传,需要产生新的Nonce,要不可能会被拒

  • Nonce需要配合Timestamp一起,比如将Nonce存放到Redis,给Nonce设置一个过期时间,这个过期时间可以为Timestamp的有效期

  • 如果条件允许,可以将Nonce缓存独立出来,避免故障引起服务不可用

显示全文