iOS 应用网络安全之 HTTPS
安全套接字层(Secure Socket Layer, SSL)是实现互联网安全通信的最普遍标准。Web 应用程序使用 HTTPS(基于 SSL 的 HTTP),HTTPS 使用数字证书来确保在服务器和客户端之间进行安全、加密的通信。在 SSL 连接中,客户机和服务器在发送数据之前都要对数据进行加密,然后由接受方对其进行解密。
1. HTTPS 握手流程
当浏览器(客户端)需要与某个安全站点建立连接时,先建立 TCP 连接(三次握手),然后再发生 SSL 会话握手:
- 请求:浏览器通过网络发送请求安全会话的消息(通常以 https 而非 http 开头的 URL)。
- 响应:服务器通过发送其证书(包括公钥)进行响应。
- 验证:浏览器检验服务器的证书是否有效,是否由其位于浏览器数据库中的可信 CA 签发,以及 CA 证书是否已过期。
- 密钥交换:如果证书有效,浏览器生成一个一次性的会话密钥,并使用服务器的公钥对该会话密钥进行加密后发送给服务器。
- 解密:服务器使用其专用密钥对消息进行解密,恢复会话密钥。
- 加密通信:握手之后,客户端已验证 Web 站点的身份,双方拥有会话密钥副本,后续所有通信均使用该密钥加密。
上述是普遍的单向验证方式(客户端验证服务端)。也可以做双向验证(服务端也验证客户端),一般用于银行业务(如 U 盾)。本文主要关注普遍的单向验证方式的应用。
2. iOS 移动开发 HTTPS 应用现状
当下绝大多数的移动互联网项目都采用 HTTP、HTTPS 协议作为前后端的数据接口协议。在 iOS 开发群体中,绝大部分都在项目中应用了第三方开源的 HTTP 请求框架 AFNetworking 来快速高效地开发。
从 iOS 9.0 开始,Apple 默认只允许采用经过权威证书颁发机构签名的证书的 HTTPS 站点的访问。一切是为了安全。开发者需要在 Info.plist 中设置 App Transport Security 才能加载非 HTTP 的资源,或者配置例外域名。
HTTPS 的服务器配置的证书分两大类:
- 权威机构签名颁发的证书:通常需要购买服务,也有少数机构提供免费证书(如 Let's Encrypt, WoSign)。
- 自签名证书:服务器配置的是研发人员自己签名生成的证书,常用于内部测试环境。
3. AFN 调用使用权威机构颁发证书的 HTTPS 接口
AFNetworking 框架修复了 SSL 中间人攻击漏洞,并强烈要求开发者使用公钥绑定或者证书绑定的安全策略。正确使用 AFNetworking 请求这类证书的 HTTPS 站点代码如下:
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
policy.validatesDomainName = YES;
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = policy;
manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
对于这类证书的站点,Info.plist 都不需要特殊设置,因为已经是权威机构颁发的证书了。我们只需要设置验证绑定方式和验证域名以防止中间人攻击。
4. AFN 调用使用我们自己签名证书的 HTTPS 接口
对于使用自签名证书的站点,浏览器打开 web 站点也会默认阻止访问,除非用户手动把该站点加入信任列表。这个手动加入的过程其实就是不去验证服务器的合法性,任性地认为服务器是可信赖的。
如果在 App 端不严格验证,虽然可以成功访问目标服务器返回数据,但中间很有可能返回的数据不是真正的目标服务器返回的数据,也可能是网络传输中间的第三者伪装返回的数据。传输的数据被人窃取甚至篡改都是很可能的。
4.1 不正确的做法
浏览器手动加入自签名站点到信任列表的操作,相当于 iOS 开发中 AFNetworking 的 API 如下设置:
- Info.plist 设置:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> - AFNetworking 代码设置 SecurityPolicy:
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; // 允许非权威机构颁发的证书 manager.securityPolicy.allowInvalidCertificates = YES; // 也不验证域名一致性 manager.securityPolicy.validatesDomainName = NO; // 关闭缓存避免干扰测试 manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; [manager GET:@"https://your-domain.com/api" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSLog(@"%@", responseObject); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"%@", error); }];
经过如上两步设置之后,可以在 iOS 应用中访问采用自签名证书的 HTTPS 站点。但是这个是不安全的,因为它在没有使用 HTTPS/SSL 代理和使用像 Charles 那样的 HTTPS/SSL 代理的情况下都可以访问服务器资源。这只能防止'君子'在网络中用 Wireshark 之类来 TCP 抓包嗅探,因为毕竟还是 HTTPS 加密了传输数据。但这不能防止中间人攻击!例如用户可以给手机设置 HTTPS 的 SSL 代理(比如 Charles),完全可以在代理中看到明文数据。
4.2 正确的做法:证书锁定(Certificate Pinning)
要在 App 端严格验证服务器的合法性,防止网络中间的代理或者防火墙进行中间人的攻击和证书欺骗,我们需要把服务器配置的证书打包到客户端程序中(私钥留服务器不要分发,非常重要),在代码里去读取该证书/公钥信息和服务器返回的进行匹配验证。
步骤一:配置 Info.plist 白名单
从 Xcode 7 和 iOS 9 开始,Apple 提升了 App 的网络安全性。App 默认只能进行对采用权威机构签名颁发证书的 Web 站点进行访问。自签名证书的 HTTPS 站点属于例外,所以需要在 App 的 Info.plist 中单独为域名设置 Exception Domains 白名单,而不是打开 Allow Arbitrary Loads 全部放开。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>tv.diveinedu.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
建议使用白名单模式,仅针对特定域名放开,而不是全局放开。
步骤二:实施证书锁定
要做到严格验证防止像 Charles 那样的中间人代理抓包,AFNetworking 代码应该使用公钥绑定模式。将服务器端配置的包含公钥的证书分发到客户端后,需要转换为 DER 格式的证书文件。
-
转换证书格式: 使用 OpenSSL 命令将
.crt转换为.der:openssl x509 -outform der -in tv.diveinedu.com.crt -out tv.diveinedu.com.der将生成的
.der文件添加到 Xcode 项目的 Bundle 中。 -
代码实现:
NSString *certFilePath = [[NSBundle mainBundle] pathForResource:@"tv.diveinedu.com" ofType:@"der"]; NSData *certData = [NSData dataWithContentsOfFile:certFilePath]; NSSet *certSet = [NSSet setWithObject:certData]; AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey withPinnedCertificates:certSet]; policy.allowInvalidCertificates = YES; // 允许自签名,但需配合 pinning 验证 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.securityPolicy = policy; manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; [manager GET:@"https://tv.diveinedu.com/api" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSLog(@"%@", responseObject); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"%@", error); }];
上面的代码能够验证服务器身份。在没有使用代理的时候可以正常访问服务器的资源,但是一旦用户给手机网络设置了如 Charles 那样的 HTTPS/SSL 代理服务,则会出现服务器证书验证失败,SSL 网络连接会断开。这样就达到了防止中间人攻击的效果,老板再也不用担心数据接口被人抓包或者代理给扒出来了。
5. 总结与建议
在移动端开发中,仅仅开启 HTTPS 并不足以保证绝对安全。为了防止中间人攻击和数据泄露,建议遵循以下最佳实践:
- 优先使用权威证书:生产环境务必申请并部署由权威 CA 机构签发的证书。
- 实施证书锁定:对于必须使用自签名证书的场景(如内网测试),务必在客户端实现证书锁定(Pin),确保只有特定的公钥或证书能被信任。
- 最小化权限配置:Info.plist 中尽量使用白名单机制,避免全局允许任意 HTTP 加载。
- 定期更新证书:注意证书的有效期,及时更新过期的证书,防止因证书失效导致的服务不可用或被利用。
- 原生方案替代:随着 AFNetworking 维护频率的变化,新项目可考虑使用系统原生的
NSURLSession配合URLSessionDelegate来实现更底层的证书验证逻辑。
通过上述措施,可以有效提升 iOS 应用的网络通信安全性,防止敏感数据在传输过程中被窃听或篡改。


