在企业级应用中,将服务拆分解耦是很常见的,所以也就有了服务器间调用API的场景。
一般会将提供基础能力的服务独立部署,然后前端业务应用通过API去调用这些基础能力。由于前端业务应用和基础服务一般是多对一的关系,故在调用API的时候,前端业务应用需要标识身份,以便基础服务能够针对性地提供服务。
设定个场景
先具象化的设定一个场景,后面比较容易说清楚:
服务S提供了一个短信发送的API,即调用此服务可以实现给指定号码发送短信。有A、B、C业务应用会使用这个服务,且服务S需要知道哪些业务调用了它。
这个服务的API调用方式是通过HTTP的GET方式(不要吐槽这个,这是确实可行的)
http://service.domain.com/sms?
number=17012345678&
content=helloworld
简单的方式
如果A、B、C和S在同一个私网内,且API访问仅限此网内,A、B、C也均可信可控,那么根本不用麻烦,只要加上一个标识参数告知S即可。看起来就像这样:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA
使用Token
如果业务部门比较分散,导致A、B、C并不完全可信,不排除会出现B使用A的appId的这类冒名的情况。
那么S可以给A、B、C分别预先生成一个Token,要求在请求时一并发送,并会校验appId和token是否匹配。看起来就像这样:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
token=0UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ
这样,各业务就不能冒用标识了。
使用Signature (签名)
Token相当于是一个密码,那么上述的方式等于将密码明文传输了,不是太妥当。所以可以再改进一下:
- 将appId和token作为字符串连接,进行一次SHA1计算(MD5也行),生成一个signature;
- 不再传输token,而是传输appId和signature;
- S收到请求后,通过appId和token的作同样计算,校验signature是否一致。
所以请求就变成这样:(这里用了SHA1计算)
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
signature=6f3db6934eeb685cfdb2295c35856f00ebea29a3
加入时间戳
如果私网内存在不可控的服务器或者干脆就是在公网上通信,那目前实际上仍然不是非常安全,因为上述的appId和signature在每次调用的时候是不变的,如果被非法调用者得知,仍然可以冒名。再进一步改进:
- API增加一个必选参数timestamp,即当前时间的Unix时间戳,单位到秒;
- 同时,要求使用appId、timestamp、token三者相接计算signature;
- S收到请求后,不仅校验signature是否一致,还校验时间是否为当前时间。(由于各服务器时间存在误差,所以这里实际是比较时间戳和当前时间是否在一个范围内,在此设为1分钟)
请求就进化为:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
timestamp=1502610966&
signature=ff0447ab272947edd965df6d2ef19576eabb3fe9
这样,即使签名被非法得知,也仅仅能在设定的几分钟范围内被调用,大大降低了风险。
限制Signature重复
当然,最好的情况是完全杜绝被非法调用。可以进一步处理:
- 在S暂存每次请求的signature,保证不能重复使用。每个signature暂存时间为之前设定的时间范围1分钟。
- 如果是低频API,每秒调用最多调用一次的,不受影响。
- 如果是高频API,则需要保证每次的签名不同,不然在同一秒的请求会被受限;可以再增加一个noise的字段,值为随机字符串(一般为4位字符),并加入到signature计算中。
所以,像这样请求:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
timestamp=1502610966&
noise=xWk2&
signature=c2b7e467a7bd14bf2ef768702be1c7f6f95a2d09
再加上S上做的限制signature重复使用,可以保证signature泄露的时候不会造成非法调用。
参数防篡改
还有一种更糟的情况,就是A、B、C发往S的请求被劫持,劫持者修改了手机号码和短信内容,再发往S。这样,signature是不会重复使用的,仍然能够通过校验。
所以更好的办法是,把业务参数即number和content的值也加入到signature计算中。这里需要注意的是,为了更通用以及确保字符串连接的顺序一致,须按照参数名对除signature以外的所有参数(包括token)进行一个排序,然后将其值连接。
拿例子来说,排序好是这样:
http://service.domain.com/sms?
appId=appNameA&
content=helloworld&
noise=xWk2
number=17012345678&
timestamp=1502610966&
(token=0UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ)
然后将appNamehelloworldxWk21701234567815026109660UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ
这样一个字符串做SHA1计算,得到最终的请求:
http://service.domain.com/sms?
appId=appNameA&
content=helloworld&
noise=xWk2
number=17012345678&
timestamp=1502610966&
signature=76168273fd018b89df674d5275a6c16f3daf9b10
大杀器
如果A、B、C三者的网路环境不复杂,可以固定IP的话,在S上通过IP来验证即可。轻松加愉快。
附
上述内容中一些计算方法:(NodeJS)
//计算token和noise
function generateToken(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var token = [], i;
radix = radix || chars.length;
if(!len) len = 32;
for (i = 0; i < len; i++) token[i] = chars[0 | Math.random()*radix];
return token.join('');
}
//计算签名
const crypto = require('crypto');
function sha1(input){
return crypto.createHash('sha1').update(input).digest('hex')
}
本文同步自本人博客:https://phxsun.com/post/authentication-between-servers