1 為什么寫?
個人最近看了周志明的《鳳凰架構(gòu)》中架構(gòu)安全性部分,書中對于架構(gòu)安全性做了非常體系的講解,開拓了自己的視野,希望通過本文能夠?qū)ζ渲械年P(guān)鍵點做下實戰(zhàn)和總結(jié)。 如今,大家也比較關(guān)心個人的隱私問題,學(xué)好安全性相關(guān)的內(nèi)容,能夠?qū)τ诎踩韵嚓P(guān)的設(shè)計提供一些理論指導(dǎo),能夠明白安全性做得好的公司為什么要這么設(shè)計,也能指導(dǎo)自己在公司設(shè)計出更加安全的架構(gòu),而安全相關(guān)問題基本上是與具體業(yè)務(wù)無關(guān),會有通用的標準,掌握之后可以應(yīng)用在任何業(yè)務(wù)場合。
2 你能收獲什么內(nèi)容?
- 架構(gòu)安全性包含的部分有哪幾類,以及每一類的側(cè)重點是什么?大家也可以通過周志明的文章進一步了解細節(jié)。
- 對于每一個類中的安全性比較主流/傾向的做法是什么?這些做法的原理是什么?并且能夠通過一些示例分析加深理解。
3 架構(gòu)安全性包含的內(nèi)容及本文講解的關(guān)鍵技術(shù)點
架構(gòu)安全性包含比較多的內(nèi)容,其中至少包含了以下六個部分。 以下定義擴充自周志明書中的定義:
認證(Authentication):系統(tǒng)如何正確分辨出操作用戶的真實身份?(需要解決你是誰的問題)
授權(quán)( Authorization):系統(tǒng)如何控制一個用戶該看到哪些數(shù)據(jù)、能操作哪些功能?(需要解決你能干什么的問題)
憑證(Credential):系統(tǒng)如何保證它與用戶之間的承諾是雙方當時真實意圖的體現(xiàn),是準確、完整且不可抵賴的?
保密(Confidentiality):系統(tǒng)如何保證敏感數(shù)據(jù)無法被包括系統(tǒng)管理員在內(nèi)的內(nèi)外部人員所竊取、濫用?
傳輸(Transport Security):系統(tǒng)如何保證通過網(wǎng)絡(luò)傳輸?shù)男畔o法被第三方竊聽、篡改和冒充?
驗證(Verification):系統(tǒng)如何確保提交到每項服務(wù)中的數(shù)據(jù)是合乎規(guī)則的,不會對系統(tǒng)穩(wěn)定性、數(shù)據(jù)一致性、正確性產(chǎn)生風(fēng)險?
3.1 認證
以HTTP協(xié)議為基礎(chǔ)的認證框架,一般是依靠內(nèi)容而不是傳輸協(xié)議來實現(xiàn)的認證方式,由于實現(xiàn)形式上一般是用了登錄表單的方式,因此通常也被稱為“表單認證”。 在2019年之前,表單認證沒有什么行業(yè)標準可循,表單什么樣子,其中的用戶字段、密碼字段、驗證碼字段是否要在客戶端加密、采用何種方式加密,接受表單的服務(wù)地址是什么?都完全由客戶端和服務(wù)端的開發(fā)者自行協(xié)商決定。 直到2019年3月,萬維網(wǎng)聯(lián)盟批準了一個世界首份Web內(nèi)容標準“WebAuthn”,WebAuthn徹底拋棄了傳統(tǒng)的密碼登錄方式,改為直接采用生物識別(指紋、人臉等)或者實體密鑰來作為身份憑證,體驗和安全性上較既有的表單認證方式有較好的提升。 一些APP客戶端已經(jīng)有啟用了這些認證方式,chrome的最新版本也提供了webAuthn的支持,但是到目前為止,已經(jīng)支持webAuthn登錄的網(wǎng)站還比較少,以下是一個網(wǎng)友實現(xiàn)的一個Demo,登錄的效果如下:
實現(xiàn)的代碼可以從網(wǎng)友的原始博文中獲取。
使用webAuthn注冊和驗證過程原理比較類似,下圖展示了注冊流程:
圖里的注冊流程摘自網(wǎng)站,解釋如下(暫時看不懂沒有關(guān)系,你只要知道它作為認證非常安全,體驗也很好):
0.應(yīng)用程序請求注冊 - 應(yīng)用程序發(fā)出注冊請求。這個請求的協(xié)議和格式不在 WebAuthn 標準的范圍內(nèi)。
1.服務(wù)器發(fā)送挑戰(zhàn)、用戶信息和依賴方信息 - 服務(wù)器將挑戰(zhàn)、用戶信息和依賴方信息發(fā)送回應(yīng)用程序。在這里,協(xié)議和格式不在 WebAuthn 標準的范圍內(nèi)。通常,這可以是基于 HTTPS 連接的 REST(可能會使用 XMLHttpRequest 或 Fetch)API。不過只要在安全連接中,也可以使用 SOAP、RFC 2549 或幾乎任何其他協(xié)議。從服務(wù)器接收到的參數(shù)將傳遞給 create() ,大部分情況下只需很少修改甚至不需要做任何修改。create() 會返回一個Promise,并返回包含 AuthenticatorAttestationResponse (en-US) 的 PublicKeyCredential (en-US)。需要注意的是挑戰(zhàn)必須是隨機的 buffer(至少 16 字節(jié)),并且必須在服務(wù)器上生成以確保安全。
2.瀏覽器向認證器調(diào)用 authenticatorMakeCredential() - 在瀏覽器內(nèi)部,瀏覽器將驗證參數(shù)并用默認值補全缺少的參數(shù),然后這些參數(shù)會變?yōu)?AuthenticatorResponse.clientDataJSON。其中最重要的參數(shù)之一是 origin,它是 clientData 的一部分,同時服務(wù)器將能在稍后驗證它。調(diào)用 create() 的參數(shù)與clientDataJSON 的 SHA-256 哈希一起傳遞到身份驗證器(只有哈希被發(fā)送是因為與認證器的連接可能是低帶寬的 NFC 或藍牙連接,之后認證器只需對哈希簽名以確保它不會被篡改)。
3.認證器創(chuàng)建新的密鑰對和證明 - 在進行下一步之前,認證器通常會以某種形式要求用戶確認,如輸入 PIN,使用指紋,進行虹膜掃描等,以證明用戶在場并同意注冊。之后,認證器將創(chuàng)建一個新的非對稱密鑰對,并安全地存儲私鑰以供將來驗證使用。公鑰則將成為證明的一部分,被在制作過程中燒錄于認證器內(nèi)的私鑰進行簽名。這個私鑰會具有可以被驗證的證書鏈。
4.認證器將數(shù)據(jù)返回瀏覽器 - 新的公鑰、全局唯一的憑證 ID 和其他的證明數(shù)據(jù)會被返回到瀏覽器,成為 attestationObject。
5.瀏覽器生成最終的數(shù)據(jù),應(yīng)用程序?qū)㈨憫?yīng)發(fā)送到服務(wù)器 - create() 的 Promise 會返回一個 PublicKeyCredential (en-US),其中包含全局唯一的證書 ID PublicKeyCredential.rawId (en-US) 和包含 AuthenticatorResponse.clientDataJSON 的響應(yīng) AuthenticatorAttestationResponse (en-US)。你可以使用任何你喜歡的格式和協(xié)議將 PublicKeyCredential (en-US) 發(fā)送回服務(wù)器(注意 ArrayBuffer 類型的屬性需要使用 base64 或類似編碼方式進行編碼)
6.服務(wù)器驗證數(shù)據(jù)并完成注冊 - 最后,服務(wù)器需要執(zhí)行一系列檢查以確保注冊完成且數(shù)據(jù)未被篡改。步驟包括:
- 驗證接收到的挑戰(zhàn)與發(fā)送的挑戰(zhàn)相同
- 確保 origin 與預(yù)期的一致
- 使用對應(yīng)認證器型號的證書鏈驗證 clientDataHash 的簽名和證明
驗證步驟的完整列表可以在 WebAuthn 規(guī)范中找到。一旦驗證成功,服務(wù)器將會把新的公鑰與用戶帳戶相關(guān)聯(lián)以供將來用戶希望使用公鑰進行身份驗證時使用。
WebAuthn 采用非對稱加密的公鑰、私鑰替代傳統(tǒng)的密碼,這是非常理想的認證方案,私鑰是保密的,只有驗證器需要知道它,連用戶本人都不需要知道,也就沒有人為泄漏的可能。也解決了傳統(tǒng)密碼在網(wǎng)絡(luò)傳輸上的風(fēng)險。 當前的 WebAuthn 還很年輕,普及率暫時還很有限,但書中作者相信幾年之內(nèi)它必定會發(fā)展成 Web 認證的主流方式,被大多數(shù)網(wǎng)站和系統(tǒng)所支持。
3.2 授權(quán)
授權(quán)實質(zhì)上是在解決:“誰(User)能夠操作(Operation)哪些資源(Resource)”。日常開發(fā)中比較多見的是RBAC和OAuth2兩種訪問控制和授權(quán)方案。 RBAC通過引入角色概念,賦予角色操作資源的權(quán)限,并且給用戶賦予權(quán)限能夠讓擁有這些角色的用戶操作對應(yīng)的資源。基本上公司的權(quán)限控制模塊都是基于RBAC實現(xiàn)的。
而OAuth2 是面向于解決第三方應(yīng)用的認證授權(quán)協(xié)議。允許用戶讓第三方應(yīng)用訪問該用戶在某一網(wǎng)站上存儲的私密的資源(如照片,視頻,聯(lián)系人列表),而無需將用戶名和密碼提供給第三方應(yīng)用。 比如很多網(wǎng)站都有通過微信掃一掃登錄的功能,也是使用了OAuth2的認證授權(quán)過程。
微信掃一掃登錄使用了OAuth2中授權(quán)碼模式(其他的更多模式可以參考《鳳凰架構(gòu)》),OAuth2的授權(quán)碼方式的訪問流程如下圖所示:
在微信掃一掃登錄場景中,圖中的資源所有者就是用戶,操作代理是瀏覽器,第三方應(yīng)用是希望使用微信掃一掃登錄的第三方網(wǎng)站,授權(quán)服務(wù)器是微信服務(wù)端。
比如簡書使用微信掃一掃登錄的過程如下:
-
選擇微信掃一掃登錄,打開該服務(wù)器的微信的二維碼認證頁面,該頁面帶有原始網(wǎng)頁的回調(diào)地址:
https://open.weixin.qq.com/connect/qrconnect?appid=wxe9199d568fe57fdd&client_id=wxe9199d568fe57fdd&redirect_uri=https%3A%2F%2Fwww.lxweimin.com%2Fusers%2Fauth%2Fwechat%2Fcallback&response_type=code&scope=snsapi_login&state=%257B%257D#wechat_redirect
-
用戶掃描微信,認證成功后返回code,如下圖所示:
o_220212082923_%E7%AE%80%E4%B9%A6callback.png 第三方應(yīng)用服務(wù)器使用code,appsecret獲取accesstoken 并且拿到微信用戶的基本信息,標記用戶已經(jīng)登錄。
重定向到第三方登錄成功后的頁面。
3.3 憑證
HTTP 協(xié)議是一種無狀態(tài)的傳輸協(xié)議,每一個請求都是完全獨立的,所以一般web開發(fā)都會采用cookie-session機制讓服務(wù)器有辦法能夠區(qū)分出發(fā)送請求的用戶是誰,在服務(wù)器中會維護了一些用戶的session信息,為了服務(wù)器的高可用,會把用戶的狀態(tài)信息存儲到Redis等集中式的存儲中。 除了cookie-session機制,還可以使用JWT(JSON Web Token)的方案,能夠讓信息不存儲在服務(wù)端,且也能防止信息在傳輸過程中被篡改。
JWT的原理:服務(wù)器認證以后,生成JSON 對象,發(fā)回給用戶。以后,用戶與服務(wù)端通信的時候,都要發(fā)回這個 JSON 對象。服務(wù)器完全只靠這個對象認定用戶身份。為了防止用戶篡改數(shù)據(jù),服務(wù)器在生成這個對象的時候,會加上簽名。 JWT:的數(shù)據(jù)結(jié)構(gòu)由三部分組成Header(頭部)、Payload(負載)、Signature(簽名)。寫成一行,就是下面的樣子:
Header.Payload.Signature
Header 部分是一個 JSON 對象,描述 JWT 的元數(shù)據(jù),通常是下面的樣子。
{
"alg": "HS256",
"typ": "JWT"
}
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數(shù)據(jù),可以定義業(yè)務(wù)需要的私有字段,JWT 規(guī)定了7個官方字段,供選用。
iss (issuer):簽發(fā)人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發(fā)時間
jti (JWT ID):編號
Signature 部分是對前兩部分的簽名,防止數(shù)據(jù)篡改。需要指定一個密鑰(secret)。這個密鑰只有服務(wù)器才知道,不能泄露給用戶。然后,使用 Header 里面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產(chǎn)生簽名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,其中header和payload部分需要使用Base64URL進行轉(zhuǎn)化。然后三個部分之間用"點"(.)分隔,就可以返回給用戶。
最后的效果如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwMTIxMiIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.RoeGlqmkqtGrgjmV0Z5EF8bwCLQdzRXwPiG1ZmiNVfU
可以通過網(wǎng)站https://jwt.io/, 查看和生成編碼后的數(shù)據(jù)。java有jjwt工具可以在項目中生成和解析JWT, github地址:https://github.com/jwtk/jjwt。
客戶端每次與服務(wù)器通信,都要帶上這個 JWT。你可以把它放在 Cookie 里面自動發(fā)送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭信息Authorization字段里面。
JWT的應(yīng)用目前感覺還是比較少,但是提供了一種服務(wù)端無狀態(tài)的思路,正常情況下使用cookie-session機制能夠滿足要求了。
3.4 保密
密碼如何從客戶端傳輸?shù)椒?wù)端,一般會在客戶端對用戶密碼進行簡單的哈希摘要。從根本上杜絕在服務(wù)器數(shù)據(jù)庫或者日志中存儲密碼明文, 大網(wǎng)站被拖庫的事情層出不窮,如果使用明文,就比較危險。 但是不存儲明文,如果只是做了簡單的哈希在服務(wù)端進行存儲,也比較容易通過彩虹表攻擊得到明文,為了應(yīng)對彩虹表攻擊應(yīng)加鹽處理。 下面會重點講下為什么普通的hash方式拿到密文后容易被破解。
比如windows xp的登錄密碼會通過一些hash算法保存在sam文件,通過彩虹表攻擊可以快速的獲取到windows xp的登錄密碼,如下是通過應(yīng)用了彩虹表的ophcrack工具破解后的截圖:
案例中包含了字母和數(shù)字的14長度的密碼在15分鐘內(nèi)被破解,如果更簡單的密碼,可以使用輕量級的彩虹表,基本上在秒級就可以被破解掉。
以14位字母和數(shù)字的組合密碼為例,如果使用窮舉法進行破解,共有1.24×1025((10+26+26)14)種可能,即使電腦每秒鐘能進行10億次運算,也需要4億年才能破解。 如果使用海量的磁盤進行hash串的存儲,然后通過查表法獲取明文,就算只有128位的哈希串的hash串存儲(先不考慮明文存儲)就是一個天文數(shù)字(2128/8 字節(jié))。
那么彩虹表的做法是在計算時間和存儲空間中做了下平衡。
彩虹表會由很多條鏈組成,針對每一條鏈,會有很多鏈的節(jié)點,這些鏈節(jié)點之間的關(guān)系如下:
【第一個節(jié)點(明文1)】->(H)-->【第二個節(jié)點(Hash值1)】-->(R1)--->【第三個節(jié)點(明文2)】-->(H)-->【第四個節(jié)點(Hash值2)】-->(R2)-->【第五個節(jié)點(明文3)】
示例:
12345->(H)->abwefsdfse->(R1)->32112->(H)->asdasgasdf->(R2)-->13423
- 其中H是要破解Hash串使用的Hash函數(shù)。
- R1~Rx是彩虹表構(gòu)建的能夠從Hash串到明文的函數(shù)。
而彩虹表存儲的時候只需要存儲這條鏈的第一個節(jié)點和最后一個節(jié)點,從而大大減少了存儲空間(鏈條越長,節(jié)點個數(shù)越多,節(jié)約空間越長)。
彩虹表破解過程如下:
假如一條鏈上有K個Hash值的節(jié)點
1. 假設(shè)要破解的密文位于某一鏈條的最后一個Hash值節(jié)點位置處,先對其進行Rk運算,看是否能夠在末節(jié)點中找到對應(yīng)的值。如果找到,生成這個Hash值的明文即為要破解的明文。
2. 如果找不到,繼續(xù)判斷要破解的密文位于倒數(shù)第二個Hash值節(jié)點位置處,那么對其做R(k-1),H,RK三步操作,然后再進行末節(jié)點的比對。
3. 以此類推,最終找到要破解的明文。
彩虹表自己構(gòu)建比較麻煩,可以直接從互聯(lián)網(wǎng)下載各種Hash方式和大小的彩虹表。 說了這個彩虹表的例子,是說我們需要在做保密的時候增強對于彩虹表破解的難度,可以在Hash的時候加上鹽值。
3.5 傳輸
傳輸層非常重要,如果傳輸層是明文傳遞,比如使用了HTTP傳輸,那么非常容易被第三方獲取到http內(nèi)容。 為了讓http傳輸不被第三方竊取,通過https是唯一的手段。那么https是怎么確保安全的呢? 簡單來說HTTPS使用了對稱加密和非對稱加密,先通過非對稱加密傳遞了對稱加密密鑰,然后后續(xù)通過對稱加密傳遞http內(nèi)容,非對稱加密中的私鑰由服務(wù)端保存,而公鑰可以公開給所有人,通過公鑰加密的內(nèi)容只能由服務(wù)端的私鑰解密獲得。
目前HTTPS一般采用的是TLS1.2協(xié)議,TSL的握手時序如下圖所示:
在第二步服務(wù)端返回Server Hello的同時,也會返回包含了非對稱加密公鑰的數(shù)字證書。 客戶端就使用該公鑰對對稱加密的密鑰進行加密再傳遞給服務(wù)端。
通過WireShark抓包也可以清楚看到整個握手過程,如下圖所示:
至此,還有一個問題,就是客戶端怎么確保傳遞給他的公鑰就是服務(wù)端希望它拿到的公鑰呢。這邊就需要使用到了CA機構(gòu)。CA機構(gòu)需要對包含了服務(wù)器公鑰的數(shù)字證書進行簽名。 然后客戶端會在操作系統(tǒng)中內(nèi)置了一些CA根證書,用來對數(shù)字證書的有效性進行驗證,從而確保客戶端拿到正確的公鑰。
出于好奇,我通過互聯(lián)網(wǎng)了解了下是不是存在CA機構(gòu)泄漏私鑰的情況,如果CA機構(gòu)的私鑰泄露了,那就是一件非常危險的事情,那么基于該CA機構(gòu)簽名的數(shù)字證書就會不可信了。 歷史上確實存在CA機構(gòu)泄漏私鑰的情況,比如荷蘭的CA安全證書提供商DigiNotar,服務(wù)器遭受到了黑客入侵,私鑰被竊取。攻擊者基于此私鑰共發(fā)行了 531 個偽造證書,然后微軟緊急發(fā)布了操作系統(tǒng)補丁, 將其列入不信任CA名單,而DigiNotar也因此宣告破產(chǎn)。
而我們平時如果希望在客戶端上抓HTTPS包進行分析,可以用兩類方式:
- 使用抓包工具簽發(fā)證書,并且在操作系統(tǒng)中標記簽發(fā)證書的機構(gòu)是受信的。比如charles就是用這個方式進行https的抓包。
- 獲取到https對稱加密的密鑰,并且使用該密鑰對獲取的加密內(nèi)容進行解密。比如wireshark使用chrome https傳輸?shù)膶ΨQ密鑰。
3.6 驗證
這部分更多的提到了業(yè)務(wù)驗證,平時我們實際寫業(yè)務(wù)代碼最多的也是這個部分,往往會在從客戶端到接入層到邏輯層的各層進行驗證。 代碼中進行業(yè)務(wù)邏輯驗證一般會存在的兩個問題:
- 代碼邏輯里充斥著大量的判空邏輯及其他校驗,影響代碼的簡潔
- 調(diào)用端和被調(diào)用端在哪層做校驗更加合理
書中作者提倡的做法是把校驗行為從分層中剝離出來,不是具體在哪一層做邏輯校驗,而是在 Bean 上做。即 Java Bean Validation。 而在Bean上做的可以比較容易地在各層做到重用。比如以下面的代碼為例,對于Account的驗證可以通過@UniqueAccount進行標注就可以。
public Response createUser(@Valid @UniqueAccount Account user) {
return CommonResponse.op(() -> service.createAccount(user));
}
更多例子和使用方式,可以通過周老師的鳳凰架構(gòu)查看。
4 總結(jié)
感謝周老師的書籍(文中有部分截圖也引用自周老師的書籍網(wǎng)站),讓我更加體系化地進行了安全部分的學(xué)習(xí),并且通過一些示例加深了對于該部分的理解,同時也希望能夠幫助大家理解。如果大家希望更完整地了解相關(guān)內(nèi)容,建議也完整地讀一下周老師的書籍。歡迎大家評論交流。