架構安全性設計、部分示例及原理分析

csophys發表於2022-02-12

 

1 為什麼寫?

個人最近看了周志明的《鳳凰架構》中架構安全性部分,書中對於架構安全性做了非常體系的講解,開拓了自己的視野,希望通過本文能夠對其中的關鍵點做下實戰和總結。 如今,大家也比較關心個人的隱私問題,學好安全性相關的內容,能夠對於安全性相關的設計提供一些理論指導,能夠明白安全性做得好的公司為什麼要這麼設計,也能指導自己在公司設計出更加安全的架構,而安全相關問題基本上是與具體業務無關,會有通用的標準,掌握之後可以應用在任何業務場合。

2 你能收穫什麼內容?

  1. 架構安全性包含的部分有哪幾類,以及每一類的側重點是什麼?大家也可以通過周志明的文章進一步瞭解細節。
  2. 對於每一個類中的安全性比較主流/傾向的做法是什麼?這些做法的原理是什麼?並且能夠通過一些示例分析加深理解。

3 架構安全性包含的內容及本文講解的關鍵技術點

架構安全性包含比較多的內容,其中至少包含了以下六個部分。 以下定義擴充自周志明書中的定義:

認證(Authentication):系統如何正確分辨出操作使用者的真實身份?(需要解決你是誰的問題)
授權( Authorization):系統如何控制一個使用者該看到哪些資料、能操作哪些功能?(需要解決你能幹什麼的問題)
憑證(Credential):系統如何保證它與使用者之間的承諾是雙方當時真實意圖的體現,是準確、完整且不可抵賴的?
保密(Confidentiality):系統如何保證敏感資料無法被包括系統管理員在內的內外部人員所竊取、濫用?
傳輸(Transport Security):系統如何保證通過網路傳輸的資訊無法被第三方竊聽、篡改和冒充?
驗證(Verification):系統如何確保提交到每項服務中的資料是合乎規則的,不會對系統穩定性、資料一致性、正確性產生風險?  

o_220129033442_%E6%9E%B6%E6%9E%84%E6%B5%81%E7%A8%8B%E5%9B%BE.png

3.1 認證

以HTTP協議為基礎的認證框架,一般是依靠內容而不是傳輸協議來實現的認證方式,由於實現形式上一般是用了登入表單的方式,因此通常也被稱為“表單認證”。 在2019年之前,表單認證沒有什麼行業標準可循,表單什麼樣子,其中的使用者欄位、密碼欄位、驗證碼欄位是否要在客戶端加密、採用何種方式加密,接受表單的服務地址是什麼?都完全由客戶端和服務端的開發者自行協商決定。 直到2019年3月,全球資訊網聯盟批准了一個世界首份Web內容標準“WebAuthn”,WebAuthn徹底拋棄了傳統的密碼登入方式,改為直接採用生物識別(指紋、人臉等)或者實體金鑰來作為身份憑證,體驗和安全性上較既有的表單認證方式有較好的提升。 一些APP客戶端已經有啟用了這些認證方式,chrome的最新版本也提供了webAuthn的支援,但是到目前為止,已經支援webAuthn登入的網站還比較少,以下是一個網友實現的一個Demo,登入的效果如下:

o_220129094431_zhiwendenglu.gif

實現的程式碼可以從網友的原始博文中獲取。

使用webAuthn註冊和驗證過程原理比較類似,下圖展示了註冊流程:

o_220129120953_webAuthn%E6%B5%81%E7%A8%8B.png

圖裡的註冊流程摘自網站,解釋如下(暫時看不懂沒有關係,你只要知道它作為認證非常安全,體驗也很好):

