Alamofire隔离网络框架封装 + 签名 + ssl证书信任

/ 0评 / 1

Apple在元旦开始就要强制iOS开发者们使用 https 请求了,就来一篇吧。简单的讲,强制iOS开发者使用https的作用就是为了防止应用在和后端进行数据通信过程中传输的数据被第三方中间攻击(篡改请求),或者被抓包工具获取我们传输的数据,从而提升我们数据的安全性。https参考链接:http://baike.baidu.com/view/14121.htm 。

封装请求工具类

我们在使用第三方网络请求库时,一般都不会直接调用第三方库的方法,而是自己基于第三方库封装一个工具类,并提供一个单利对象用于网络请求。从而降低耦合度和代码污染,也方便统一处于网络请求和后期替换网络框架等。

我这里这个例子比较简单,仅作为本文章演示签名和ssl证书信任的demo代码,实际项目请根据实际需求去封装即可。

自定义响应枚举

/// 请求响应状态
///
/// - success: 响应成功  - 就是成功
/// - unusual: 响应异常  - 例如 手机已被注册
/// - notLogin: 未登录   - 例如 token到期
/// - failure: 请求错误  - 例如 比如网络错误
enum JFResponseStatus: Int {
    case success  = 0
    case unusual  = 1
    case notLogin = 2
    case failure  = 3
}

用来统一处理请求可能导致的结果,具体按照项目需求来定义,如果项目需要,还可以定义一套客户端的错误码表,以便我们更好的定位错误。

请求回调闭包

/// 网络请求回调
typealias NetworkFinished = (_ status: JFResponseStatus, _ result: JSON?, _ tipString: String?) -> ()

将响应状态枚举、后端返回的数据和提示语回调给调用者,具体也需要根据自己项目的实际情况来定义。

工具类单利对象

/// 网络工具类单例
static let shareNetworkTool = JFNetworkTools()

请求网络我们不需要重复创建很多对象,所以只需要使用单利即可,节约内存也方便调用。

配置SessionManager

并且我们在第一次获取单利对象的同时,需要配置好ssl证书信任策略。首先将后台给我们的证书文件导入项目中,我这里命名为 cert.cer 。然后配置 Alamofire 提供的 SessionManager 对象,这个对象以后将作为我们请求接口使用。

fileprivate var afManager: SessionManager!

override init() {
    super.init()
    
    // 确保项目已经成功导入了cert.cer证书文件
    let pathToCert = Bundle.main.path(forResource: "cert", ofType: "cer")
    let localCertificate = NSData(contentsOfFile: pathToCert!)
    let certificates = [SecCertificateCreateWithData(nil, localCertificate!)!]
    
    let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
        certificates: certificates,
        validateCertificateChain: true,
        validateHost: true
    )
    let serverTrustPolicies = ["不带协议头的域名" : serverTrustPolicy]
    let serverTrustPolicyManager = ServerTrustPolicyManager(policies: serverTrustPolicies)
    
    afManager = SessionManager(
        configuration: URLSessionConfiguration.default,
        serverTrustPolicyManager: serverTrustPolicyManager
    )
    
}

设置公共请求头

我们一般会将app的一些公共版本信息、token/session、签名等设置为请求头,这里我们主要说的是签名的问题。为了防止API调用过程中被黑客恶意篡改,我们调用API的时候都会对请求进行签名,后端也会对签名进行验证,如果验证不通过,请求也就不会继续处理了。(我们和后端使用相同的签名算法,并且比较最终签名结果,从而确保请求未被篡改)

参考地址:http://open.taobao.com/docs/doc.htm?spm=a3142.7395905.4.16.eQ4qIu&articleId=101617&docType=1&treeId=1 。

具体算法以后端提供的为准,我们只需根据后端提供的算法进行处理即可。这里我采用较为通用的算法:hex(md5(headers + parameters + secret))其中 secret 是和后端协商的API密钥。

