背景
首先思考一个重要的问题:如果我们为开发者提供了一个接口,却对于调用者一无所知,这是一件很恐怖的事情。
假设我们的 Server 只能够允许 100 个人同时调用接口。如果有攻击者疯狂请求这个接口,这是很危险的。一方面可能会损害我们的安全性,另一方面也可能耗尽 Server 性能,影响正常用户的使用。
因此我们必须为接口设置保护措施,例如限制每个用户每秒只能调用十次接口,即实施请求频次的限额控制。
如果在后期,我们的业务扩大,可能还需要收费。因此,我们必须知道谁在调用接口,并且不能让无权限的人随意调用。
那么现在,我们需要设计一个方法,来确定谁在调用接口。在我们之前开发后端时,我们会进行一些权限检查。例如,当管理员执行删除操作时,后端需要检查这个用户是否为管理员。那么,我们如何获取用户信息呢,是否直接从后端的 session 中获取?但问题来了,当我们调用接口时,我们有 session 吗?比如说,前端直接发起请求,没有登录操作,没有输入用户名和密码,这个时候怎么去调用呢?因此,一般情况下,我们会采用一个叫API签名认证的机制。这是一个重要的概念。
什么是 API 签名认证?
简单来说,如果你想来我家做客,我不可能随便让任何陌生人进来。所以我会提前给你发一个类似于请帖的东西,作为授权或许可证。当你来访问我的时候,你需要带上这个许可证。我可能并不认识你,但我认识你的请帖,只要你有这个请帖,我就允许你进来。
所以,API签名认证主要包括两个过程,
- 签发签名
- 使用签名和校验签名。
为什么需要签名?
为了保证安全性,不能让任何人都可以调用接口。
如何实现呢?
我们需要两个 Key,即 accessKey
和 secretKey
。这跟用户名和密码类似,不过每次调用接口都需要带上,实现无状态的请求。这样,即使之前没有来过,这次的状态正确也可以调用接口,所以我们需要这两个东西来标识用户。
签名认证实现
通过 HTTP Request Header
传递参数:
- 参数1:
accessKey
,调用的标识,复杂、无序、无规律 - 参数2:
secretKey
,密钥,复杂、无序、无规律,该参数不直接放在请求头中,而是通过某种算法生成签名
类似于 用户名 / 密码,区别是 AK / SK 是无状态的。
- 参数3:用户请求参数
- 参数4:
Sign
,签名
加密方式
对称加密、非对称加密、MD5摘要等等。
过程:
graph LR left[用户参数 + 密钥] -- 签名生成算法 --> right["签名(不可解密的值)"]
如何校验签名呢?
服务端在拿到用户传递的参数时,只需要使用一模一样的参数和算法生成签名,只要和用户传递的一致,就表示正确。
如何防止请求重放?
- 参数5:
nonce
,随机数,只能用一次,服务端要保存使用过的随机数 - 参数6:
timestamp
,时间戳,校验时间戳是否过期
第一种方式是通过加入一个随机数(nonce)实现标准的签名认证。每次请求时,发送一个随机数给后端。后端只接受并认可该随机数一次,一旦随机数被使用过,后端将不再接受相同的随机数。这种方式解决了请求重放的问题,因为即使对方使用之前的时间和随机数进行请求,后端会认识到该请求已经被处理过,不会再次处理。然而,这种方法需要后端额外开发来保存已使用的随机数。并且,如果接口的并发量很大,每次请求都需要一个随机数,那么可能会面临处理百万、千万甚至亿级别请求的情况。因此,除了使用随机数之外,我们还需要其他机制来定期清理已使用的随机数。
第二种方式是加入一个时间戳(timestamp)。每个请求在发送时携带一个时间戳,并且后端会验证该时间戳是否在指定的时间范围内,例如不超过10分钟或5分钟。这可以防止对方使用昨天的请求在今天进行重放。通过这种方式,我们可以一定程度上控制随机数的过期时间。因为后端需要同时验证这两个参数,只要时间戳过期或随机数被使用过,后端会拒绝该请求。因此,时间戳可以在一定程度上减轻后端保存随机数的负担。通常情况下,这两种方法可以相互配合使用。
总结
在标准的签名认证算法中,建议至少添加以下五个参数:accessKey
、secretKey
、sign
、nonce
(随机数)、timestamp
(时间戳)。此外,建议将用户请求的其他参数,例如接口中的
name 参数,也添加到签名中,以增加安全性。
需要注意的是,API 签名认证是一个很灵活的设计,具体要有哪些参数、参数名要根据实际的场景来(比如 userId、appId、version、固定值等)
一些问题
为什么需要两个 Key?
如果仅凭一个 Key 就可以调用接口,那么任何拿到这个 Key 的人都可以无限制的调用这个接口。这就好比,为什么你在登录网站的时候需要输入密码,而不是仅仅输入用户名就可以了?这二者的原理是一样的。
安全传递性
密钥一定不能传递。也就是说,在向对方发送请求时,密钥绝对不能以明文的形式传递,必须通过特殊的方式进行传递。在标准的API签名认证中,我们需要传递一个签名,这个签名是根据密钥生成的。
简单示例
在用户表中添加字段:
create table if not exists user |
用户在申请 ak / sk 时,使用随机生成算法生成:
public static String[] genUserKey(String salt) { |
使用拦截器或者网关层做用户信息的校验:
private User doUserAuth(ServerHttpRequest request) { |
SDK
为什么需要 SDK?
理想情况:开发者只需要关心调用那些接口、传递哪些参数,就跟调用自己写的代码一样简单。
开发Starter的好处:开发者引入Starter之后,可以直接在application.yml
中写配置,自动创建客户端。
如果按照我们上述的过程,用户在请求接口时,可能需要自己组装签名、随机数、时间戳等等,这是很不友好的,比如:
private Map<String, String> getHeaders(String body) { |
这也不利于统一。
进一步说明
为了方便开发者的调用,我们不能让他们每次都自己编写签名算法,这显然很繁琐。因此,我们需要开发一个简单易用的 SDK,使开发者只需关注调用哪些接口、传递哪些参数,就像调用自己编写的代码一样简单。实际上,RPC(远程过程调用)就是为了实现这一目的而设计的。RPC它就是追求简单化调用的理想情况。类似的例子是小程序开发或者调用第三方 API,如腾讯云的 API,它们都提供了相应的 SDK。
如何开发Starter?
- 确认所需要的依赖,需要引入
spring-boot-configuration-processor
(配置文件编写提示) - 过程代码编写
- 配置类编写
- 在
resources
目录下创建META-INF/spring.factories
,其中配置自动配置类org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
一些问题
明确一个核心点:一旦接口投入使用后,肯定要针对这个接口进行 SDK 的开发。你需要不断地完善 SDK,使其适应不断变化的需求。当然,也可以让 SDK 从接口信息表中读取信息,然后动态生成方法等等,这种做法也是可行的,然而,并不建议这样做。实际上,接口的发布可能不太像我们在应用商店发布应用那样灵活,在这种接口的发布过程中,建议还是介入一点人工。
每次发布一个新的接口,都需要对 SDK 进行更新。建议的做法是这样的:由于用户并不关心具体的接口地址,因此你可以让他们直接调用方法名,然后根据这些方法名去动态生成对应的方法。每次发布新接口时,更新 SDK 的操作可以做得非常简单,可以采用脚本的方式,从数据库中读取接口地址,与之前已有的地址进行对比,然后补充相应的方法,这个过程是可行的,事实上,很多公司都在这样做。
推荐参考:[美团开放平台SDK自动生成技术与实践](