0.應用程式請求註冊 - 應用程式發出註冊請求。這個請求的協議和格式不在 WebAuthn 標準的範圍內。
1.伺服器傳送挑戰、使用者資訊和依賴方資訊 - 伺服器將挑戰、使用者資訊和依賴方資訊傳送回應用程式。在這裡,協議和格式不在 WebAuthn 標準的範圍內。通常,這可以是基於 HTTPS 連線的 REST(可能會使用 XMLHttpRequest 或 Fetch)API。不過只要在安全連線中,也可以使用 SOAP、RFC 2549 或幾乎任何其他協議。從伺服器接收到的引數將傳遞給 create() ,大部分情況下只需很少修改甚至不需要做任何修改。create() 會返回一個Promise,並返回包含 AuthenticatorAttestationResponse (en-US) 的 PublicKeyCredential (en-US)。需要注意的是挑戰必須是隨機的 buffer(至少 16 位元組),並且必須在伺服器上生成以確保安全。
2.瀏覽器向認證器呼叫 authenticatorMakeCredential() - 在瀏覽器內部,瀏覽器將驗證引數並用預設值補全缺少的引數,然後這些引數會變為 AuthenticatorResponse.clientDataJSON。其中最重要的引數之一是 origin,它是 clientData 的一部分,同時伺服器將能在稍後驗證它。呼叫 create() 的引數與clientDataJSON 的 SHA-256 雜湊一起傳遞到身份驗證器(只有雜湊被髮送是因為與認證器的連線可能是低頻寬的 NFC 或藍芽連線,之後認證器只需對雜湊簽名以確保它不會被篡改)。
3.認證器建立新的金鑰對和證明 - 在進行下一步之前,認證器通常會以某種形式要求使用者確認,如輸入 PIN,使用指紋,進行虹膜掃描等,以證明使用者在場並同意註冊。之後,認證器將建立一個新的非對稱金鑰對,並安全地儲存私鑰以供將來驗證使用。公鑰則將成為證明的一部分,被在製作過程中燒錄於認證器內的私鑰進行簽名。這個私鑰會具有可以被驗證的證照鏈。
4.認證器將資料返回瀏覽器 - 新的公鑰、全域性唯一的憑證 ID 和其他的證明資料會被返回到瀏覽器,成為 attestationObject。
5.瀏覽器生成最終的資料,應用程式將響應傳送到伺服器 - create() 的 Promise 會返回一個 PublicKeyCredential (en-US),其中包含全域性唯一的證照 ID PublicKeyCredential.rawId (en-US)  和包含 AuthenticatorResponse.clientDataJSON 的響應 AuthenticatorAttestationResponse (en-US)。你可以使用任何你喜歡的格式和協議將 PublicKeyCredential (en-US) 傳送回伺服器(注意 ArrayBuffer 型別的屬性需要使用 base64 或類似編碼方式進行編碼)
6.伺服器驗證資料並完成註冊 - 最後,伺服器需要執行一系列檢查以確保註冊完成且資料未被篡改。步驟包括:
  - 驗證接收到的挑戰與傳送的挑戰相同
  - 確保 origin 與預期的一致
  - 使用對應認證器型號的證照鏈驗證 clientDataHash 的簽名和證明
  驗證步驟的完整列表可以在 WebAuthn 規範中找到。一旦驗證成功,伺服器將會把新的公鑰與使用者帳戶相關聯以供將來使用者希望使用公鑰進行身份驗證時使用。   

WebAuthn 採用非對稱加密的公鑰、私鑰替代傳統的密碼,這是非常理想的認證方案,私鑰是保密的,只有驗證器需要知道它,連使用者本人都不需要知道,也就沒有人為洩漏的可能。也解決了傳統密碼在網路傳輸上的風險。 當前的 WebAuthn 還很年輕,普及率暫時還很有限,但書中作者相信幾年之內它必定會發展成 Web 認證的主流方式,被大多數網站和系統所支援。

3.2 授權

授權實質上是在解決:“誰(User)能夠操作(Operation)哪些資源(Resource)”。日常開發中比較多見的是RBAC和OAuth2兩種訪問控制和授權方案。 RBAC通過引入角色概念,賦予角色操作資源的許可權,並且給使用者賦予許可權能夠讓擁有這些角色的使用者操作對應的資源。基本上公司的許可權控制模組都是基於RBAC實現的。

而OAuth2 是面向於解決第三方應用的認證授權協議。允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。 比如很多網站都有通過微信掃一掃登入的功能,也是使用了OAuth2的認證授權過程。

微信掃一掃登入使用了OAuth2中授權碼模式(其他的更多模式可以參考《鳳凰架構》),OAuth2的授權碼方式的訪問流程如下圖所示:

o_220212083558_%E6%8E%88%E6%9D%83%E7%A0%81code.png

在微信掃一掃登入場景中,圖中的資源所有者就是使用者,操作代理是瀏覽器,第三方應用是希望使用微信掃一掃登入的第三方網站,授權伺服器是微信服務端。

比如簡書使用微信掃一掃登入的過程如下:

  • 選擇微信掃一掃登入,開啟該伺服器的微信的二維碼認證頁面,該頁面帶有原始網頁的回撥地址:

    https://open.weixin.qq.com/connect/qrconnect?appid=wxe9199d568fe57fdd&client_id=wxe9199d568fe57fdd&redirect_uri=https%3A%2F%2Fwww.jianshu.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

  • 第三方應用伺服器使用code,appsecret獲取accesstoken 並且拿到微信使用者的基本資訊,標記使用者已經登入。
  • 重定向到第三方登入成功後的頁面。

3.3 憑證

HTTP 協議是一種無狀態的傳輸協議,每一個請求都是完全獨立的,所以一般web開發都會採用cookie-session機制讓伺服器有辦法能夠區分出傳送請求的使用者是誰,在伺服器中會維護了一些使用者的session資訊,為了伺服器的高可用,會把使用者的狀態資訊儲存到Redis等集中式的儲存中。 除了cookie-session機制,還可以使用JWT(JSON Web Token)的方案,能夠讓資訊不儲存在服務端,且也能防止資訊在傳輸過程中被篡改。

