Pwnage Checker - 使用 Swift 构建的 iOS 应用





5.00/5 (12投票s)
Pwnage Checker 是一款 iOS 应用,允许用户检查某个帐户是否在已知数据泄露中被泄露。

引言
Adobe、Snapchat、Sony,这些只是近年来众多备受瞩目数据泄露事件中的几个。你是否曾经想过你的账户是否在这些数据泄露事件中被泄露过?
在本文中,我们将介绍 Pwnage Checker 的创建过程。Pwnage Checker 是一款使用 Swift 构建的 iOS 应用。它允许用户轻松地检查一个账户是否在任何 iOS 设备上的已知数据泄露事件中被泄露。它利用了 Troy Hunt 创建的 https://haveibeenpwned.com API,该 API 聚合了公开的泄露数据,并使其易于搜索。
核心功能
搜索功能
该应用的主要功能是搜索账户是否在已知的数据泄露事件中被发现,因此主屏幕提供了此功能。搜索的工作流程如下:
- 用户通过以下两种方式之一指定要搜索的账户来开始搜索:
- 用户选择“输入账户”以通过输入账户来开始搜索。
- 用户选择“从联系人中选择”以通过选择现有联系人的电子邮件地址来开始搜索。如果您想为他人(例如,不太懂技术的朋友或家中的长辈)进行检查,这将非常有用。
- 用户将看到搜索结果。
- 如果没有发现泄露,将显示一个带有绿色背景和令人欣慰的笑脸的屏幕。
- 如果账户在一个或多个数据泄露事件中被发现,将显示一个带有红色背景和悲伤表情的屏幕以引起注意。屏幕还将列出账户所在的泄露事件。
- 如果账户在任何数据泄露事件中被发现,用户可以深入查看数据泄露事件的详细信息。
浏览功能
订阅功能
该应用允许用户订阅其账户的泄露通知。订阅功能无法通过 https://haveibeenpwned.com API 使用,但在网站上可用。此屏幕仅托管一个加载该网站订阅页面的 Webview。这仅为方便用户而提供。
使用代码
SearchViewController
类class SearchViewController: UIViewController, CNContactPickerDelegate {
列表 2: SearchViewController.enterAccountButtonTouch
方法
@IBAction func enterAccountButtonTouch(sender: AnyObject) {
var inputTextField: UITextField?
let prompt = UIAlertController(title: "Enter an account", message: "", preferredStyle: UIAlertControllerStyle.Alert)
prompt.addAction(UIAlertAction(title: "Check", style: UIAlertActionStyle.Default, handler: { (action) -> Void in
if let account = inputTextField?.text {
self.checkAccount(account)
}
}))
prompt.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Default, handler: nil))
prompt.addTextFieldWithConfigurationHandler({(textField: UITextField!) in
textField.placeholder = "Enter email address or username"
inputTextField = textField
})
presentViewController(prompt, animated: true, completion: nil)
}
“输入账户”按钮的触摸操作已连接到 enterAccountButtonTouch
方法。当按钮被触摸时,它会创建一个 UIAlertController
来提示用户输入账户。UIAlertController 上的控件是动态添加的。addAction
方法用于添加具有适当处理程序代码的检查按钮和取消按钮。addTextFieldWithConfigurationHandler
方法用于添加和配置文本字段。
列表 3: SearchViewController.selectContactButtonTouch
方法
@IBAction func selectContactButtonTouch(sender: AnyObject) { // show contact picker and configure it with the following behaviors: // contacts with 0 email address: disabled, cannot be selected. // contacts with 1 email address: when selected, the contact's only email address will be used // contacts with 1+ email addresses: when selected, the detail of the contact will be presented, // and user can select one of the email addresses let contactPickerViewController = CNContactPickerViewController() // disable contacts with no email addresses contactPickerViewController.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0") // when a contact is selected, show contact detail if there are more than 1 email addresses contactPickerViewController.predicateForSelectionOfContact = NSPredicate(format: "emailAddresses.@count == 1") // in contact detail card, only show the email address contactPickerViewController.displayedPropertyKeys = [CNContactEmailAddressesKey] contactPickerViewController.delegate = self presentViewController(contactPickerViewController, animated: true, completion: nil) }
“从联系人中选择”按钮的触摸操作已连接到 selectContactButtonTouch
方法。当按钮被触摸时,它会创建并呈现一个 CNContactPickerViewController
,允许用户从联系人中选择电子邮件。
predicateForEnablingContact
属性用于设置联系人何时启用的条件。我们只希望在联系人至少有一个电子邮件地址时启用它,我们这样设置:
contactPickerViewController.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
在下面的屏幕截图中,请注意 David Taylor 联系人在此选择器中被禁用,因为它没有电子邮件地址。
predicateForSelectionOfContact
属性用于设置是否将选定的联系人返回给我们的代码,或者显示选定联系人的详细信息。如果我们希望选定的联系人只有一封电子邮件地址时立即返回,否则我们希望显示详细信息视图,以便用户可以选择多个电子邮件地址中的一个。我们这样设置谓词:
contactPickerViewController.predicateForSelectionOfContact = NSPredicate(format: "emailAddresses.@count == 1")
displayedPropertyKeys
属性用于限制将在联系人详细信息视图中显示的属性。对于我们的应用,我们只需要电子邮件地址。我们这样设置:
contactPickerViewController.displayedPropertyKeys = [CNContactEmailAddressesKey]
下面的屏幕截图显示了当选择具有多个电子邮件地址的联系人时会发生什么。将显示联系人详细信息视图。用户可以从多个电子邮件地址中选择一个。
列表 4: SearchViewController.checkAccount
方法
func checkAccount(account: String) {
LoadingIndicatorView.show("checking")
HaveIBeenPwnedClient.sharedInstance().getBreachesForAccount(account) {
(hasBreaches, result, error) in
dispatch_async(dispatch_get_main_queue()) {
if (error != nil) {
LoadingIndicatorView.hide()
ViewHelper.showError("Unable to check account. ")
print(error)
}
else {
let controller = self.storyboard?.instantiateViewControllerWithIdentifier("SearchResultViewController") as! SearchResultsViewController
controller.account = account
controller.hasBreaches = hasBreaches
controller.apiResult = result
self.navigationController?.pushViewController(controller, animated: true)
LoadingIndicatorView.hide()
}
}
}
}
checkAccount
方法将使用 HaveIBeenPwnedClient
发起搜索,并使用 SearchResultViewController
显示搜索结果。当用户在账户提示中输入账户,或者当用户从联系人选择器中选择电子邮件时,都会调用此方法。SearchResultsViewController
类class SearchResultsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
SearchResultsViewController
显示搜索结果。它使用 UITableView
来显示账户所在的泄露事件列表,因此实现了相关的 UITableViewDataSource
、UITableViewDelegate
协议。
列表 6: SearchResultsViewController.showBreach
方法
func showBreach() { let breachArray = apiResult as! [[String:AnyObject]] for breachItem in breachArray { let breach = Breach(apiBreachResult: breachItem, context: tempContext) breaches.append(breach) print(breach.title) } print(breaches.count) view.backgroundColor = UIColor(red: 229/255, green: 000/255, blue: 0/255, alpha: 1) headerView.backgroundColor = UIColor(red: 229/255, green: 000/255, blue: 0/255, alpha: 1) iconLabel.text = "\u{e403}" titleLabel.text = "Oh no — pwned! Pwned on \(breaches.count) breached sites" subtitleLabel.text = "A \"breach\" is an incident where a site's data has been illegally accessed by hackers and then released publicly. Review the types of data that were compromised (email addresses, passwords, credit cards etc.) and take appropriate action, such as changing passwords." tableView.hidden = false tableView.reloadData() }
当账户在一个或多个数据泄露事件中被发现时,会调用 showBreach
方法。它会更新屏幕背景和标签。它还设置 breachArray 并重新加载 UITableView
来显示账户所在的所以泄露事件。下面的屏幕截图显示了它的样子。
列表 7: SearchResultsViewController.showNoBreach
方法
func showNoBreach() { view.backgroundColor = UIColor(red: 0/255, green: 100/255, blue: 0/255, alpha: 1) headerView.backgroundColor = UIColor(red: 0/255, green: 100/255, blue: 0/255, alpha: 1) iconLabel.text = "\u{e415}" titleLabel.text = "Good news — no pwnage found!" subtitleLabel.text = "No breached accounts" tableView.hidden = true }
当账户未在系统中的任何数据泄露事件中被发现时,会调用 showNoBreach
方法。它会更新屏幕背景和标签。下面的屏幕截图显示了它的样子。
列表 8: BreachTableViewController
类
class BreachTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate
BreachTableViewController
负责显示加载到服务中的所有泄露事件。它在一个 UITableView
中显示数据泄露事件,因此实现了相关的 UITableViewDataSource
和 UITableViewDelegate
协议。它还实现了 NSFetchedResultsControllerDelegate
协议,以在底层数据更改时管理 UITableView
的更新。以下是示例屏幕截图。
列表 9: BreachViewController
类
class BreachViewController: UIViewController { var breach : Breach! @IBOutlet weak var logoImageView: UIImageView! @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var compromisedDataLabel: UILabel! @IBOutlet weak var pwnCountLabel: UILabel! @IBOutlet weak var breachDateLabel: UILabel! @IBOutlet weak var isSensitiveSwitch: UISwitch! override func viewDidLoad() { super.viewDidLoad() navigationItem.title = breach.title compromisedDataLabel.text = breach.dataClasses ?? "N/A" pwnCountLabel.text = breach.pwnCount?.stringValue ?? "N/A" breachDateLabel.text = breach.breachDate ?? "N/A" isSensitiveSwitch.on = breach.isSensitive?.integerValue > 0 var text = "" if let desc = breach.desc?.dataUsingEncoding(NSUTF8StringEncoding) { do { text = try NSAttributedString(data: desc, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute:NSUTF8StringEncoding], documentAttributes: nil).string } catch let error as NSError { print(error.localizedDescription) } } descriptionLabel.text = text if let image = ImageCache.sharedInstance().imageWithName(breach.domain!) { logoImageView.image = image } } }
BreachViewController
负责显示数据泄露事件的详细信息。以下是屏幕截图。
列表 10: NotifyViewController
类
class NotifyViewController: UIViewController { @IBOutlet weak var webView: UIWebView! override func viewDidLoad() { webView.loadRequest(NSURLRequest(URL: NSURL(string: "https://haveibeenpwned.com/NotifyMe")!)) self.navigationController?.navigationBarHidden = true } }
NotifyViewController
类负责加载并显示 haveibeenpwned.com 网站的订阅页面。它
列表 11: HaveIBeenPwnedClient
类
class HaveIBeenPwnedClient : NSObject { func getBreachesForAccount(emailOrUsername : String, completionHandler : (hasBreaches: Bool, result: AnyObject!, error: String?)->Void) { .... } func refreshBreachesInBackground(completionHandler: (error: String?)->Void)->Void { ... } ...
HaveIBeenPwnedClient
类负责向 haveibeenpwned.com API 发出 API 请求。
列表 12: ClearbitClient
类
class ClearbitClient : NSObject { // get image given a domain func getImage(domain: String, completionHandler: (imageData: NSData?, error: String?)->Void) -> Void { ... } ...
ClearbitClient
类负责向 Clearbit Logo API 发出 API 请求,以检索数据泄露事件中公司的 Logo 图像。
列表 13: HttpClient
类
class HttpClient: NSObject { // Perform a HTTP GET operation func httpGet(baseUrl: String, method: String, urlParams: [String:AnyObject]?, headerParams: [String:AnyObject]?, completionHandler: (result: NSData?, code: Int?, error: String?) -> Void) { .... } // Perform a HTTP POST operation func httpPost(baseUrl: String, method: String, urlParams: [String:AnyObject]?, headerParams: [String:AnyObject]?, jsonBody: [String:AnyObject], completionHandler: (result: NSData?, code: Int?, error: String?) -> Void) { .... } // Perform a HTTP PUT operation func httpPut(baseUrl: String, method: String, urlParams: [String:AnyObject]?, headerParams: [String:AnyObject]?, jsonBody: [String:AnyObject], completionHandler: (result: NSData?, code: Int?, error: String?) -> Void) { .... } ...
HttpClient
类是一个辅助类,它提供了方便地发出 HTTP 请求的实用方法。HaveIBeenPwnedClient
和 ClearbitClient
都使用它来发出 API 请求。
关注点
该应用的应用程序逻辑相对简单。大部分时间实际上花在了使应用程序更加完善上。这包括尝试不同的 UI 布局和查找应用程序可以使用的图像资源。在追求使应用程序更具吸引力的过程中,我发现以下很有用:
Google Material Icons
Google Material Icons 是一套精美制作且易于使用的图标,它们根据知识共享许可,可以在您的 Web、Android 和 iOS 项目中使用。下载图标时,您有多种选择。您可以选择 18dp、24dp、36dp 或 48dp 的密度相关像素。您可以选择白色或深色背景。此外,您可以选择 svg、png 或图标字体格式。应用程序中使用的所有图标均来自 Google Material Icons。
Clearbit Logo API
Clearbit Logo API 允许您根据域名快速检索公司的 Logo 图像。在 haveibeenpwned.com API 返回的泄露数据中,每个项目都包含公司域名,但没有公司图像。借助 Clearbit API,该应用程序能够显示图像以及泄露数据。请参阅下图。在纯文本列表和文本+图像列表方面,美学差异巨大。
使用表情符号作为图像
表情符号在不同平台上得到广泛支持。它看起来很有吸引力,并且有广泛的选择。要将它们用作“假”图像,您只需创建一个标签并将其文本设置为所需的表情符号代码。您可以通过更改字体大小来调整大小。在此应用程序中,搜索结果屏幕使用了两个表情符号。
iconLabel.text = "\u{e403}"
当发现泄露时,它会显示悲伤的表情符号。如下图所示:
iconLabel.text = "\u{e415}"
当未发现泄露时,它会显示笑脸表情符号。如下图所示: