如何為雲服務設計鑑權機制?
什麼是鑑權?為什麼要鑑權?
現在市面上絕大部分 SaaS (Software as a Service)模式的雲服務都是這樣的模式:雲服務廠商對外提供一套介面,開發者在自己的客戶端或者伺服器端程式上直接呼叫介面,通常情況下是按呼叫量計費。
為了能夠精確地計費,雲服務需要識別呼叫者是誰,這就需要雲服務的鑑權機制。
一個簡單的例子
假設我們現在有一套神奇的雲服務,它是以http介面的形式對外開放的,它的功能非常簡單:呼叫者通過 http 請求介面,然後它返回一個數字。
await request('https://foo.com/') //=> 42
下面我們就以這個簡單的雲服務為例,看看如何建立一套完善的鑑權機制。
身份標識
既然要知道呼叫者是誰,最簡單的,就是多加入一個引數 caller
,這個引數代表了使用者的身份,也就是使用者名稱、uin、郵箱這樣的全域性唯一的使用者標識,我們通過識別這個標識,來確定呼叫者的身份,從而確定要向哪個開發者收錢(嗯重點)。
這時介面類似這樣:
await request('https://foo.com/', { caller: 'tom' })

這樣就存在一個明顯的問題,任何人都可以隨意偽造他人的身份,從而可以讓別人為自己的呼叫次數買單,只要隨意修改 caller
這個引數就好了。
這就是一個非常嚴重的安全隱患,沒有任何雲服務廠商會幹這麼傻的事情。
下發 Token
為了解決 caller
引數無法完備地驗證使用者身份的問題,我們可以改用一個新的引數 token
,它是雲服務事先下發給開發者的唯一身份令牌, 雲服務拿到這個 Token 之後,會驗證 Token 是否有效,從而識別開發者的身份(然後收錢)。
這個時候介面類似這樣:
await request('https://foo.com/', { token: 'AKID3...' })

這樣就有了最基礎的鑑權,但它依然不完美: 只要 Token 被暴露給了壞人,那麼他就可以直接偽造別人的身份 。
對 Token 動態加密
為了讓 Token 在不至於暴露得那麼明顯,我們可以對 Token 進行加密,比如 sha256 就是一種常見的加密演算法,我們可以用時間戳當做 key 對 Token 進行加密:
await request('https://foo.com/', { credential: '24c6c3f...', timestamp: 1548301642 })
然後雲服務用時間戳從 credential 欄位中解密出 Token ,從而確認呼叫方身份。
這樣我們就避免了黑客直接擷取請求獲得明文的 Token,如果黑客不知道加密解密的演算法流程,那麼就無法通過擷取 credential
欄位來破解 Token。更加複雜的加密演算法可以進一步提高黑客的破解成本。
呼叫者的身份
使用 Token 這種簡單的機制是不是就安全了呢?當然不是。
SaaS 服務的呼叫方通常來說會有以下兩種:
- 客戶端 :開發者的使用者端程式,比如 App、Web 網頁等等,它直接在 C 端使用者的裝置中執行
- 伺服器端 :開發者的服務端程式,通常在開發者自己的伺服器中執行
這兩種呼叫方,因為執行的環境不同,安全程度也完全不一樣:
- 如果開發者是使用自己的伺服器來請求介面的,Token 是儲存在伺服器上,那麼暴露的風險比較低,這種做法還是可以接受的(部分雲服務介面就是這麼實現的)。如下圖:

- 如果是直接在使用者端(C 端使用者)進行呼叫,那麼這個 Token 就會被儲存在使用者端,暴露的風險大大提高。對於 web 平臺的應用來說,更是這樣,因為 js 檔案是直接明文暴露出去的。而其它原生平臺的應用,也可以通過反編譯來獲取這個 Token。

換句話說, 永久且靜態的 Token 類似銀行卡密碼,是不能放在客戶端裡面的 。對於客戶端程式,我們必須通過別的機制來保證身份認證的安全。
動態 Token
為了對客戶端進行安全的鑑權,我們可以使用動態化的 Token,即通過一個鑑權伺服器拿到動態的 Token,再呼叫介面(比如微信的 open API 就是這樣做的)。
- 客戶端請求獲取一個臨時 Token;
- 開發者的伺服器向雲服務端請求一個臨時 Token;
- 把臨時 Token 轉發給客戶端;
- 客戶端呼叫雲服務時,帶上這個臨時 Token,從而鑑權。