JWT的原理:伺服器認證以後,生成JSON 物件,發回給使用者。以後,使用者與服務端通訊的時候,都要發回這個 JSON 物件。伺服器完全只靠這個物件認定使用者身份。為了防止使用者篡改資料,伺服器在生成這個物件的時候,會加上簽名。 JWT:的資料結構由三部分組成Header(頭部)、Payload(負載)、Signature(簽名)。寫成一行,就是下面的樣子:

Header.Payload.Signature

Header 部分是一個 JSON 物件,描述 JWT 的後設資料,通常是下面的樣子。

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload 部分也是一個 JSON 物件,用來存放實際需要傳遞的資料,可以定義業務需要的私有欄位,JWT 規定了7個官方欄位,供選用。

iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號

Signature 部分是對前兩部分的簽名,防止資料篡改。需要指定一個金鑰(secret)。這個金鑰只有伺服器才知道,不能洩露給使用者。然後,使用 Header 裡面指定的簽名演算法(預設是 HMAC SHA256),按照下面的公式產生簽名。

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字串,其中header和payload部分需要使用Base64URL進行轉化。然後三個部分之間用"點"(.)分隔,就可以返回給使用者。

最後的效果如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwMTIxMiIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.RoeGlqmkqtGrgjmV0Z5EF8bwCLQdzRXwPiG1ZmiNVfU   

可以通過網站https://jwt.io/, 檢視和生成編碼後的資料。java有jjwt工具可以在專案中生成和解析JWT, github地址:https://github.com/jwtk/jjwt

客戶端每次與伺服器通訊,都要帶上這個 JWT。你可以把它放在 Cookie 裡面自動傳送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭資訊Authorization欄位裡面。

JWT的應用目前感覺還是比較少,但是提供了一種服務端無狀態的思路,正常情況下使用cookie-session機制能夠滿足要求了。

3.4 保密

密碼如何從客戶端傳輸到服務端,一般會在客戶端對使用者密碼進行簡單的雜湊摘要。從根本上杜絕在伺服器資料庫或者日誌中儲存密碼明文, 大網站被拖庫的事情層出不窮,如果使用明文,就比較危險。 但是不儲存明文,如果只是做了簡單的雜湊在服務端進行儲存,也比較容易通過彩虹表攻擊得到明文,為了應對彩虹表攻擊應加鹽處理。 下面會重點講下為什麼普通的hash方式拿到密文後容易被破解。

比如windows xp的登入密碼會通過一些hash演算法儲存在sam檔案,通過彩虹表攻擊可以快速的獲取到windows xp的登入密碼,如下是通過應用了彩虹表的ophcrack工具破解後的截圖:

o_220212063159_Ophcrack.png

案例中包含了字母和數字的14長度的密碼在15分鐘內被破解,如果更簡單的密碼,可以使用輕量級的彩虹表,基本上在秒級就可以被破解掉。

以14位字母和數字的組合密碼為例,如果使用窮舉法進行破解,共有1.24×1025((10+26+26)14)種可能,即使電腦每秒鐘能進行10億次運算,也需要4億年才能破解。 如果使用海量的磁碟進行hash串的儲存,然後通過查表法獲取明文,就算只有128位的雜湊串的hash串儲存(先不考慮明文儲存)就是一個天文數字(2128/8 位元組)。

那麼彩虹表的做法是在計算時間和儲存空間中做了下平衡。

彩虹表會由很多條鏈組成,針對每一條鏈,會有很多鏈的節點,這些鏈節點之間的關係如下:

【第一個節點(明文1)】->(H)-->【第二個節點(Hash值1)】-->(R1)--->【第三個節點(明文2)】-->(H)-->【第四個節點(Hash值2)】-->(R2)-->【第五個節點(明文3)】

示例:
12345->(H)->abwefsdfse->(R1)->32112->(H)->asdasgasdf->(R2)-->13423

- 其中H是要破解Hash串使用的Hash函式。
- R1~Rx是彩虹表構建的能夠從Hash串到明文的函式。

而彩虹表儲存的時候只需要儲存這條鏈的第一個節點和最後一個節點,從而大大減少了儲存空間(鏈條越長,節點個數越多,節約空間越長)。

彩虹表破解過程如下:

假如一條鏈上有K個Hash值的節點
1. 假設要破解的密文位於某一鏈條的最後一個Hash值節點位置處,先對其進行Rk運算,看是否能夠在末節點中找到對應的值。如果找到,生成這個Hash值的明文即為要破解的明文。
2. 如果找不到,繼續判斷要破解的密文位於倒數第二個Hash值節點位置處,那麼對其做R(k-1),H,RK三步操作,然後再進行末節點的比對。
3. 以此類推,最終找到要破解的明文。

