Alamofire-安全策略

web服務器和服務器通信的時候,使用https連接是非常重要的,能夠對數據加密傳輸、身份認證。https協議需要到ca申請證書,部署到服務器,應用端連接都是對該鏈接受信任的。證書可申請也可以自籤,自簽證書需要客戶端驗證通過才能訪問。

一、HTTP協議

HTTP是互聯網的基礎協議,默認端口80,爲滿足應用需求HTTP也在不斷的版本升級改進,從0.9版本1.1版本功能不斷的強大起來。HTTP演變可參考:http://www.ruanyifeng.com/blog/2016/08/http.html

HTTP的特點:

  • HTTP客戶端請求只需要確定請求方法和路徑。常用方法有GET、POST、HEAD,由於對參數封裝形式不一樣,一般獲取數據使用GET,上傳數據使用POSTGET參數暴露在鏈接中,POST則封裝在請求體中
  • 使用靈活,可傳輸任意類型的數據,通過Content-Type標記傳輸的數據類型
  • HTTP協議是無狀態協議,對處理過的事務無記憶能力,爲了客戶端服務端更好的交互,引用了CookieSession來存儲請求中產生的狀態

工作流程:

http.png

不需要任何處理,客戶端要服務端就給。

二、HTTPS協議

HTTPS協議爲超文本傳輸安全協議,默認端口443HTTPS=HTTP+SSL,對數據進行加密,保護數據在交互時不被竊取,提高了對服務器惡意攻擊及數據僞裝的成本。使用該協議要求服務器申請證書並配置協議環境。

HTTPS的工作流程:

https.png

  • http、https請求都需要建立TCP三次握手連接,略《Socket》
  • ca頒發的證書加密爲非對稱加密,公鑰加密,私鑰解密,私鑰加密,公鑰解密。加密原理參考《RSA加密》

需要申請證書,綁定域名,服務器中配置證書。一般個人會使用阿里的免費證書,雖然加密性一般,至少證書是受信任的。也可以自己創建證書,自建證書可以使用,但不會被信任,既然有免費的我們就走正規路線😁。

分別發起http和https請求:

http://www.yahibo.top
https://www.yahibo.top

通過Charles抓包觀察數據如下:

charles.png

  • 未加密請求直接抓取服務器響應的所有數據
  • 加密請求抓取到的是未知數據

當然開啓SSL代理還是可以抓到的:

ssl.png

因此在做APP的時候,爲了防止APP被抓包,我們需要做反代理設置抓包,判斷應用代理設置或證書驗證。

三、HTTP和HTTPS主要區別

  1. HTTPS協議需要到ca申請證書,個人頒發證書是不受信任的;

  2. HTTP是超文本傳輸協議,明文傳輸,HTTPS則是在HTTP上加了一層SSL(安全套接層),對數據加密傳輸;

  3. HTTP在服務器上的默認端爲80HTTPS443端口

  4. HTTP連接是無狀態的,HTTPS協議等價於HTTP+SSL/TLS,可進行加密傳輸、身份認證的網絡協議。

兩者的優缺點很明顯,HTTP不存在加密,明文傳輸,不安全,但傳輸速度快,HTTPS密文傳輸,傳輸中需要確定應用端和服務端的保密性和數據完整性。

證書長什麼樣?

ca申請的證書,包括兩個文件.key文件.pem文件,一般在nginx配置文件中配置即可。.key是證書的私鑰文件,.pem爲證書文件。

四、安全策略

1、ServerTrustPolicyManager

在初始化SessionManager中可以看到,有一個初始化類型設置了ServerTrustPolicyManager類對象,該對象就是安全策略的管理者。在實際開發中,APP可能會用到不同的主機地址host,根據業務不同需要要給不同的host設置不同的安全策略。因此管理者的實現如下:

open class ServerTrustPolicyManager {
    public let policies: [String: ServerTrustPolicy]
    public init(policies: [String: ServerTrustPolicy]) {
        self.policies = policies
    }
    open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
        return policies[host]
    }
}
  • 初始化了一個policies字典,keyhost,值爲選擇的響應的策略
  • 初始化方法中設置安全策略及ServerTrustPolicy
  • 通過serverTrustPolicy方法通過host獲取ServerTrustPolicy對象

不要問爲什麼分離一個manager來管理ServerTrustPolicy,不直接使用,因爲這是大廠分工要明確,責任到每一層。

1、ServerTrustPolicy

ServerTrustPolicy是一個枚舉類:

public enum ServerTrustPolicy {
    case performDefaultEvaluation(validateHost: Bool)
    case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags)
    case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)
    case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool)
    case disableEvaluation
    case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool)
     //代碼省略若干
}