/// 获取请求头
///
/// - Returns: 请求头
fileprivate func getHTTPHeaders(parameters: [String : Any]?) -> [String : String] {
    
    // api版本
    var headers = [
        "X-API-VERSION" : API_VERSION,
        ]
    
    // token
    if let token = JFUserModel.shareAccount()?.token {
        headers["Authorization"] = "Bearer \(token)"
        print("请求头加入了token", "Bearer \(token)")
    }
    
    // uuid
    if let uuid = JFObjcTools.uuidString() {
        headers["X-DEVICE-ID"] = uuid
        print("请求头加入了device", uuid)
    }
    
    // 请求签名
    // 字典key排序
    var keys = [String]()
    if let parameters = parameters {
        for key in parameters.keys {
            keys.append(key)
        }
    }
    for key in headers.keys {
        keys.append(key)
    }
    keys = keys.sorted(by: {$0 < $1})
    
    // 拼接排序后的参数
    var jointParameters = ""
    for key in keys {
        if headers.keys.contains(key) {
            jointParameters += key
            jointParameters += "\(headers[key]!)"
        }
        if let parameters = parameters {
            if parameters.keys.contains(key) {
                jointParameters += key
                jointParameters += "\(parameters[key]!)"
            }
        }
        
    }
    jointParameters += API_SECRET
    
    let md5Hex =  jointParameters.md5Data()!.map { String(format: "%02hhx", $0) }.joined().uppercased()
    
    // 算法 hex(md5(headers + parameters + secret))
    headers["X-SIGNATURE"] = md5Hex
    print("请求头加入了signature", md5Hex)
    
    return headers
}

封装基本请求方法

接下来我们就可以封装我们的基本请求方法了,这里我封装了基本的 get/post 请求,并使用我们前面配置好的 SessionManager 对象来处理网络请求。这样我们的请求就不会被 Charles 等抓包工具抓取数据或者请求被恶意篡改了。

/// GET请求
///
/// - Parameters:
///   - APIString: urlString
///   - parameters: 参数
///   - finished: 完成回调
func get(APIString: String, parameters: [String : Any]?, finished: @escaping NetworkFinished) {
    
    UIApplication.shared.isNetworkActivityIndicatorVisible = true
    afManager.request(APIString, parameters: parameters, headers: getHTTPHeaders(parameters: parameters)).responseJSON { (response) in
        self.handle(response: response, finished: finished)
    }
}

/// POST请求
///
/// - Parameters:
///   - APIString: urlString
///   - parameters: 参数
///   - finished: 完成回调
func post(APIString: String, parameters: [String : Any]?, finished: @escaping NetworkFinished) {
    
    UIApplication.shared.isNetworkActivityIndicatorVisible = true
    afManager.request(APIString, method: .post,parameters: parameters, headers: getHTTPHeaders(parameters: parameters)).responseJSON { (response) in
        self.handle(response: response, finished: finished)
    }
}

/// 处理响应结果
///
/// - Parameters:
///   - response: 响应对象
///   - finished: 完成回调
fileprivate func handle(response: DataResponse<Any>, finished: @escaping NetworkFinished) {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
    
    switch response.result {
    case .success(let value):
        
        print(response.request?.url ?? "", value)
        let json = JSON(value)
        if json["code"].intValue == 0 {
            finished(.success, json["data"], nil)
        } else if json["code"].intValue == 20104 {
            JFProgressHUD.dismiss()
            // 注销登录
            JFUserModel.logout()
            finished(.notLogin, nil, "请重新登录(\(json["code"].intValue))")
        } else {
            finished(.unusual, nil, json["message"].stringValue + "(\(json["code"].intValue))")
        }
        
    case .failure(let error):
        finished(.failure, nil, error.localizedDescription)
    }
}

封装网络工具类并没有完成,这里只是一个演示demo而已,具体大家自己根据自己的业务逻辑和后台响应来封装吧。

完整代码

JFNetworkTools.swift

import UIKit
import Alamofire
import SwiftyJSON

/// 请求响应状态
///
/// - success: 响应成功  - 就是成功
/// - unusual: 响应异常  - 例如 手机已被注册
/// - notLogin: 未登录   - 例如 token到期
/// - failure: 请求错误  - 例如 比如网络错误
enum JFResponseStatus: Int {
    case success  = 0
    case unusual  = 1
    case notLogin = 2
    case failure  = 3
}

/// 网络请求回调
typealias NetworkFinished = (_ status: JFResponseStatus, _ result: JSON?, _ tipString: String?) -> ()

class JFNetworkTools: NSObject {
    
    /// 网络工具类单例
    static let shareNetworkTool = JFNetworkTools()
    
    fileprivate var afManager: SessionManager!

    override init() {
        super.init()
        
        print("配置ssl,防止抓包工具抓取数据")
        let pathToCert = Bundle.main.path(forResource: "cert", ofType: "cer")
        let localCertificate = NSData(contentsOfFile: pathToCert!)
        let certificates = [SecCertificateCreateWithData(nil, localCertificate!)!]
        
        let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
            certificates: certificates,
            validateCertificateChain: true,
            validateHost: true
        )
        let serverTrustPolicies = ["xxxxxxxx" : serverTrustPolicy]
        let serverTrustPolicyManager = ServerTrustPolicyManager(policies: serverTrustPolicies)
        
        afManager = SessionManager(
            configuration: URLSessionConfiguration.default,
            serverTrustPolicyManager: serverTrustPolicyManager
        )
        
    }
    
    /// 获取请求头
    ///
    /// - Returns: 请求头
    fileprivate func getHTTPHeaders(parameters: [String : Any]?) -> [String : String] {
        
        // api版本
        var headers = [
            "X-API-VERSION" : API_VERSION,
            ]
        
        // token
        if let token = JFUserModel.shareAccount()?.token {
            headers["Authorization"] = "Bearer \(token)"
            print("请求头加入了token", "Bearer \(token)")
        }
        
        // uuid
        if let uuid = JFObjcTools.uuidString() {
            headers["X-DEVICE-ID"] = uuid
            print("请求头加入了device", uuid)
        }
        
        // 请求签名
        // 字典key排序
        var keys = [String]()
        if let parameters = parameters {
            for key in parameters.keys {
                keys.append(key)
            }
        }
        for key in headers.keys {
            keys.append(key)
        }
        keys = keys.sorted(by: {$0 < $1})
        
        // 拼接排序后的参数
        var jointParameters = ""
        for key in keys {
            if headers.keys.contains(key) {
                jointParameters += key
                jointParameters += "\(headers[key]!)"
            }
            if let parameters = parameters {
                if parameters.keys.contains(key) {
                    jointParameters += key
                    jointParameters += "\(parameters[key]!)"
                }
            }
            
        }
        jointParameters += API_SECRET
        
        let md5Hex =  jointParameters.md5Data()!.map { String(format: "%02hhx", $0) }.joined().uppercased()
        
        // 算法 hex(md5(headers + parameters + secret))
        headers["X-SIGNATURE"] = md5Hex
        print("请求头加入了signature", md5Hex)
        
        return headers
    }

}

// MARK: - 普通get/post请求方法,可以根据自己的业务多封装一些通用请求方法
extension JFNetworkTools {
    
    /**
     GET请求
     
     - parameter APIString:  urlString
     - parameter parameters: 参数
     - parameter finished:   完成回调
     */
    func get(APIString: String, parameters: [String : Any]?, finished: @escaping NetworkFinished) {
        
        UIApplication.shared.isNetworkActivityIndicatorVisible = true
        afManager.request(APIString, parameters: parameters, headers: getHTTPHeaders(parameters: parameters)).responseJSON { (response) in
            self.handle(response: response, finished: finished)
        }
    }

    /**
     POST请求
     
     - parameter APIString:  urlString
     - parameter parameters: 参数
     - parameter finished:   完成回调
     */
    func post(APIString: String, parameters: [String : Any]?, finished: @escaping NetworkFinished) {
        
        UIApplication.shared.isNetworkActivityIndicatorVisible = true
        afManager.request(APIString, method: .post,parameters: parameters, headers: getHTTPHeaders(parameters: parameters)).responseJSON { (response) in
            self.handle(response: response, finished: finished)
        }
    }