這樣做有幾個好處:
- 不存在永久的 Token,即使黑客破解客戶端,獲得了這個臨時 Token,造成的損害也是有限的。
- 臨時 Token 是通過開發者自己的伺服器進行轉發的,所以開發者可以在這個過程中可以甄別客戶端的合法性。
絕大部分的雲服務,做到這裡已經足夠了。
簡化
我們不妨重新審視一下上圖的架構,畫出一條開發者和雲服務的分界線:

這張圖裡只有三條流程(②、③和⑤)跨越了開發者和雲服務的界限,所以我們可以簡化成下圖這樣:

獨立的鑑權服務
對於雲服務廠商來說,不可能為每個 SaaS 服務都去獨立開發一套鑑權體系。對於維護 SaaS 服務的團隊來說,也很難有充足的人力去自己定製一套鑑權體系。所以必然會使用第三方提供的服務(這裡的“第三方”只是相對於 SaaS 團隊而言,不見得是外部服務)。這時,我們可以把鑑權服務抽出來變成一個獨立模組:

把鑑權服務 Serverless 化
傳統的鑑權服務,通常會和資料庫結合使用,下發一個 Token 後,在資料庫中記錄這個 Token 的相關資訊。等需要鑑定時,再去資料庫中尋找這個 Token 是否存在且合法。
但我們不妨重新思考一下鑑權服務的意義,總體上講,鑑權服務對外提供的介面只需要兩個:
- 對外下發 Token
- 對內鑑定 Token 是否合法(可能還需要把 Token 轉化為具體的身份資訊)
這兩個介面實際上非常函式化,我們完全可以把鑑權服務抽象為兩個無伺服器函式(類似 AWS 的 Lamda),也是新潮的 Serverless 架構。
Serverless 架構的前提,就是要 保證服務的無狀態 。鑑權服務配合 JWT(JSON Web Token),實際上是可以做到 無狀態 的,在獲取 Token 時,我們可以把一些鑑權的關鍵資訊加密後變成 Token 下發,然後在鑑定時,使用私鑰進行解密即可。下面是一個簡單的例子:
const jwt = require('jsonwebtoken') // 自定一個 secret const secret = 'This is a secret' // 獲取 Token 的介面 exports.getToken = function getToken(info) { // 使用 jwt 直接生成一個 token const token = jwt.sign( info, secret, { expiresIn: 7 * 24 * 60 * 60 }// 有效期 7 天 ) return token } // 鑑定 Token 的介面 exports.verify = function(token) { // 使用 secret 解密出鑑權資訊 const info = jwt.verify(token, secret) return info }

把鑑權服務 Serverless 化有幾個好處:
- 鑑權服務在實際業務中的呼叫量通常比較大,傳統的架構裡,需要大規模的叢集才能滿足需求,把它變成 Serverless 函式,可以減少大量的部署和運維成本;
- Serverless 函式會有統一的呼叫方式和介面,我們不需要重新設計鑑權介面的 RPC 協議。
一個實際的例子
在我們年後即將上線的小程式 WebSocket 雲服務中,我們就使用了 Serverless 函式來為我們完成使用者側鑑權的功能。
首先小程式端呼叫一個雲函式,下發 Token,然後帶上這個 Token 對 WebSocket 伺服器請求連線,WS 服務收到後,呼叫雲函式校驗 Token 是否合法,如果合法則允許小程式端連線。

這裡有一個很有趣的細節是, 鑑權使用的雲函式是暴露給開發者的,開發者可以自行修改 ,這樣就可以通過修改這個鑑權雲函式的邏輯,來控制小程式使用者是否有許可權連線 WebSocket。
廣告區域
我們是騰訊雲 TCB 團隊,團隊專注於 Serverless 領域的雲服務,目前主要的產品有:
我們是騰訊內部為數不多的以 Node.js、Golang 技術為核心的開發團隊,團隊目前正處於快速發展期,業務高速擴張,如果你滿足下面的條件,歡迎傳送簡歷到 [email protected] (工作地點在深圳):
- 本科或以上學歷,三年以上工作經驗;
- 對 Golang / Node.js 伺服器端開發有豐富的經驗;
- 對開發大規模、高可用服務有一定經驗(加分項)。
另外我們還招 19 年的畢業生 以及 20 年畢業的(暑期)實習生 ,也歡迎投遞簡歷~