以上是類型關聯,根據不同類型做處理。

  • performDefaultEvaluation默認策略類型,始終驗證主機證書的有效性
  • performRevokedEvaluation對吊銷過的證書做設置
  • pinCertificates驗證服務器返回的證書的正確性,參數決定是否驗證證書鏈
  • pinPublicKeys公鑰驗證
  • disableEvaluation無需驗證,無條件信任
  • customEvaluation自定義驗證,返回一個布爾值

以上方法在項目中並沒有默認配置,需要我們配置使用,經常會選擇證書驗證、公鑰驗證、不做驗證模式。具體設置根據需要選擇。

2、發起請求

實踐出真知,我們發送一個https請求看看效果。

let url = "https://www.yahibo.top/project/public/index.php?s=api/test/list"
sessionManager.request(url).response {
    (response) in
    print(response)
}

運行如下:

result.png

運行一切正常,能夠請求到數據,好像也沒什麼區別,其實上面已經做了數據傳輸的說明了加密驗證。下面看一下抓包,開啓Charles,重新發送請求。

http請求一切正常。

https請求運行結果如下:

error.png

http能夠正常請求,https確報錯,這個好像是我們想要的結果,都還沒有配置相關信息😂。在AF中即是https在抓包狀態下也是可以獲取數據的,而Alamofire好像做了更多的處理,直接避免抓包。

該安全策略其實對自簽證書做的驗證,既然我的證書是合法的被認可的就不做配置了😂。如果是自簽證書需要對域名或證書做驗證如下設置:

let policies: [String: ServerTrustPolicy] = [
    "www.yahibo.top": .disableEvaluation
]
let sessionManager = SessionManager(serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
return sessionManager
  • 不做驗證,或者做以上提到的策略

我以爲的被驗證錯誤,打臉😂。後續補充自簽證書驗證,確定是否能夠通過請求。這是一個失敗的實踐,不過還是有收穫的。

……

五、網絡監測

分別看一下AFAlamofire的實現。

AFNetworking-AFNetworkReachabilityManager

枚舉類型,確定網絡狀態:

typedef NS_ENUM(NSInteger, AFNetworkReachabilityStatus) {
    AFNetworkReachabilityStatusUnknown          = -1,
    AFNetworkReachabilityStatusNotReachable     = 0,
    AFNetworkReachabilityStatusReachableViaWWAN = 1,
    AFNetworkReachabilityStatusReachableViaWiFi = 2,
};
  • 未連接網絡、不可用網絡、蜂窩網絡、WiFi網絡
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status){
    [weakSelf setNetworkStates:status];
    switch (status) {
        case AFNetworkReachabilityStatusUnknown:
            NSLog(@"未知網絡");
            break;
        case AFNetworkReachabilityStatusNotReachable:
            NSLog(@"這是一個不可達網絡");
            break;
        case AFNetworkReachabilityStatusReachableViaWWAN:
            NSLog(@"這是一個蜂窩網絡");
            break;
        case AFNetworkReachabilityStatusReachableViaWiFi:
            NSLog(@"這是一個WiFi網絡");
            break;
        default:
            break;
    }
}];
[[AFNetworkReachabilityManager sharedManager] startMonitoring];

網絡監測無非就是以上幾個狀態,設置回調代碼塊,具體監測行爲由AFNetworkReachabilityManager完成。這裏就簡單看一下,下面看一下Alamofire的網絡監測,其實都是一樣的調用的底層api不變。

Alamofire-NetworkReachabilityManager

同樣提供了這麼一個類,來管理網絡監測任務。對框架的學習,最大的收穫就是能夠更快一些的對源碼做分析,快速使用框架。

具體使用

枚舉類型:

public enum NetworkReachabilityStatus {
    case unknown
    case notReachable
    case reachable(ConnectionType)
}

public enum ConnectionType {
    case ethernetOrWiFi
    case wwan
}

使用:

