为了防止API接口中的数据被篡改,很多时候我们需要对API接口做签名。
接口请求方将请求参数 + 时间戳 + 密钥拼接成一个字符串,然后通过md5等hash算法,生成一个前面sign。
然后在请求参数或者请求头中,增加sign参数,传递给API接口。
API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。
如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。
如果两个sign不相等,则API接口的网关服务会直接返回签名错误。
问题来了:签名中为什么要加时间戳?答:为了安全性考虑,防止同一次请求被反复利用(窃取请求内容,反复调用),增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:正负5分钟。
这样一次请求,在5分钟之内是有效的,超过5分钟,API接口的网关服务会返回超过有效期的异常提示。
目前生成签名中的密钥有两种形式:
一种是双方约定一个固定值privateKey。
另一种是API接口提供方给出AK/SK两个值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口提供方,这样API接口提供方可以根据AK获取到SK,而生成新的sgin。
有些时候,我们的API接口直接传递的非常重要的数据,比如:用户的登录密码、银行卡号、转账金额、用户身份证等,如果将这些参数,直接明文,暴露到公网上是非常危险的事情。
由此,我们需要对数据进行加密。
比如:用户注册接口,用户输入了用户名和密码之后,需要将密码加密。
我们可以使用AES对称加密算法。
在前端使用公钥对用户密码加密。
然后注册接口中,可以使用密钥解密,做一些业务需求校验。然后再换成其他的加密方式加密,保存到数据库当中。
为了进一步加强API接口的安全性,防止接口的签名或者加密被破解了,攻击者可以在自己的服务器上请求该接口。
需求限制请求ip,增加ip白名单。
只有在白名单中的ip地址,才能成功请求API接口,否则直接返回无访问权限。
ip白名单也可以加在API网关服务上。
但也要防止公司的内部应用服务器被攻破,这种情况也可以从内部服务器上发起API接口的请求。
这时候就需要增加web防火墙了,比如:ModSecurity等。
如果你的API接口被第三方平台调用了,这就意味着着,调用频率是没法控制的。
第三方平台调用你的API接口时,如果并发量一下子太高,可能会导致你的API服务不可用,接口直接挂掉。
由此,必须要对API接口做限流。
限流方法有三种:
我们在实际工作中,可以通过nginx,redis或者gateway实现限流的功能。
我们需要对API接口做参数校验,比如:校验必填字段是否为空,校验字段类型,校验字段长度,校验枚举值等等。
在Java中校验数据使用最多的是hiberate的Validator框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。
我之前调用过别人的API接口,正常返回数据是一种json格式,比如:
{
"code":0,
"message":null,
"data":[{"id":123,"name":"abc"}]
},
签名错误返回的json格式:
{
"code":1001,
"message":"签名错误",
"data":null
}
所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。
我们的API接口需要对异常进行统一处理。
不知道你有没有遇到过这种场景:有时候在API接口中,需要访问数据库,但表不存在,或者sql语句异常,就会直接把sql信息在API接口中直接返回。
返回值中包含了异常堆栈信息、数据库信息、错误代码和行数等信息。
因此非常有必要对API接口中的异常做统一处理,把异常转换成这样:
{
"code":500,
"message":"服务器内部错误",
"data":null
}
我们可以在gateway中对异常进行拦截,做统一封装,然后给第三方平台的是处理后没有敏感信息的错误信息。
在第三方平台请求你的API接口时,接口的请求日志非常重要,通过它可以快速的分析和定位问题。
我们需要把API接口的请求url、请求参数、请求头、请求方式、响应数据和响应时间等,记录到日志文件中。
最好有traceId,可以通过它串联整个请求的日志,过滤多余的日志。
第三方平台极有可能在极短的时间内,请求我们接口多次,比如:在1秒内请求两次。有可能是他们业务系统有bug,或者在做接口调用失败重试,因此我们的API接口需要做幂等设计。
也就是说要支持在极短的时间内,第三方平台用相同的参数请求API接口多次,第一次请求数据库会新增数据,但第二次请求以后就不会新增数据,但也会返回成功。
这样做的目的是不会产生错误数据。
我们在日常工作中,可以通过在数据库中增加唯一索引,或者在redis保存requestId和请求参来保证接口幂等性。
对于对我提供的批量接口,一定要限制请求的记录条数。
如果请求的数据太多,很容易造成API接口超时等问题,让API接口变得不稳定。
通常情况下,建议一次请求中的参数,最多支持传入500条记录。
如果用户传入多余500条记录,则接口直接给出提示。
建议这个参数做成可配置的,并且要事先跟第三方平台协商好,避免上线后产生不必要的问题。
对于一次性查询的数据太多的情况,我们需要将接口设计成分页查询返回的。
一般的API接口的逻辑都是同步处理的,请求完之后立刻返回结果。
但有时候,我们的API接口里面的业务逻辑非常复杂,特别是有些批量接口,如果同步处理业务,耗时会非常长。
这种情况下,为了提升API接口的性能,我们可以改成异步处理。
在API接口中可以发送一条mq消息,然后直接返回成功。之后,有个专门的mq消费者去异步消费该消息,做业务逻辑处理。
直接异步处理的接口,第三方平台有两种方式获取到。
第一种方式是:我们回调第三方平台的接口,告知他们API接口的处理结果,很多支付接口就是这么玩的。
第二种方式是:第三方平台通过轮询调用我们另外一个查询状态的API接口,每隔一段时间查询一次状态,传入的参数是之前的那个API接口中的id集合。
有时候第三方平台调用我们API接口时,获取的数据中有一部分是敏感数据,比如:用户手机号、银行卡号等等。
这样信息如果通过API接口直接保留到外网,是非常不安全的,很容易造成用户隐私数据泄露的问题。
这就需要对部分数据做数据脱敏了。
说实话,一份完整的API接口文档,在双方做接口对接时,可以减少很多沟通成本,让对方少走很多弯路。
接口文档中需要包含如下信息:
统一字段的类型和长度,比如:id字段用Long类型,长度规定20。status字段用int类型,长度固定2等。
统一时间格式字段,比如:time用String类型,格式为:yyyy-MM-dd HH:mm:ss。
接口支持的请求方式有很多,比如:GET、POST、PUT、DELETE等等。
我们在设计接口的时候,要根据实际情况选择使用哪种请求方式。
实际工作中使用最多的是:GET和POST,这两种请求方式。
如果没有输入参数的接口,可以使用GET请求方式,问题不大。
如果有输入参数的接口,推荐使用POST请求方式,坑更少。
主要原因有下面两点:
我们在设计接口的时候,无论是查询数据、添加数据、修改数据,还是删除的场景,都应该考虑一下能否设计成批量的。
很多时候,需要通过id查询数据详情,比如:通过订单id,查询订单详情。
如果你的接口只支持,通过一个id,查询一个订单的详情。
那么,后面需要通过多个id,查询多个订单详情的时候,就需要额外增加接口了。
如果你添加数据的接口,只支持一条数据一条数据的添加。
后面,有个job需要一次性添加1000条数据的时候,这时在代码中循环1000次,一个个添加,这种做法效率比较低。
为了让你的接口设计的更加通用,满足更多的业务场景,能设计成批量的,尽量别设计成单个的。
我之前见过有些小伙伴设计的接口,在入参中各种条件都支持,在Service层有N多的if…else判断。
而且返回的实体类中,包含了各种场景下的返回值字段,字段很多很全。
接口上线一年之后,自己可能都忘了,在哪些业务场景下,要传入哪些字段,返回值是哪些字段。
这类接口的维护成本非常高,而且又不敢轻易重构,怕改了A业务场景,影响B业务场景的功能,这种接口让人非常痛苦的。
好的接口设计原则是:职责单一。
比如用户下单的场景,有web端和移动端。
而每个端都有普通下单和快速下单,两种不同的业务场景。
我们在设计接口的时候,可以将web端和移动端的接口在controller层完全分开。
/web/v1/order
/mobile/v1/order
并且将普通下单和快速下单也分开:
/web/v1/order/create
/web/v1/order/fastCreate
/mobile/v1/order/create
/mobile/v1/order/fastCreate
这样可以设计成4个接口。业务逻辑更清晰一些,方便后面维护。