彩虹表自己構建比較麻煩,可以直接從網際網路下載各種Hash方式和大小的彩虹表。 說了這個彩虹表的例子,是說我們需要在做保密的時候增強對於彩虹表破解的難度,可以在Hash的時候加上鹽值。

3.5 傳輸

傳輸層非常重要,如果傳輸層是明文傳遞,比如使用了HTTP傳輸,那麼非常容易被第三方獲取到http內容。 為了讓http傳輸不被第三方竊取,通過https是唯一的手段。那麼https是怎麼確保安全的呢? 簡單來說HTTPS使用了對稱加密和非對稱加密,先通過非對稱加密傳遞了對稱加密金鑰,然後後續通過對稱加密傳遞http內容,非對稱加密中的私鑰由服務端儲存,而公鑰可以公開給所有人,通過公鑰加密的內容只能由服務端的私鑰解密獲得。

目前HTTPS一般採用的是TLS1.2協議,TSL的握手時序如下圖所示:

o_220212091029_TLS%E6%8F%A1%E6%89%8B%E8%BF%87%E7%A8%8B.png

在第二步服務端返回Server Hello的同時,也會返回包含了非對稱加密公鑰的數字證照。 客戶端就使用該公鑰對對稱加密的金鑰進行加密再傳遞給服務端。

通過WireShark抓包也可以清楚看到整個握手過程,如下圖所示:

o_220211105723_https%E4%BC%9A%E8%AF%9D%E8%BF%87%E7%A8%8B.png

至此,還有一個問題,就是客戶端怎麼確保傳遞給他的公鑰就是服務端希望它拿到的公鑰呢。這邊就需要使用到了CA機構。CA機構需要對包含了伺服器公鑰的數字證照進行簽名。 然後客戶端會在作業系統中內建了一些CA根證照,用來對數字證照的有效性進行驗證,從而確保客戶端拿到正確的公鑰。

出於好奇,我通過網際網路瞭解了下是不是存在CA機構洩漏私鑰的情況,如果CA機構的私鑰洩露了,那就是一件非常危險的事情,那麼基於該CA機構簽名的數字證照就會不可信了。 歷史上確實存在CA機構洩漏私鑰的情況,比如荷蘭的CA安全證照提供商DigiNotar,伺服器遭受到了黑客入侵,私鑰被竊取。攻擊者基於此私鑰共發行了 531 個偽造證照,然後微軟緊急釋出了作業系統補丁, 將其列入不信任CA名單,而DigiNotar也因此宣告破產。

而我們平時如果希望在客戶端上抓HTTPS包進行分析,可以用兩類方式:

  1. 使用抓包工具簽發證照,並且在作業系統中標記簽發證照的機構是受信的。比如charles就是用這個方式進行https的抓包。
  2. 獲取到https對稱加密的金鑰,並且使用該金鑰對獲取的加密內容進行解密。比如wireshark使用chrome https傳輸的對稱金鑰。

3.6 驗證

這部分更多的提到了業務驗證,平時我們實際寫業務程式碼最多的也是這個部分,往往會在從客戶端到接入層到邏輯層的各層進行驗證。 程式碼中進行業務邏輯驗證一般會存在的兩個問題:

  1. 程式碼邏輯裡充斥著大量的判空邏輯及其他校驗,影響程式碼的簡潔
  2. 呼叫端和被呼叫端在哪層做校驗更加合理

書中作者提倡的做法是把校驗行為從分層中剝離出來,不是具體在哪一層做邏輯校驗,而是在 Bean 上做。即 Java Bean Validation。 而在Bean上做的可以比較容易地在各層做到重用。比如以下面的程式碼為例,對於Account的驗證可以通過@UniqueAccount進行標註就可以。

public Response createUser(@Valid @UniqueAccount Account user) {
  return CommonResponse.op(() -> service.createAccount(user));
}

更多例子和使用方式,可以通過周老師的鳳凰架構檢視。

4 總結

感謝周老師的書籍(文中有部分截圖也引用自周老師的書籍網站),讓我更加體系化地進行了安全部分的學習,並且通過一些示例加深了對於該部分的理解,同時也希望能夠幫助大家理解。如果大家希望更完整地瞭解相關內容,建議也完整地讀一下週老師的書籍。歡迎大家評論交流。

5 附錄

個人也開通了微信公眾號,大家可以關注公眾號參與評論,也及時收到最新的文章推送。

o_220212151121_cs%20%E5%85%AC%E4%BC%97%E5%8F%B7.jpg

Author: csophys

Created: 2022-02-12 Sat 23:46

Validate

相關文章