AppPreflight Logo
AppPreflight
loading
返回指南

iOS 应用安全最佳实践:保护用户数据并通过审核

AppPreflight 团队
2026-05-10
5 分钟阅读

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: "&amp;")
        .replacingOccurrences(of: "<", with: "&lt;")
        .replacingOccurrences(of: ">", with: "&gt;")
        .replacingOccurrences(of: "\"", with: "&quot;")
        .replacingOccurrences(of: "'", with: "&#x27;")
}

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 移动风险
  • 第三方依赖项已更新
  • 安全测试已完成

后续步骤

  1. 审核应用的安全漏洞
  2. 为所有 API 调用实现 HTTPS
  3. 在 Keychain 中存储敏感数据
  4. 实现适当的会话管理
  5. 在真实设备上测试安全问题
  6. 使用 AppPreflight 预审工具 验证合规性
  7. 如果处理敏感数据,考虑聘请安全专家进行审查

优先安全开发 = App Store 批准 + 用户信任 + 受保护数据。


这篇指南对你有帮助吗?