聊聊微服務架構(gòu)中的認證鑒權(quán)那些事
點擊上方“服務端思維”,選擇“設為星標”
回復”669“獲取獨家整理的精選資料集
回復”加群“加入全國服務端高端社群「后端圈」
上半年參與的項目涉及到 gateway 和 id 權(quán)限認證系統(tǒng),通過系統(tǒng)性的學習與接觸,了解很多 HTTP 鑒權(quán)的那些事。分享實踐的細節(jié),都是通用做法,符合標準協(xié)議,不涉及公司機密
本文主要講如何給第三方服務,即 API 做鑒權(quán),而不是用戶登錄系統(tǒng)。一般做后端微服務的很少接觸這方面的概念,網(wǎng)關(guān)層或是入口做好認證后,鏈路下游都是默認開放的,最多用 iptables 或 aws securit group 類似的做網(wǎng)絡可達的限制。用戶信息傳到 Context 中即可
如果業(yè)務需要與第三方公司 partner 合作,或是開放 API 給外部,那么需要全面了解 HTTP 鑒權(quán)的知識。本文參考了鳳凰架構(gòu)[1] 和 HTTP API 認證授權(quán)術(shù)[2]
基本概念
鑒權(quán)的本質(zhì):用戶 (user / service) 是否有以及如何獲得權(quán)限 (Authority) 去操作 (Operate) 哪些資源 (Resource)
認證(Authentication):系統(tǒng)如何正確分辨出操作用戶的真實身份?認證方式有很多種,后面會講解
授權(quán)(Authorization):系統(tǒng)如何控制一個用戶該看到哪些數(shù)據(jù)、能操作哪些功能?授權(quán)與認證是硬幣的兩面
憑證(Credential):系統(tǒng)如何保證它與用戶之間的承諾是雙方當時真實意圖的體現(xiàn),是準確、完整且不可抵賴的?我們一般把憑證代稱為 TOKEN
保密(Confidentiality):系統(tǒng)如何保證敏感數(shù)據(jù)無法被包括系統(tǒng)管理員在內(nèi)的內(nèi)外部人員所竊取、濫用?這里要求我們不能存儲明文到 DB 中,不能將密碼寫到 http url 中,同時要求 id 服務僅有少部分人能夠訪問,并且有審計
傳輸(Transport Security):系統(tǒng)如何保證通過網(wǎng)絡傳輸?shù)男畔o法被第三方竊聽、篡改和冒充?內(nèi)網(wǎng)無所謂了,外網(wǎng)一般我們都用 https
驗證(Verification):系統(tǒng)如何確保提交到每項服務中的數(shù)據(jù)是合乎規(guī)則的,不會對系統(tǒng)穩(wěn)定性、數(shù)據(jù)一致性、正確性產(chǎn)生風險?
驗證方式
根據(jù)協(xié)議要求,需要將憑證 Credential 放到 header Authorization 里,憑證可是是用戶名密碼,也可以是自定義生成的 TOKEN
主流驗證方式有三大類:Basic/Digest, HMAC 和 Oauth2
1.Basic/Digest
Digest 翻譯成摘要,是 Basic 的加強版,放在一起討論,完整定義參考 RFC2617 Basic and Digest Access Authentication[3]

Basic 認證非常簡單,Server 發(fā)現(xiàn)沒有登錄返回 401 Unauthorized 并且攜帶 header WWW-Authenticate: Basic realm="User Visible Realm"
Client 用戶需要將 user:password 用戶名秘密用分號 : 組合在一起,然后求 Base64 編碼,放到 header 中發(fā)送給服務端
比如 user 是 Aladdin, 密碼是 open sesame, 此時將 (Aladdin:open sesame) 求 Base64 得到 header Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
但 Base64 只是編碼方式而己,和明文一樣沒有區(qū)別。所以引出了摘要算法, Digest 認證把用戶名和密碼加鹽(一個被稱為 Nonce 的變化值作為鹽值)后再通過 MD5/SHA 等哈希算法取摘要發(fā)送出去
但是這種認證方式依然是不安全的,無論客戶端使用何種加密算法加密,無論是否采用了 Nonce 這樣的動態(tài)鹽值去抵御重放和冒認,遇到中間人攻擊時依然存在顯著的安全風險
公司內(nèi)網(wǎng)默認可信的,可以用 Basic/Digest 隨變搞搞,但也就局限于此了
2.HMAC
HMAC hash-based message authentication code[4] 是市面上使用最廣泛的認證技術(shù),源自于 message authentication code[5], 主要用于消息簽名,防止被第三方修改

使用 HMAC 需求提前生成 app key 與 access secret key 給第三方,國內(nèi)習慣稱之為 AK/SK, 我司叫做 partner-id/secret, 叫什么不重要
AK/SK 是需要由 server 提前生成給第三方客戶端. Client 獲取待簽名的消息,使用 SK 加密后,將簽名一起發(fā)送到服務端。Server 用同樣的簽名算法 + SK 加密消息,如果一致說明訪問有效
?stringToSign?:=?verb?+
??newLineBreak?+
??contentType?+
??newLineBreak?+
??date?+
??newLineBreak?+
??path?+
??newLineBreak?+
??contentDigest?+
??newLineBreak?+
??//optional,?if?no?customised?header,?this?part?will?be?left?blank.
??partnerTokenComponent?+
??//optional,?if?no?customised?header,?this?part?will?be?left?blank.
??customisedHeaderComponent
如何構(gòu)建消息呢?業(yè)界沒有統(tǒng)一標準,但一般都是將 method, path, date, content-type, body 柔和在一起, 其中 date 起到防止重放攻擊的作用。有時我們可以不把 body 放到消息中,有時還要把 http params 排序放到消息體中
func?ComputeBase64EncodedHMACSHA256Signature(message?string,?secret?string)?(string,?error)?{
?hash?:=?hmac.New(sha256.New,?[]byte(secret))
?if?_,?err?:=?hash.Write([]byte(message));?err?!=?nil?{
??return?"",?err
?}
?val?:=?base64.StdEncoding.EncodeToring(hash.Sum(nil))
?return?val,?nil
}
至于簽名算法,我們一般用 SHA256,同時需要傳入 SK 密鑰
感興趣的可以參考 aws s3[6] 的玩法,原理是一樣的
我司正在廢棄遺留的 HMAC 認證方式,改用統(tǒng)一,流程更規(guī)范的 Two-Legged Oauth2 Client 認證模式
3.Oauth2
Oauth2[7] 是業(yè)界標準的授權(quán)協(xié)議,專注于客戶端開發(fā)人員的簡單性,同時為網(wǎng)絡應用、桌面應用、手機和客廳設備提供特定的授權(quán)流程。

主要有以下四種模式:
授權(quán)碼模式(Authorization Code) 隱式授權(quán)模式(Implicit) 密碼模式(Resource Owner Password Credentials) 客戶端模式(Client Credentials)
其中最常用的是 Authorization Code 與 Client Credentials 模式,Oauth2 涉及三個角色 owner 所有者(一般指人), server 服務端(一般指資源服務器和資源認證服務器), client 第三方客戶端,所以第一種的 Authorization Code 稱之為 Three-Legged 三腿模式
而 Client Credentials 其實更像是給服務端做認證,只有 client, server 所以稱之為 Two-Legged 兩腿模式,我司就使用這種做 API 鑒權(quán)

客戶端模式非常簡單,server 提前創(chuàng)建好 ClientID&ClientSecret 給第三方服務,然后第三方通過 ClientID&ClientSecret 調(diào)用 /oauth2/token 接口生成 token, 后續(xù)所有訪問攜帶這個 token 即可,每次由 id 服務調(diào)用 /oauth2/verify 去驗證
舉個測試的例子:
curl?-X?POST?https://xxxxxxxx/oauth2/token?-H?'Content-Type:?application/x-www-form-urlencoded'?-d?'client_id=c45594c80f3342ed93eea95ad4ab18bc&grant_type=client_credentials&client_secret=5aL1LOhXHi6X7E2r&scope=test.scope'
{"access_token":"this?is?jwt?token","token_type":"Bearer","expires_in":107999}
curl?-i?https://xxxxxxxx/v1/styles/dark.json?-H?'Authorization:?Bearer?this-is-a-jwt-token'
指定 grant_type 為 client_credentials 同時需要填寫 scope, 非常重要,用于控制該 user 能訪問哪些資源

和 Two-Legged 一樣,開始進行授權(quán)過程以前,第三方應用先要到授權(quán)服務器上進行注冊,所謂注冊,是指向認證服務器提供一個域名地址(用于回調(diào)),然后從授權(quán)服務器中獲取 ClientID 和 ClientSecret
這里有幾個概念:
資源所有者 owner: 一般是指我們用戶 操作代理 delelegate: 一般指瀏覽器 chrome edge ... 資源服務器:比如微信,比如新浪等等 授權(quán)服務器:用于鑒權(quán)的,有時和資源服務器是一臺 第三方應用:比如一些小程序,想訪問我的微信頭像等等
授權(quán)過程如下:
第三方應用將資源所有者(用戶)導向授權(quán)服務器的授權(quán)頁面,并向授權(quán)服務器提供 ClientID 及用戶同意授權(quán)后的回調(diào) URI,這是一次客戶端頁面轉(zhuǎn)向
授權(quán)服務器根據(jù) ClientID 確認第三方應用的身份,用戶在授權(quán)服務器中決定是否同意向該身份的應用進行授權(quán),用戶認證的過程未定義在此步驟中,在此之前應該已經(jīng)完成
如果用戶同意授權(quán),授權(quán)服務器將轉(zhuǎn)向第三方應用在第 1 步調(diào)用中提供的回調(diào) URI,并附帶上一個授權(quán)碼和獲取令牌的地址作為參數(shù),這是第二次客戶端頁面轉(zhuǎn)向
第三方應用通過回調(diào)地址收到授權(quán)碼,然后將授權(quán)碼與自己的 ClientSecret 一起作為參數(shù),通過服務器向授權(quán)服務器提供的獲取令牌的服務地址發(fā)起請求,換取令牌。該服務器的地址應與注冊時提供的域名處于同一個域中
授權(quán)服務器核對授權(quán)碼和 ClientSecret,確認無誤后,向第三方應用授予令牌。令牌可以是一個或者兩個,其中必定要有的是訪問令牌(Access Token),可選的是刷新令牌(Refresh Token)。訪問令牌用于到資源服務器獲取資源,有效期較短,刷新令牌用于在訪問令牌失效后重新獲取,有效期較長
資源服務器根據(jù)訪問令牌所允許的權(quán)限,向第三方應用提供資源。
理解 Oauth2 時一定要明確,哪些是通過瀏覽器訪問的,哪些是第三方服務器直接與授權(quán)服務器交互,還要注入兩次頁面轉(zhuǎn)向。建議看一下 github oauth2 或者微信的開發(fā)文檔
JSON Web Tokens
上面是三種主流的驗證方式,其實 Oauth2 只規(guī)定了大致框架,并沒有規(guī)定 token 如何生成。我們一般使用 JWT[8] 開放的標準(RFC 7519), 它定義了一種緊湊和獨立的方式,以 JSON 對象的形式在各方之間安全地傳輸信息。這種信息可以被驗證和信任,因為它是經(jīng)過數(shù)字簽名的,一般我們使用 RSA private key 進行簽名。
JWT 格式由三段組成,header.payload.signature, 讓我們看個例子

header 頭表示用什么算法進行簽名,payload 是一堆 json 可以自定義,但一些公用的己經(jīng)定義好,signature 是簽名
HMACSHA256(
??base64UrlEncode(header)?+?"."?+
??base64UrlEncode(payload),
??your-256-bit-secret
)
大致算法也比較簡單,your-256-bit-secret 是你的密鑰,相同算法生成的 signature 一致,說明 JWT 沒有被篡改。一般都用 RSA private 去加密,然后用 public key 解密,好處是可以把 public key 放到 cdn lambda 去驗證請求是否有效。如果用對稱算法,就做不到這樣的效果
{
??"alg":?"RS256",
??"kid":?"_default",
??"typ":?"JWT"
}
我司對 header/payload 都進行了定制,比如 Header 增加 kid 字段,代表是用哪個 RSA?key pair 進行的加密,可以用于 rotate RSA 密鑰,非常方便
{
??"aud":?"c45594cXXXXXXXXXadb18bc",
??"exp":?1635263037,
??"iat":?1635155037,
??"iss":?"https://idp.grab.com",
??"jti":?"oD-VHixWQXXXXXXXXXXXAR6ctYg",
??"nbf":?1635154857,
??"pid":?"e0923XXXXXXXXXX943690d4b",
??"pst":?1,
??"scp":?"[\"7c14974d3d0e462bXXXXXXXXXXXXXXXbf7bc6\"]",
??"sub":?"TWO_LEGGED_OAUTH",
??"svc":?"",
??"tk_type":?"access"
}
payload 里面 iss 指頒發(fā)者信息,exp 是指 JWT 過期時間,如果允許篡改,那就可以無限續(xù)約了,同時增加了 partner-id, scope, two-legged 等等輸助信息。其實 TOKEN 用什么實現(xiàn)無所謂,能防止篡改的都可以,?token 搬發(fā)出去就很難控制,為了防止重放攻擊,一般要對 token 做控制
穩(wěn)定性
以前滴滴出過很多 ID 鑒權(quán)服務故障,原因五花八門,現(xiàn)在回頭看,都是穩(wěn)定性建設不到位
uuid 生成,通過 shell exec 做系統(tǒng)調(diào)用,網(wǎng)絡抖動時,司機頻繁登入登出,系統(tǒng)負載增加,直接打掛服務
token 相關(guān)的接口沒有 ratelimter, 直接打掛 DB
其它的想不起來了,那一系列文章在 way 社區(qū)還蠻火的。只能說 stability 永遠的難題,尤其這種基礎(chǔ)服務
雜談
RSA public private 在 DB 不能存儲明文,要用 vault 或是 kms 加密
Base64 是為了傳輸方便,省去空格特殊字符等等,但仍然是明文。hash 才是為了加密
RSA 公鑰加密是不想讓別人看到內(nèi)容,因為只有私鑰才能解開。私鑰加密是為了傳遞數(shù)據(jù),不想讓別人篡改
JWT TOKEN 能防篡改但是不能防重放攻擊,所以 exp 要短,同時要有 token 黑名單,還得有限流,哪怕是一小時也能把服務打爆
TOKEN 是否存儲 DB 呢?存有好處,也可以選擇不存
RSA 加密 Encrypt 和摘要 Digest 的區(qū)別,前者可逆,后者不可逆
JWT payload 自定義內(nèi)容不易過多,一般 http header 都是有大小限制的
三個概念:編碼 Base64Encode、簽名 HMAC、加密 RSA。編碼是為了更的傳輸,等同于明文,簽名是為了信息不能被篡改,加密是為了不讓別人看到是什么信息
本文不涉及 TLS, 歷史上很多鑒權(quán)的方案都是為了應對沒有 TLS 的情況,外網(wǎng)基本都是 https, 所以很多方案現(xiàn)在己經(jīng)不適合
Scope 非常重要,基于 least privilege 原則,只允許最小訪問權(quán)限,一定要控制第三方能訪問的資源
運維能力,ID 服務一般訪問受限,只有特定服務或是 admin header 才能訪問,需要提供 bot 或是網(wǎng)頁運維能力
密鑰需要定期 rotate, 業(yè)務代碼當然也要適配
小結(jié)
非安全及 web authentication 專業(yè),如果有描述錯誤的請大家指出~?
寫文章不容易,如果對大家有所幫助和啟發(fā),請大家?guī)兔c擊在看,點贊,分享 三連
關(guān)于 HTTP 鑒權(quán) 大家有什么看法,歡迎留言一起討論,大牛多留言 ^_^
— 本文結(jié)束 —

關(guān)注我,回復 「加群」 加入各種主題討論群。
對「服務端思維」有期待,請在文末點個在看
喜歡這篇文章,歡迎轉(zhuǎn)發(fā)、分享朋友圈


