前言
本文最初是由 Chris Lowe 编写的, 后来经过 Ryan Ackermann(ios 系统开发者)的修改, 已经可以针对最新的 Xcode 9.2,Swift 4,iOS 11 和 iPhone X 了
软件开发最重要的一个方面同时也被认为是最核心的部分就是应用程序如何实现更好的安全性用户都希望他们的应用程序能够安全运行, 以避免受潜在的威胁
我会在本文中, 为你仔细讲解 iOS 安全的基础知识在了解的同时, 我还会告诉你如何使用一些基本的加密哈希方法来安全地将你的输入信息存储在 iOS 钥匙串中, 这样一来, 你数据的私密性和受保护程度都将大幅度提高
我查了一下, 目前苹果公司共提供了几个 API 来帮助用户提高其所使用的应用程序安全, 并且你将在使用钥匙串时探索这些 API 另外, 你将使用 CryptoSwift 一个经过良好审查的开源库, 实现加密算法
入门知识
如果你是 ios 的初学者, 你点此下载基本的安全知识介绍
材料中所列举的那个应用程序样本是允许用户登录并查看其好友照片的, 目前你正在使用的大部分应用程序已经涉及了你的个人隐私, 所以本文中, 你的工作就是确保应用程序的安全
下载资料在解压后, 请确保打开 Friendvatars.xcworkspace 包含了所有 CocoaPod 依赖项如下所示, 构建并运行该应用程序后, 你将打开一个登录屏幕
不过此时, 当你点击登录按钮时没有任何反应, 这是因为用户的凭证还没有办法进行保存因此, 你要做的第一件事就是要先添加用户的凭证
为什么安全是苹果的重中之重?
在深入了解代码之前, 你应该明白为什么你的应用程序需要强有力的安全保证如果你要存储比较隐私的用户数据, 如电子邮件, 密码或银行帐户信息, 则应用程序的安全性尤其重要
但对苹果来说, 随着系统的更新换代, 安全信息可不止以上这些, 从你拍摄的照片到当天记录的健康数据, 如行走步数, 你的 iPhone 会存储大量更加个性化的数据, 因此这些数据是否安全, 就显得非常重要
俗话说知己知彼, 方能百战百胜, 既然有威胁, 那 iOS 生态系统中的攻击者是谁? 他们想要什么? 攻击者可能是犯罪分子, 商业竞争者, 甚至是朋友或亲戚, 而且每个攻击者想要的内容都不一样有些攻击者可能想要盗取用户隐私信息已进行牟利, 而另一些人可能想看用户的手机中存有什么有价值的商业机密
所以我要在重复一遍, 确保应用程序保存的数据免受潜在威胁的影响是你阅读本文的目的幸运的是, 你不需要像软件开发者那样从头架构一个新的安全框架, 苹果已经构建了许多强大的 API 来简化这项任务
苹果的钥匙串
iOS 开发人员最重要的安全手段之一就是钥匙串, 从 iOS3.0 开始, 系统就提供了钥匙串作为存储账号, 密码, 网络密码, 认证令牌的工具每个应用程序的钥匙串相对来说是独立的, 但是在一些情况下也可以实现应用程序之间钥匙串数据的共享, 前提是必须同一个 TeamID 下的应用简而言之, 它是存储元数据和敏感信息的专用数据库, 使用钥匙串是存储对你的应用至关重要的小块数据 (如秘密和密码) 的最佳做法
为什么要使用钥匙串来作为安全解决方案? 难道仅仅是因为在 UserDefaults 中不存储 base-64 编码的用户密码吗? 当然不是! 对于攻击者来说, 恢复以这种方式存储的密码简直再简单不过了, 如果是这样, 那安全性就很难保证了如果你尝试自己来自定义一套安全解决方案也不是一个好主意即使你的应用程序不涉及金融信息, 存储私人用户信息也不应该掉以轻心
不过, 要直接与钥匙串进行交互, 那是相当复杂的, 尤其是在 Swift 中, 因为你必须使用主要由 C 语言编写的安全框架
幸运的是, 你可以通过从下载材料中的样本代码 GenericKeychain 借用 Swift 封装器来避免使用这些低级 API 就在下载材料中, KeychainPasswordItem 已为钥匙串提供了一个易于使用的 Swift 接口
使用钥匙串
打开 AuthViewController.swift, 该视图控制器会负责你最初看到的登录表单如果向下滚动到 Actions 部分, 你会注意到 signInButtonPressed 没有做任何事情所以你需要花点时间, 来做一些小的修改, 你可以将以下内容添加到 Helpers 的底部:
- private func signIn() {
- // 1
- view.endEditing(true)
- // 2
- guard let email = emailField.text, email.count > 0 else {
- return
- }
- guard let password = passwordField.text, password.count > 0 else {
- return
- }
- // 3
- let name = UIDevice.current.name
- let user = User(name: name, email: email)
- }
接下来会发生以下改变:
1. 你可以通过关闭键盘操作来避免用户的操作行为被人追踪;
2. 你可以接收用户输入的电子邮件和密码, 如果 Eithe 类是零长度, 那么你就不要继续往下在真实的应用程序中, 此时用户就会收到错误提示
3. 你可以为用户分配一个名称, 就本文而言, 你可以从设备名称中分配一个名称
注意: 你可以进入系统偏好设置? 共享并在顶部更改计算机名称来更改你的 Mac 的名称 (由 sim 使用) 此外, 你可以进入 设置? 常规? 关于? 名称来更改 iPhone 的名称
现在在 signInButtonPressed 中添加以下内容:
signIn()
当 signInButtonPressed 被触发时, 会调用你的 signIn 方法, 找到 textFieldShouldReturn 并将 case TextFieldTag.password.rawValue 中的 break 替换为以下内容
signIn()
现在 signIn()被调用, 当用户在键盘上点击返回时, 密码字段就会出现焦点并包含文本不过此时, signIn()尚未完成你仍然需要存储用户对象以及密码, 这些都会在 helper 类中实现
打开 AuthController.swift, 这是一个静态类, 它将保存与此应用程序的身份验证相关的逻辑
首先, 在 isSignedIn 以上的文件顶部添加以下内容:
static let serviceName = "FriendvatarsService"
这是为了定义了即将用于识别钥匙串中的应用程序数据的服务名称如果要使用这个常量, 请在类的最后创建一个 signIn 方法, 如下所示
- class func signIn(_ user: User, password: String) throws {
- try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(password)
- Settings.currentUser = user }
此方法会将用户的登录信息安全地存储在钥匙串中, 然后创建了一个 KeychainPasswordItem, 其中包含你定义的服务名称和唯一标识符(帐户)
对于这个应用程序样本, 用户的电子邮件会被用作钥匙串的标识符, 但对其他样本来说也可以是唯一的用户标识或用户名最后, Settings.currentUser 由存储在 UserDefaults 中的 user 设置的
不过, 此方法并不是最完美的, 因为直接存储用户密码并不是最安全的做法例如, 如果攻击者破坏了苹果的钥匙串, 他就可以用纯文本形式读取用户的密码所以更好的解决方案是存储由用户身份构建的哈希
在 AuthController.swift 的顶部, 由 Foundation 导入以下添加内容:
import CryptoSwift
CryptoSwift 是用 Swift 编写的许多标准加密算法中最受欢迎的集合之一, 不过加密过程是个技术活, 需要正确地使用才可以使用一个流行的安全库意味着你不必从头在设计一遍那些标准化的哈希函数, 最好的加密技术是向公众开放的
注意: 苹果的 CommonCrypto 框架为你提供了许多有用的哈希函数, 但在 Swift 中与它进行交互并不容易这就是为什么我们选 CryptoSwift 库的原因
接下来添加以下的 signIn:
- class func passwordHash(from email: String, password: String) -> String {
- let salt = "x4vV8bGgqqmQwgCoyXFQj+(o.nUNQhVP7ND"
- return "\(password).\(email).\(salt)".sha256()
- }
实现这种方法的前提是需要一个电子邮件和密码, 并返回一个哈希字符串通过加入盐值 (salt) 即盐化可以用来制作通用密码的唯一字符串 sha256()是一种 CryptoSwift 方法, 可以在输入字符串上完成 SHA-2 哈希
在前面我讲过, 攻击者可以通过泄露了钥匙串发现这个哈希攻击者可能会创建一个常用密码表及其哈希表来与此哈希进行比较如果你没有进行盐化处理, 那么输入的哈希密码照样会被攻击盐化会增加攻击的复杂性, 此外, 你可以将用户的电子邮件和密码与盐化值结合在一起以创建一个不易被破解的哈希
注意: 对于使用服务器后端进行身份验证, 应用程序和服务器将共享相同的盐化值, 这就允许他们以相同的方式构建哈希并比较两个哈希来验证身份
返回 signIn(_:password:), 将调用 savePassword 的行替换为:
- let finalHash = passwordHash(from: user.email, password: password)
- try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(finalHash)
这样 signIn 现在就存储了一个强大的哈希, 而不是一个原始密码现在是时候将其添加到视图控制器了, 返回 AuthViewController.swift 并将以下内容添加到 signIn()的底部
- do {
- try AuthController.signIn(user, password: password)
- } catch {
- print("Error signing in: \(error.localizedDescription)")
- }
虽然这会存储用户并保存哈希密码, 但当身份认证更改时, AppController.swift 需要提前得到通知, 所以此时用户要登录应用程序就比较慢了
你可能已经注意到 AuthController.swift 有一个名为 isSignedIn 的静态变量目前, 即使用户登录, 它总是返回 false
在 AuthController.swift 中, 将 isSignedIn 更新为以下内容:
- static
- var isSignedIn: Bool {
- // 1
- guard let currentUser = Settings.currentUser
- else {
- return false
- } do { // 2
- let password =
- try KeychainPasswordItem(service: serviceName, account: currentUser.email).readPassword()
- return password.count > 0
- } catch { return false
- }
- }
接下来会发生以下改变:
1. 你可以马上检查存储在 UserDefaults 中的当前用户, 如果没有存储的用户, 就不会有一个标识符来查找来自钥匙串中的密码哈希, 这就代表用户没有登录
2. 你可以从钥匙串中读取密码哈希, 如果密码存在且不为空, 则就表示该用户已登录
现在, AppController.swift 中的 handleAuthState 将正常工作, 但登录应用程序后才能正确更新 UI 否则, 只能通知应用程序更改状态(如身份验证)
将以下内容添加到 AuthController.swift 的底部:
- extension Notification.Name {
- static let loginStatusChanged = Notification.Name("com.razeware.auth.changed")
- }
在编写自定义通知时使用反向域标识符是一种很好的做法, 这通常来自于应用程序的 bundle 标识符使用唯一标识符可以在调试时提供帮助, 这样任何与你的通知相关的内容都可以从日志中提到的其他框架中被提取出来
若要使用自定义的通知名称, 请将以下内容添加到 signIn(_:password:)的底部:
NotificationCenter.default.post(name: .loginStatusChanged, object: nil)
这样该通知就会被应用程序的其他部分被看见, 在 AppController.swift 的内部, 你可以在 show(in:)之上添加一个 init 方法
- init() {
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(handleAuthState),
- name: .loginStatusChanged,
- object: nil
- )
- }
这样你一旦登录, 经过注册的 AppController 就会通知你已经登录的消息, 它会在触发时调用 handleAuthState 这样在使用任何电子邮件和密码组合登录后, 你都会看到一各好友列表
你可能会注意到, 这些好友没有头像, 只有名字虽然这不太好看, 但已经实现了安全登录的目的了, 至于美观设计, 我会在下面讲到
你会发现, 虽然登录过程很顺利, 但却没有办法退出应用程序这实际上很容易实现, 因为会有一个对身份验证状态更改的通知
返回 AuthController.swift 并在 signIn(_:password:)下面添加以下内容
- class func signOut() throws {
- // 1
- guard let currentUser = Settings.currentUser
- else {
- return
- } // 2
- try KeychainPasswordItem(service: serviceName, account: currentUser.email).deleteItem() // 3
- Settings.currentUser = nil NotificationCenter.
- default.post(name:
- .loginStatusChanged, object: nil)
- }
其主要作用是:
1. 检查你是否已经存储了一个当前用户, 如果没有, 就可以提前退出了;
2. 从钥匙串中删除密码哈希;
3. 清除用户对象并发布通知;
要连接它, 就请跳转到 FriendsViewController.swift, 并将以下内容添加到当前空的 signOut 中:
try? AuthController.signOut()
当选择注销按钮时, 程序就会调用你设置的新方法来清除登录用户的数据
在应用程序中处理错误是一个好主意, 构建并运行, 然后点击注销按钮
现在你就有了一个在应用程序中使用身份验证的完整示例!
哈希
还记得刚刚说到的朋友列表里只有名字, 没有头像的问题吗? 现在我就来解决这个问题
在 FriendsViewController.swift 中, 会显示用户模型对象的列表要想在朋友列表视图中显示头像, 就必须先搞清楚一件事, 那就是用户只有两个属性, 名称和电子邮件, 那你应该如何添加图像呢?
事实证明, 有一项服务可以在接受电子邮件地址的同时将该邮件人的头像显示出来, 这个服务就是 Gravatar
Gravatar 是 Globally Recognized Avatar 的缩写, 是 gravatar 推出的一项服务, 意为全球通用头像如果在 Gravatar 的服务器上放置了你自己的头像, 那么在任何支持 Gravatar 的 blog 或者留言本上留言时, 只要提供你与这个头像关联的 email 地址, 就能够显示出你的 Gravatar 头像来
我们在很多博客或者网站留言, 评论的时候会看到有的人头像很酷很个性化, 但是这个博客和网站本身并没有提供设置头像的功能, 感觉有点神奇, 那么是怎么做到的呢? 其实这是使用了 Gravatar
Gravatar 的概念首先是在国外的独立 WordPress 博客中兴起的, 当你到任何一个支持 Gravatar 的网站留言时, 这个网站都会根据你所提供的 Email 地址为你显示出匹配的头像当然, 这个头像, 是需要你事先到 Gravatar 的网站注册并上传的, 否则, 在这个网站上, 就只会显示成一个默认的头像
所以你唯一需要做的就是向 Gravatar 提出请求并获取他们匹配的头像为此, 你就要创建其电子邮件的 MD5 哈希以构建请求 URL 如果你查看 Gravatar 网站上的文档, 你会发现它需要一个哈希邮件地址来构建用户的请求由于你可以利用 CryptoSwift, 这将是小菜一碟只需在 tableView(_:cellForRowAt:)中添加以下代替关于 Gravatar 的注释即可
- // 1
- let emailHash = user.email.trimmingCharacters(in: .whitespacesAndNewlines)
- .lowercased()
- .md5()
- // 2
- if let url = URL(string: "https://www.gravatar.com/avatar/" + emailHash) {
- URLSession.shared.dataTask(with: url) { data, response, error in
- guard let data = data, let image = UIImage(data: data) else {
- return
- }
- // 3
- self.imageCache.setObject(image, forKey: user.email as NSString)
- DispatchQueue.main.async {
- // 4
- self.tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- }.resume()
- }
具体进程如下:
1. 首先根据 Gravatar 的文档将电子邮件规范化, 然后创建 MD5 哈希;
2. 通过你构建的 Gravatar URL 和 URLSession, 从返回的数据中加载 UIImage;
3. 缓存与头像有关的图像以避免重复获取电子邮件地址;
4. 重新加载表格视图中的行, 以便显示与头像有关的图像;
构建并运行, 现在, 你可以查看朋友的头像和名称了
来源: http://jaq.alibaba.com/community/art/show?articleid=1559