在企業(yè)級應(yīng)用中,將服務(wù)拆分解耦是很常見的,所以也就有了服務(wù)器間調(diào)用API的場景。
一般會將提供基礎(chǔ)能力的服務(wù)獨立部署,然后前端業(yè)務(wù)應(yīng)用通過API去調(diào)用這些基礎(chǔ)能力。由于前端業(yè)務(wù)應(yīng)用和基礎(chǔ)服務(wù)一般是多對一的關(guān)系,故在調(diào)用API的時候,前端業(yè)務(wù)應(yīng)用需要標(biāo)識身份,以便基礎(chǔ)服務(wù)能夠針對性地提供服務(wù)。
設(shè)定個場景
先具象化的設(shè)定一個場景,后面比較容易說清楚:
服務(wù)S提供了一個短信發(fā)送的API,即調(diào)用此服務(wù)可以實現(xiàn)給指定號碼發(fā)送短信。有A、B、C業(yè)務(wù)應(yīng)用會使用這個服務(wù),且服務(wù)S需要知道哪些業(yè)務(wù)調(diào)用了它。
這個服務(wù)的API調(diào)用方式是通過HTTP的GET方式(不要吐槽這個,這是確實可行的)
http://service.domain.com/sms?
number=17012345678&
content=helloworld
簡單的方式
如果A、B、C和S在同一個私網(wǎng)內(nèi),且API訪問僅限此網(wǎng)內(nèi),A、B、C也均可信可控,那么根本不用麻煩,只要加上一個標(biāo)識參數(shù)告知S即可。看起來就像這樣:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA
使用Token
如果業(yè)務(wù)部門比較分散,導(dǎo)致A、B、C并不完全可信,不排除會出現(xiàn)B使用A的appId的這類冒名的情況。
那么S可以給A、B、C分別預(yù)先生成一個Token,要求在請求時一并發(fā)送,并會校驗appId和token是否匹配??雌饋砭拖襁@樣:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
token=0UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ
這樣,各業(yè)務(wù)就不能冒用標(biāo)識了。
使用Signature (簽名)
Token相當(dāng)于是一個密碼,那么上述的方式等于將密碼明文傳輸了,不是太妥當(dāng)。所以可以再改進一下:
- 將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
加入時間戳
如果私網(wǎng)內(nèi)存在不可控的服務(wù)器或者干脆就是在公網(wǎng)上通信,那目前實際上仍然不是非常安全,因為上述的appId和signature在每次調(diào)用的時候是不變的,如果被非法調(diào)用者得知,仍然可以冒名。再進一步改進:
- API增加一個必選參數(shù)timestamp,即當(dāng)前時間的Unix時間戳,單位到秒;
- 同時,要求使用appId、timestamp、token三者相接計算signature;
- S收到請求后,不僅校驗signature是否一致,還校驗時間是否為當(dāng)前時間。(由于各服務(wù)器時間存在誤差,所以這里實際是比較時間戳和當(dāng)前時間是否在一個范圍內(nèi),在此設(shè)為1分鐘)
請求就進化為:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
timestamp=1502610966&
signature=ff0447ab272947edd965df6d2ef19576eabb3fe9
這樣,即使簽名被非法得知,也僅僅能在設(shè)定的幾分鐘范圍內(nèi)被調(diào)用,大大降低了風(fēng)險。
限制Signature重復(fù)
當(dāng)然,最好的情況是完全杜絕被非法調(diào)用??梢赃M一步處理:
- 在S暫存每次請求的signature,保證不能重復(fù)使用。每個signature暫存時間為之前設(shè)定的時間范圍1分鐘。
- 如果是低頻API,每秒調(diào)用最多調(diào)用一次的,不受影響。
- 如果是高頻API,則需要保證每次的簽名不同,不然在同一秒的請求會被受限;可以再增加一個noise的字段,值為隨機字符串(一般為4位字符),并加入到signature計算中。
所以,像這樣請求:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
timestamp=1502610966&
noise=xWk2&
signature=c2b7e467a7bd14bf2ef768702be1c7f6f95a2d09
再加上S上做的限制signature重復(fù)使用,可以保證signature泄露的時候不會造成非法調(diào)用。
參數(shù)防篡改
還有一種更糟的情況,就是A、B、C發(fā)往S的請求被劫持,劫持者修改了手機號碼和短信內(nèi)容,再發(fā)往S。這樣,signature是不會重復(fù)使用的,仍然能夠通過校驗。
所以更好的辦法是,把業(yè)務(wù)參數(shù)即number和content的值也加入到signature計算中。這里需要注意的是,為了更通用以及確保字符串連接的順序一致,須按照參數(shù)名對除signature以外的所有參數(shù)(包括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三者的網(wǎng)路環(huán)境不復(fù)雜,可以固定IP的話,在S上通過IP來驗證即可。輕松加愉快。
附
上述內(nèi)容中一些計算方法:(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