概述
背景
之所以想跟大家分享怎么做好基础的接口安全,一方面是工作需要,之前这部分的工作是我在负责,所以正好有这部分的经验;二是我之前也接触过很多做后端的同学,对于这一块大部分涉及不深或不太了解,也不知道怎么样做才算相对安全。所以把我的想法写出来,供大家参考和讨论,共同学习和进步。
当然了,我分享的也只是我的个人经验,自然有很多的不足之处,也并不是说我后续阐述的一些观点和实现就一定是安全的,大家要加入自己的判断,并且混入一些定制化的东西
适用范围
本章节内容,主要说的
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'
可以约定接口地址的键为
path
,并且不参与排序,直接拼在后面,那么上述请求的待签数据是:
ab=123&p=testp&x=456&path=/testGet
这样是不是即解决了参数可能为空,又避免了别人用同样的参数和签名值去访问其它接口的问题。
签名值存放
关于签名值放置的位置,可以根据大家的需要,放请求头
Header
里或公共参数都可以。
关于算法
关于SM2签名算法有几点跟大家提一提。
- 签名肯定都是私钥签名,公钥验签。
- 验签的复杂度比签名高,因为验签有两次点乘一次点加,而签名是一次点乘。
- 签名如果只有公钥的话,要多一次点乘,即从私钥生成公钥,所以服务器写签名算法,就把公私钥都保存起来,可以省掉一次点乘。
- 签名算法在计算z的时候,要传入
userid
,默认都是1234567812345678。这里如果是非标的话,可以定制和作文章,加大安全性。 - 签名需要随机生成大数 in [1, n - 1],不要偷懒,当然别人封装好的,一般都会处理。
集群部署
通常为满足集群部署,水平扩展的要求,我们需要把用户用于验签相关的内容缓存到内存中(比如
Redis
),以减少数据库这方面的查询压力。
注意事项
- 对于
GET
类的请求,通常的参数的值可能需要经过encodeURI
处理,这里指的是拼装待签名数据的时候,请求传参时不需要 - 对于
POST
类请求,在写拦截器的时候,要定制InputStream,因为拦截验签的时候,会先把BODY读出来,所以处理完要写回去,或采用其它类似方法处理 - 这类非功能性需求,如果条件允许,可以将缓存用户公钥这类服务独立出来,增加服务的可用性。
- 前端和后端加密的参数一定要保证每个参数的数据类型一致,否则最后加密的结果可能不同(博主亲自踩的坑)
降级处理
针对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×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缓存独立出来,避免故障引起服务不可用
版权归原作者 多云&秋雨 所有, 如有侵权,请联系我们删除。