保护用户数据,别让App变“裸奔”:一个搜索工程师的iOS安全实践手记
上周五晚上十一点半,我正瘫在成都家里那张被猫抓得有点破的沙发上刷《三体》,突然钉钉“叮”了一声。一看是产品经理发来的消息:“兄弟,咱们新版本要过审了,Apple那边说我们敏感权限没处理好,再不修就拒了。”
我差点一口冰可乐喷出来。这已经是本月第三次因为隐私问题被打回来了。作为一个在百度干了两年、天天和搜索算法打交道的后端偏移动端杂食型工程师,我本以为自己对“数据安全”理解得够深了——毕竟每天都在处理TB级的用户查询日志。但真到了写 iOS App 的时候,才发现理论和实践之间,隔着一整个App Store审核团队的距离。
说实话,以前我对前端(或者说客户端)的安全防护总有点“傲慢”。觉得“不就是存个 token、加个 HTTPS 嘛”,直到自己踩了坑才明白:在 Apple 的生态里,用户数据不是你“能不能拿”的问题,而是你“有没有资格碰”的问题。
今天这篇笔记,不讲大道理,也不堆术语。就聊聊我在开发内部工具类 App 时,从被拒到一次过审的过程中,总结出的那些真正管用的 iOS 安全开发最佳实践。顺便安利几本让我少走弯路的书——毕竟,代码人生,不能光靠 Stack Overflow 硬扛。
从“裸存”到 Keychain:本地存储的血泪史
最开始,为了赶双11前上线,我图省事,直接把用户登录后的 access token 存在 UserDefaults 里。代码简单到令人发指:
// 千万别学!这是反面教材!
UserDefaults.standard.set(accessToken, forKey: "user_token")
结果呢?第一次提审就被拒了。理由很官方:“App stores sensitive user data insecurely.”(应用以不安全方式存储敏感用户数据)。我当时还纳闷:HTTPS 都用了,数据传输没问题啊?
后来翻 Apple 的 App Store Review Guidelines 第 5.1.1 条才恍然大悟:本地存储同样受严格监管。UserDefaults 是明文存的 plist 文件,随便一台越狱机或者用 Xcode 调试器就能 dump 出来。这等于把家门钥匙贴在楼道公告栏上。
痛定思痛,我重构成用 Keychain。Keychain 是 iOS 提供的加密存储服务,由系统级安全模块(Secure Enclave)保护,即使设备被物理获取,也极难破解。
关键代码如下(基于 KeychainSwift 封装库):
import KeychainSwift
let keychain = KeychainSwift()
keychain.set("my_secret_token", forKey: "auth_token")
keychain.set("user_id_123", forKey: "user_id")
// 读取时
if let token = keychain.get("auth_token") {
// 使用 token
}
小贴士:别自己造轮子!Apple 的原生 Keychain API 写起来又臭又长。推荐用开源库如
KeychainAccess或KeychainSwift,它们封装了复杂性,还支持 group sharing(适合主 App + Widget 场景)。
另外,不要把 Keychain 当数据库用!它适合存少量高敏信息(token、密码、证书),不适合存大量结构化数据。曾经有个同事试图把用户历史搜索记录全塞进 Keychain,结果启动慢得像蜗牛,还触发了系统限制。
网络通信:HTTPS 不是终点,而是起点
说到安全,很多人第一反应是“上 HTTPS”。没错,这是基础。但在 iOS 9 之后,Apple 强制启用了 App Transport Security (ATS),要求所有网络请求必须使用 TLS 1.2+,且证书必须有效。
你以为配个 HTTPS 就万事大吉?Too young.
去年我们对接一个第三方物流接口,对方用的是自签名证书。测试时在模拟器跑得好好的,一上真机就报错:
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
查了半天,发现是 ATS 拦截了。临时方案是在 Info.plist 里加例外:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>insecure-logistics-api.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
但!Apple 明确表示:提审时如果发现你为非必要域名关闭 ATS,大概率被拒。所以我们最后还是逼着第三方升级了证书。
更安全的做法是:证书绑定(Certificate Pinning)。原理很简单——在 App 里预埋服务器公钥指纹,每次连接时校验。这样即使中间人伪造了合法 CA 签发的证书,也无法通过验证。
用 Alamofire 实现证书绑定示例:
let serverTrustPolicies: [String: ServerTrustEvaluating] = [
"api.mysearchapp.com": PublicKeysTrustEvaluator(),
]
let session = Session(
serverTrustManager: ServerTrustManager(evaluators: serverTrustPolicies)
)
session.request("https://api.mysearchapp.com/data").response { response in
// 处理响应
}
当然,证书绑定也有坑:更新证书时必须同步发版 App,否则全量崩溃。我们吃过一次亏,半夜回滚版本,运维差点把我名字刻在工牌背面。
权限申请:别让用户觉得你在“偷看”
作为搜索工程师,我深知用户行为数据的价值。但 Apple 的哲学是:“用户有权知道你在看什么,并随时说不”。
所以,任何涉及隐私的权限(定位、相册、麦克风、通讯录等),必须提前弹窗说明用途,而且文案要具体、真诚。
比如,别写“我们需要访问您的位置以提供更好服务”——太虚了!
应该写:“开启定位后,可为您展示附近的搜索热点(如‘成都春熙路奶茶店’)”。
更重要的是:只在真正需要时才请求权限。别一启动 App 就弹五个权限框,用户会直接点“不允许”然后卸载。
我们在做语音搜索功能时,一开始在首页就请求麦克风权限。结果留存率暴跌。后来改成:点击麦克风图标时才触发授权,转化率立马回升。
SwiftUI 下优雅请求权限的示例:
import AVFoundation
import SwiftUI
struct VoiceSearchView: View {
@State private var isRecording = false
var body: some View {
Button(action: requestMicrophonePermission) {
Image(systemName: "mic.fill")
}
}
private func requestMicrophonePermission() {
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
if granted {
startRecording()
} else {
showPermissionDeniedAlert()
}
}
}
}
}
记住:拒绝权限 ≠ 功能不可用。要提供降级方案。比如用户不让用定位,就默认用城市 IP 定位,而不是直接灰掉按钮。
代码混淆与反调试:防君子也防小人
虽然 Apple 的沙盒机制已经很强,但如果你的 App 涉及金融、支付或高价值数据,建议加上反调试和代码混淆。
我们内部有个工具用于分析搜索 query 的敏感词,逻辑比较核心。为防止被逆向,我做了三件事:
- 启用 Bitcode:虽然 Apple 已逐步弃用,但在某些场景下仍能增加反编译难度。
- 字符串加密:关键 URL、API 字段名用简单 XOR 或 Base64 编码(别用太复杂的,影响性能)。
- 检测调试器:在关键函数入口加一段汇编检测。
反调试代码片段(谨慎使用,可能误伤):
#include <sys/types.h>
#include <sys/ptrace.h>
bool isDebuggerAttached() {
return ptrace(PT_TRACE_ME, 0, 0, 0) == -1;
}
// 在 AppDelegate 初始化时调用
if (isDebuggerAttached()) {
exit(0); // 或者返回假数据
}
不过要提醒:过度防护可能违反 App Store 规则。Apple 明确禁止“干扰审核流程”的行为。我们曾因检测到 Xcode 调试环境就返回空白页面,被认定为“隐藏功能”,直接拒审。
平衡之道是:只对核心数据路径做轻量防护,别搞成“谍战片”。
书籍与学习:站在巨人的肩膀上写代码
说实话,这些经验很多是从坑里爬出来的。但如果有正确的指引,能少走半年弯路。
强烈推荐两本书:
《iOS 应用安全权威指南》(作者:David Thiel)
虽然出版于 2016 年,但底层原理至今适用。讲透了 Keychain、沙盒、代码签名等机制,比网上碎片化教程系统得多。《Secure by Design》(中文版:《安全设计之道》)
不局限于 iOS,但从架构层面教你如何“从第一天起就考虑安全”。我在百度做搜索服务时就受这本书启发,现在写 App 也沿用其思想。
此外,Apple 官方文档其实写得非常清晰,尤其是 Security 和 App Store Review Guidelines 这两块。别嫌英文难啃,每年 WWDC 的 Security 相关 Session 也值得看(比如 WWDC21 的 “Explore secure app and system design”)。
总结:安全不是功能,而是态度
回到开头那个周五夜晚。我改完 Keychain 存储、加上权限说明文案、移除 ATS 例外后,重新打包上传。第二天下午,审核通过。
那一刻我瘫在椅子上,点了杯瑞幸(成都程序员续命水),突然想起刚入职百度时导师说过的一句话:“你写的每一行代码,背后都是真实的人。”
在搜索领域,我们常说“理解用户意图”;在安全领域,其实也一样——你要理解用户的恐惧。他们怕隐私泄露,怕账号被盗,怕手机变成监控器。而我们的责任,就是用技术筑起一道看不见的墙。
现在,每当我写 Swift 代码时,除了考虑性能、可维护性,还会多问一句:“这段逻辑,会让用户感到不安吗?”
代码人生,不止于跑通,更在于值得托付。
附:iOS 安全开发自查清单(提审前必看)
| 检查项 | 是否完成 | 备注 |
|---|---|---|
| 敏感数据是否使用 Keychain 存储? | ✅ / ❌ | 如 token、密码 |
| 所有网络请求是否强制 HTTPS? | ✅ / ❌ | 检查 ATS 配置 |
| 是否实现证书绑定(如必要)? | ✅ / ❌ | 金融类 App 强烈建议 |
| 隐私权限是否延迟申请 + 明确说明? | ✅ / ❌ | 文案需具体 |
Info.plist 中是否包含 NSPrivacy... 描述? |
✅ / ❌ | 如 NSLocationWhenInUseUsageDescription |
| 是否避免在日志中打印用户数据? | ✅ / ❌ | Release 版本关闭 debug log |
| 第三方 SDK 是否最小化集成? | ✅ / ❌ | 移除未使用的分析/广告 SDK |
愿你的 App 永远一次过审,用户数据安然无恙。

评论 0