在企業(yè)級(jí)應(yīng)用中,將服務(wù)拆分解耦是很常見的,所以也就有了服務(wù)器間調(diào)用API的場(chǎng)景畅涂。
一般會(huì)將提供基礎(chǔ)能力的服務(wù)獨(dú)立部署,然后前端業(yè)務(wù)應(yīng)用通過API去調(diào)用這些基礎(chǔ)能力道川。由于前端業(yè)務(wù)應(yīng)用和基礎(chǔ)服務(wù)一般是多對(duì)一的關(guān)系午衰,故在調(diào)用API的時(shí)候,前端業(yè)務(wù)應(yīng)用需要標(biāo)識(shí)身份冒萄,以便基礎(chǔ)服務(wù)能夠針對(duì)性地提供服務(wù)臊岸。
設(shè)定個(gè)場(chǎng)景
先具象化的設(shè)定一個(gè)場(chǎng)景,后面比較容易說清楚:
服務(wù)S提供了一個(gè)短信發(fā)送的API尊流,即調(diào)用此服務(wù)可以實(shí)現(xiàn)給指定號(hào)碼發(fā)送短信帅戒。有A、B崖技、C業(yè)務(wù)應(yīng)用會(huì)使用這個(gè)服務(wù)逻住,且服務(wù)S需要知道哪些業(yè)務(wù)調(diào)用了它。
這個(gè)服務(wù)的API調(diào)用方式是通過HTTP的GET方式(不要吐槽這個(gè)迎献,這是確實(shí)可行的)
http://service.domain.com/sms?
number=17012345678&
content=helloworld
簡(jiǎn)單的方式
如果A鄙信、B、C和S在同一個(gè)私網(wǎng)內(nèi)忿晕,且API訪問僅限此網(wǎng)內(nèi)装诡,A、B践盼、C也均可信可控鸦采,那么根本不用麻煩,只要加上一個(gè)標(biāo)識(shí)參數(shù)告知S即可咕幻∮娌看起來就像這樣:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA
使用Token
如果業(yè)務(wù)部門比較分散,導(dǎo)致A肄程、B锣吼、C并不完全可信选浑,不排除會(huì)出現(xiàn)B使用A的appId的這類冒名的情況。
那么S可以給A玄叠、B古徒、C分別預(yù)先生成一個(gè)Token,要求在請(qǐng)求時(shí)一并發(fā)送读恃,并會(huì)校驗(yàn)appId和token是否匹配隧膘。看起來就像這樣:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
token=0UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ
這樣寺惫,各業(yè)務(wù)就不能冒用標(biāo)識(shí)了疹吃。
使用Signature (簽名)
Token相當(dāng)于是一個(gè)密碼,那么上述的方式等于將密碼明文傳輸了西雀,不是太妥當(dāng)萨驶。所以可以再改進(jìn)一下:
- 將appId和token作為字符串連接,進(jìn)行一次SHA1計(jì)算(MD5也行)艇肴,生成一個(gè)signature腔呜;
- 不再傳輸token,而是傳輸appId和signature豆挽;
- S收到請(qǐng)求后育谬,通過appId和token的作同樣計(jì)算券盅,校驗(yàn)signature是否一致帮哈。
所以請(qǐng)求就變成這樣:(這里用了SHA1計(jì)算)
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
signature=6f3db6934eeb685cfdb2295c35856f00ebea29a3
加入時(shí)間戳
如果私網(wǎng)內(nèi)存在不可控的服務(wù)器或者干脆就是在公網(wǎng)上通信,那目前實(shí)際上仍然不是非常安全锰镀,因?yàn)樯鲜龅腶ppId和signature在每次調(diào)用的時(shí)候是不變的娘侍,如果被非法調(diào)用者得知,仍然可以冒名泳炉。再進(jìn)一步改進(jìn):
- API增加一個(gè)必選參數(shù)timestamp憾筏,即當(dāng)前時(shí)間的Unix時(shí)間戳,單位到秒花鹅;
- 同時(shí)氧腰,要求使用appId、timestamp刨肃、token三者相接計(jì)算signature古拴;
- S收到請(qǐng)求后,不僅校驗(yàn)signature是否一致真友,還校驗(yàn)時(shí)間是否為當(dāng)前時(shí)間黄痪。(由于各服務(wù)器時(shí)間存在誤差,所以這里實(shí)際是比較時(shí)間戳和當(dāng)前時(shí)間是否在一個(gè)范圍內(nèi)盔然,在此設(shè)為1分鐘)
請(qǐng)求就進(jìn)化為:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
timestamp=1502610966&
signature=ff0447ab272947edd965df6d2ef19576eabb3fe9
這樣桅打,即使簽名被非法得知是嗜,也僅僅能在設(shè)定的幾分鐘范圍內(nèi)被調(diào)用,大大降低了風(fēng)險(xiǎn)挺尾。
限制Signature重復(fù)
當(dāng)然鹅搪,最好的情況是完全杜絕被非法調(diào)用×仕唬可以進(jìn)一步處理:
- 在S暫存每次請(qǐng)求的signature涩嚣,保證不能重復(fù)使用。每個(gè)signature暫存時(shí)間為之前設(shè)定的時(shí)間范圍1分鐘掂僵。
- 如果是低頻API航厚,每秒調(diào)用最多調(diào)用一次的,不受影響锰蓬。
- 如果是高頻API幔睬,則需要保證每次的簽名不同,不然在同一秒的請(qǐng)求會(huì)被受限芹扭;可以再增加一個(gè)noise的字段麻顶,值為隨機(jī)字符串(一般為4位字符),并加入到signature計(jì)算中舱卡。
所以辅肾,像這樣請(qǐng)求:
http://service.domain.com/sms?
number=17012345678&
content=helloworld&
appId=appNameA&
timestamp=1502610966&
noise=xWk2&
signature=c2b7e467a7bd14bf2ef768702be1c7f6f95a2d09
再加上S上做的限制signature重復(fù)使用,可以保證signature泄露的時(shí)候不會(huì)造成非法調(diào)用轮锥。
參數(shù)防篡改
還有一種更糟的情況矫钓,就是A、B舍杜、C發(fā)往S的請(qǐng)求被劫持新娜,劫持者修改了手機(jī)號(hào)碼和短信內(nèi)容,再發(fā)往S既绩。這樣概龄,signature是不會(huì)重復(fù)使用的,仍然能夠通過校驗(yàn)饲握。
所以更好的辦法是私杜,把業(yè)務(wù)參數(shù)即number和content的值也加入到signature計(jì)算中。這里需要注意的是救欧,為了更通用以及確保字符串連接的順序一致衰粹,須按照參數(shù)名對(duì)除signature以外的所有參數(shù)(包括token)進(jìn)行一個(gè)排序,然后將其值連接颜矿。
拿例子來說寄猩,排序好是這樣:
http://service.domain.com/sms?
appId=appNameA&
content=helloworld&
noise=xWk2
number=17012345678&
timestamp=1502610966&
(token=0UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ)
然后將appNamehelloworldxWk21701234567815026109660UW2m6Cpu9JdrM4muXHVBTOQMb4MG9nJ
這樣一個(gè)字符串做SHA1計(jì)算,得到最終的請(qǐng)求:
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來驗(yàn)證即可泊柬。輕松加愉快椎镣。
附
上述內(nèi)容中一些計(jì)算方法:(NodeJS)
//計(jì)算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('');
}
//計(jì)算簽名
const crypto = require('crypto');
function sha1(input){
return crypto.createHash('sha1').update(input).digest('hex')
}
本文同步自本人博客:https://phxsun.com/post/authentication-between-servers