之所以想跟大家分享怎么做好基础的接口安全,一方面是工作需要,之前这部分的工作是我在负责,所以正好有这部分的经验;二是我之前也接触过很多做后端的同学,对于这一块大部分涉及不深或不太了解,也不知道怎么样做才算相对安全。所以把我的想法写出来,供大家参考和讨论,共同学习和进步。
当然了,我分享的也只是我的个人经验,自然有很多的不足之处,也并不是说我后续阐述的一些观点和实现就一定是安全的,大家要加入自己的判断,并且混入一些定制化的东西
本章节内容,主要说的HTTP
接口的安全设计,涉及内容包括防窃听、防篡改、防重放、密钥传输安全、密钥存储安全、敏感数据处理等。适用于负责后端、测试或信息接口安全的同学
为了解决这类问题,前端可以给数据加上一个签名,比如采用MD5
最简单的签名方式,可以把请求参数当作待签名的数据,计算一个MD5
值,然后传给服务端,服务端同样用参数生成MD5
值进行比对,发现不一样,那说明数据被篡改过了。
数据加签的机制是没有问题的,并且确实可以解决防篡改的问题,但是算法和机制要选好。
无论是MD5、HMAC、还是其它类型的摘要算法,都有一个问题,就是篡改者要是知道了算法,那还是可以篡改。所以要选用非对称算法来做数据的加签与验签,这样就算篡改者知道了算法,但是没有私钥
也篡改不了,因为非对称的机制就是用私钥
签名,公钥
验签,公钥
可以公开出去。
比如客户端用客户端的私钥对请求参数进行签名,并把公钥
给到服务端,服务端在收到请求后,用客户端的公钥
进行验签。就算篡改者知道了客户端的公钥
,也无法对请求数据进行篡改,此时客户端的重心就转移到了,如何保护它的私钥
问题上了,而不用担心数据会被别人篡改
同样的,服务端把自己的公钥给到客户端,服务端在响应数据的时候,进行签名,客户端进行验签,这就是双签。即客户请求时,使用客户端的私钥签名,服务端验,服务端响应时,使用服务端的私钥签名,客户端验。来确保数据交互时请求和响应都不会被篡改。
常用的算法有RSA1024、RSA2048、SM2,当然还有一些其它ECC类算法。早期大家用的基本是RSA1024,现在大部分都用2048或更长的密钥
来生成签名了。当然也有用国密的,不过用的少,像我们公共交通行业用的多,还有就是国企。我现在设计系统,基本是全国密体系了,除了一些要给第三方调用的,会做RSA+SM2,就是任选其一。
SM2有个好处就是生成的签名是64字节的,RSA的话,密钥
越长,签名值就越长,计算复杂度也越高,系统负载当然也会跟着上去。国密应用这么多年了,安全性还是不用担心的。
接下来,我们就用国密来实现数据的加签,实战一波。
对于GET
类的请求,我们可以将参数先按字母进行一个排序,然后把他们拼装起来,比如:
curl -v 'http://127.0.0.1:8080/testGet?p=testp&ab=123&x=456'
那么参数排序,并拼装后变成:
ab=123&p=testp&x=456
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
对于传输的数据是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
),以减少数据库这方面的查询压力。
GET
类的请求,通常的参数的值可能需要经过encodeURI
处理,这里指的是拼装待签名数据的时候,请求传参时不需要POST
类请求,在写拦截器的时候,要定制InputStream,因为拦截验签的时候,会先把BODY读出来,所以处理完要写回去,或采用其它类似方法处理针对Redis不可用的降级处理呢,我个人想法是这样的,可以事先根据用户的公钥,使用服务端的私钥生成一个证书,下发给客户端,比如证书内容为:
序号 | 字段名 | 字段类型 | 长度(单位字节) | 备注 |
---|---|---|---|---|
1 | 用户标识 | ANSI | N | 用户唯一标识 |
2 | 证书签发时间 | HEX | 4 | UTC时间戳,单位秒 |
3 | 证书失效时间 | HEX | 4 | UTC时间戳,单位秒 |
4 | 密钥索引 | HEX | 1 | 使用服务端的哪个私钥签发的 |
5 | 客户端公钥 | HEX | 33 | 压缩SM2公钥 |
6 | 数字签名 | HEX | 64 | 使用指定索引下的私钥对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×tamp=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×tamp=1680503150&nonce=wX5krzyVHuaYS8Ta
如果客户端超时重传,需要产生新的Nonce,要不可能会被拒
Nonce需要配合Timestamp一起,比如将Nonce存放到Redis,给Nonce设置一个过期时间,这个过期时间可以为Timestamp的有效期
如果条件允许,可以将Nonce缓存独立出来,避免故障引起服务不可用