let networkManager = NetworkReachabilityManager(host: "www.yahibo.top")
networkManager?.listener = { status in
    switch status {
    case .unknown:
        print("未知網絡")
        break
    case .notReachable:
        print("這是一個不可達網絡")
        break
    case .reachable(.ethernetOrWiFi):
        print("這是一個WiFi網絡")
        break
    case .reachable(.wwan):
        print("這是一個蜂窩網絡")
        break
}
networkManager?.startListening()
  • 注意對象需要聲明爲全局,在作用域中聲明會被銷燬

大同小異,編碼方式變的更簡潔了,這也符合swift的編碼風格。以上可以看出在Alamofire中的網絡監測將WiFi網絡和蜂窩網絡歸爲一類。下面看一下源碼。

1、屬性

1️⃣、聲明一個閉包,在外部實現呢,網絡變化時掉用閉包傳值

public typealias Listener = (NetworkReachabilityStatus) -> Void

2️⃣、網絡是否可達,包括蜂窩和WiFi網絡

open var isReachable: Bool { return isReachableOnWWAN || isReachableOnEthernetOrWiFi }

3️⃣、蜂窩網絡是否爲可達的

open var isReachableOnWWAN: Bool { return networkReachabilityStatus == .reachable(.wwan) }

4️⃣、通過WiFi連接網絡

open var isReachableOnEthernetOrWiFi: Bool { return networkReachabilityStatus == .reachable(.ethernetOrWiFi) }

5️⃣、獲取網絡類型

open var networkReachabilityStatus: NetworkReachabilityStatus {
        guard let flags = self.flags else { return .unknown }
        return networkReachabilityStatusForFlags(flags)
}

6️⃣、設置監聽閉包在哪個隊列中調用你,默認給主隊列

open var listenerQueue: DispatchQueue = DispatchQueue.main

7️⃣、定一個閉包類型的監聽屬性

open var listener: Listener?

初始化

public convenience init?(host: String) {
    guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
    self.init(reachability: reachability)
}
  • 傳入服務器域名或ip,傳入其他只要不爲空也是可以獲取到網絡類型,還是設置爲我們常用的服務地址比較好
  • 調用了系統方法來初始化一個網絡監測對象,和AF中是一樣的
  • 返回一個SCNetworkReachability對象,網絡地址或名稱的句柄
    SCNetworkReachabilityRef

官方文檔

該對象可以確定當前主機的網絡狀態和目標地址的可達性,提供同步或異步接口,獲取當前的網絡狀態。以上初始化有調用了一個init初始化方法:

private init(reachability: SCNetworkReachability) {
    self.reachability = reachability
    // Set the previous flags to an unreserved value to represent unknown status
    self.previousFlags = SCNetworkReachabilityFlags(rawValue: 1 << 30)
}
  • 存儲對象,並獲取網絡可達標識
  • 到此好像就已經初始化完成

2、啓動監聽網絡

networkManager?.startListening()
內部實現:
open func startListening() -> Bool {
    var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
    context.info = Unmanaged.passUnretained(self).toOpaque()
    let callbackEnabled = SCNetworkReachabilitySetCallback(
        reachability,
        { (_, flags, info) in
            let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
            reachability.notifyListener(flags)
        },
        &context
    )
    let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)
    listenerQueue.async {
        guard let flags = self.flags else { return }
        self.notifyListener(flags)
    }
    return callbackEnabled && queueEnabled
}
  • SCNetworkReachabilitySetCallback監聽網絡狀態的變化,發生改變即調用該回調方法
  • listenerQueue在開啓監聽是調用一次
  • 通過notifyListener通知Listener閉包
  • 將任務添加在主線程中向外回調
func notifyListener(_ flags: SCNetworkReachabilityFlags) {
    guard previousFlags != flags else { return }
    previousFlags = flags
    listener?(networkReachabilityStatusForFlags(flags))
}
  • 先判斷網絡狀態是否改變,如果沒有變化不通知,有變化記錄先前的網絡flags
  • 通過listener閉包向外傳遞當前的網絡狀態
func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
    guard isNetworkReachable(with: flags) else { return .notReachable }
    var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
#if os(iOS)
    if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
#endif
    return networkStatus
}
  • 通過flags來獲取當前的網絡狀態,首先看網絡是否爲可達網絡
  • 通過系統屬性對網絡狀態歸類
func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool {
    let isReachable = flags.contains(.reachable)
    let needsConnection = flags.contains(.connectionRequired)
    let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
    let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired)
    return isReachable && (!needsConnection || canConnectWithoutUserInteraction)
}
  • 通過屬性組合來確定網絡是否可達

3、關閉網絡監聽

當結束後執行deinit方法來調用stopListening方法:

open func stopListening() {
    SCNetworkReachabilitySetCallback(reachability, nil, nil)
    SCNetworkReachabilitySetDispatchQueue(reachability, nil)
}
  • 將回調置空,隊列置空即可

其實網絡監測並不複雜,只是對系統網絡監測類的一個封裝,設置枚舉對網絡狀態歸類,通過listener閉包向外發送網絡監測數據。使用只需要初始化設置主機地址,然後開啓監聽即可。所謂的監聽就是實現對網絡狀態監聽的回調閉包,是框架和系統底層網絡層的消息傳遞。

學習原理及對底層源碼探索是很有必要的,能夠幫助我們快速開發,快速的解決問題。加油⛽️

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章