iOS 应用安全最佳实践:保护用户数据并通过审核
发布日期: 2026-05-10
最后更新: 2026-05-10
作者: AppPreflight 团队
概述
安全漏洞是应用被拒的常见原因。本指南涵盖保护用户数据和通过 Apple 审核所需的基本安全实践。实现这些实践也向用户证明其数据是安全的。
1. HTTPS/TLS 加密
为什么 HTTPS 是强制性的
- 与服务器的所有网络通信都必须使用 HTTPS
- 需要 TLS 1.2 或更高版本
- 不允许自签名证书
- 证书必须有效且未过期
实现要求
应用传输安全(ATS)
Apple 默认执行 ATS。这意味着:
<!-- 在 Info.plist 中 -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/> <!-- 默认值:需要 HTTPS -->
<key>NSAllowsArbitraryLoadsForMedia</key>
<false/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<false/>
</dict>
证书配置
// 最好的方式:让 URLSession 自动处理 HTTPS
let url = URL(string: "https://api.example.com/users")!
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
// 自动验证 SSL/TLS
}.resume()
// 对于生产 API
var request = URLRequest(url: url)
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
2. 身份验证与会话管理
密码安全
永远不要以纯文本存储密码
// ❌ 错误的方式 - 永远不要这样做
let savedPassword = "user123"
UserDefaults.standard.set(savedPassword, forKey: "password")
// ✅ 正确的方式 - 使用 Keychain
class KeychainManager {
static func savePassword(_ password: String, for account: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: password.data(using: .utf8)!
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
static func retrievePassword(for account: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecReturnData as String: true
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
if let data = result as? Data,
let password = String(data: data, encoding: .utf8) {
return password
}
return nil
}
}
JWT 令牌(最佳实践)
class TokenManager {
static let shared = TokenManager()
private let keychainService = "com.app.tokens"
func saveAccessToken(_ token: String, expiresIn: TimeInterval) {
// 存储在 Keychain 中
let expirationDate = Date().addingTimeInterval(expiresIn)
UserDefaults.standard.set(expirationDate, forKey: "accessTokenExpiration")
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService,
kSecAttrAccount as String: "accessToken",
kSecValueData as String: token.data(using: .utf8)!
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
func getAccessToken() -> String? {
// 检查是否过期
if let expiration = UserDefaults.standard.object(forKey: "accessTokenExpiration") as? Date {
if Date() > expiration {
refreshAccessToken()
return getValidAccessToken()
}
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService,
kSecAttrAccount as String: "accessToken",
kSecReturnData as String: true
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
if let data = result as? Data,
let token = String(data: data, encoding: .utf8) {
return token
}
return nil
}
func logout() {
// 清除所有令牌
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: keychainService
]
SecItemDelete(query as CFDictionary)
UserDefaults.standard.removeObject(forKey: "accessTokenExpiration")
}
}
3. 数据存储安全
敏感数据加密
对敏感数据使用 Keychain
class SecureStorage {
static func saveCredential(
username: String,
password: String
) {
let credentials = "\(username):\(password)".data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.app.credentials",
kSecValueData as String: credentials,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
static func retriveCredential() -> (username: String, password: String)? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.app.credentials",
kSecReturnData as String: true
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
if let data = result as? Data,
let string = String(data: data, encoding: .utf8) {
let parts = string.split(separator: ":")
if parts.count == 2 {
return (String(parts[0]), String(parts[1]))
}
}
return nil
}
}
应该放在 Keychain 中的内容
- ✅ 密码
- ✅ API 令牌
- ✅ OAuth 令牌
- ✅ 证书
- ✅ 私钥
不应该不安全地存储的内容
- ❌ 用户电子邮件(但如果不敏感可以存储在 UserDefaults)
- ❌ 未加密的 PII
- ❌ 源代码中的 API 密钥
- ❌ 硬编码凭证
4. 输入验证与注入防护
SQL 注入防护
// ❌ 易受攻击 - 永远不要这样做
let query = "SELECT * FROM users WHERE email = '\(userInput)'"
// ✅ 安全 - 使用参数化查询
let query = "SELECT * FROM users WHERE email = ?"
let statement = try db.prepare(query)
let rows = try statement.bind(userInput)
跨站脚本(XSS)防护
// ❌ 易受攻击
let html = "<html><body>\(userInput)</body></html>"
// ✅ 安全 - 转义 HTML 实体
func escapeHTML(_ string: String) -> String {
return string
.replacingOccurrences(of: "&", with: "&")
.replacingOccurrences(of: "<", with: "<")
.replacingOccurrences(of: ">", with: ">")
.replacingOccurrences(of: "\"", with: """)
.replacingOccurrences(of: "'", with: "'")
}
let html = "<html><body>\(escapeHTML(userInput))</body></html>"
5. API 安全
API 密钥管理
// ❌ 永远不要在源代码中存储 API 密钥
let apiKey = "sk-1234567890abcdef"
// ✅ 正确的方式 - 从后端获取
class APIKeyManager {
static func fetchAPIKey(completion: @escaping (String) -> Void) {
let url = URL(string: "https://api.example.com/api-key")!
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data,
let response = try? JSONDecoder().decode(APIKeyResponse.self, from: data) {
completion(response.apiKey)
}
}.resume()
}
}
// ✅ 更好的方式 - 使用后端代理
// 客户端调用:https://api.example.com/some-endpoint
// 后端内部调用:https://third-party.com/api 带 API 密钥
// API 密钥永远不会暴露给客户端
安全验证清单
提交到 App Store 之前:
- 所有网络通信都使用 HTTPS/TLS 1.2+
- 没有硬编码密码或 API 密钥
- 密码存储在 Keychain,而不是 UserDefaults
- 敏感数据在静止时加密
- 注销时会话失效
- 日志中没有未加密的 PII
- 对所有用户输入进行输入验证
- 没有 SQL 注入漏洞
- 实现了速率限制
- 关键 API 的证书固定
- 支持生物特征身份验证(如适用)
- 生产构建中没有调试符号
- 已解决 OWASP Top 10 移动风险
- 第三方依赖项已更新
- 安全测试已完成
后续步骤
- 审核应用的安全漏洞
- 为所有 API 调用实现 HTTPS
- 在 Keychain 中存储敏感数据
- 实现适当的会话管理
- 在真实设备上测试安全问题
- 使用 AppPreflight 预审工具 验证合规性
- 如果处理敏感数据,考虑聘请安全专家进行审查
优先安全开发 = App Store 批准 + 用户信任 + 受保护数据。