    /// 处理响应结果
    ///
    /// - Parameters:
    ///   - response: 响应对象
    ///   - finished: 完成回调
    fileprivate func handle(response: DataResponse<Any>, finished: @escaping NetworkFinished) {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        
        switch response.result {
        case .success(let value):
            
            print(response.request?.url ?? "", value)
            let json = JSON(value)
            if json["code"].intValue == 0 {
                finished(.success, json["data"], nil)
            } else if json["code"].intValue == 20104 {
                JFProgressHUD.dismiss()
                // 注销登录
                JFUserModel.logout()
                finished(.notLogin, nil, "请重新登录(\(json["code"].intValue))")
            } else {
                finished(.unusual, nil, json["message"].stringValue + "(\(json["code"].intValue))")
            }
            
        case .failure(let error):
            finished(.failure, nil, error.localizedDescription)
        }
    }
    
}

// MARK: - 简单json缓存 本来不应该放这里的,如果项目需要缓存,应该有自己的DAL(Data access layer)工具类。参考地址:https://blog.6ag.cn/1551.html
extension JFNetworkTools {
    
    /**
     缓存json数据为指定json文件
     
     - parameter json:     JSON对象
     - parameter jsonPath: json文件路径
     */
    func saveJson(_ json: JSON, jsonPath: String) {
        do {
            if let json = json.rawString() {
                try json.write(toFile: jsonPath, atomically: true, encoding: String.Encoding.utf8)
                print("缓存数据成功", jsonPath)
            }
        } catch {
            print("缓存数据失败", jsonPath)
        }
    }
    
    /**
     删除指定文件
     
     - parameter jsonPath: 要删除的json文件路径
     */
    func removeJson(_ jsonPath: String) {
        let fileManager = FileManager.default
        if fileManager.fileExists(atPath: jsonPath) {
            do {
                try fileManager.removeItem(atPath: jsonPath)
                print("删除成功", jsonPath)
            } catch {
                print("删除失败", jsonPath)
            }
        }
    }
    
    /**
     获取缓存的json数据
     
     - parameter jsonPath: json文件路径
     
     - returns: JSON对象
     */
    func getJson(_ jsonPath: String) -> JSON? {
        if let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) {
            print("获取缓存数据成功", jsonPath)
            let json = JSON(data: data)
            return json
        }
        print("获取缓存数据失败", jsonPath)
        return nil
    }
}

// MARK: - 公共业务封装
extension JFNetworkTools {
    
    /// 检测是否能够签到
    func canCheckin(finished: @escaping (_ can: Bool) -> ()) {
        
        JFNetworkTools.shareNetworkTool.get(APIString: API_CHECKIN, parameters: nil) { (status, result, tipString) in
            switch status {
            case .success:
                isCanCheckin = result!["can_checkin"].boolValue
                finished(isCanCheckin)
            case .unusual:
                finished(false)
            case .notLogin:
                finished(false)
            case .failure:
                finished(false)
            }
        }
        
    }

    // 可以封装一些公用的业务接口......... 
}

// MARK: - 网络工具方法
extension JFNetworkTools {
    
    /**
     获取当前网络状态
     
     - returns: 0未知 1WiFi 2WAN
     */
    func getCurrentNetworkState() -> Int {
        return Reachability.forInternetConnection().currentReachabilityStatus().rawValue
    }

    // 可以封装一些网络相关的工具方法......
}

使用工具类请求api接口

JFNetworkTools.shareNetworkTool.get(APIString: API_GAME_LIST, parameters: parameters) { (status, result, tipString) in
    switch status {
    case .success:
        break
    case .unusual:
        break
    case .notLogin:
        break
    case .failure:
        break
    }
}

如有不对的地方,欢迎大神指正。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注