本文由cocoaChina翻譯小組成員YueWang(博客)翻譯自Raywenderlich。
原文:Securing iOS User Data: The Keychain, Touch ID, and 1Password
使用登錄界面來保護APP用戶數據是很好的方法--你可以使用Keychain(內嵌在iOS裡的)來確保用戶數據的安全。不過蘋果現在使用Touch ID提供了另外一層保護,該功能適用於iPhone5、iPhone6、 iPhone 6+、iPad Air2以及iPad mini 3。
如果這些都還不夠,可以嘗試iOS 8引入的擴展,你甚至可以使用AgileBits開發的1Password app來整合登錄信息的存儲和獲取。這一切都要感謝AgileBits團隊開發者開源了他們的擴展包。這樣你就可以把管理登錄信息的責任交給Keychain、TouchID或者1Password。
在這個教程中,我們將使用Keychain 來存儲和驗證登錄信息。之後,我們會學習Touch ID,最後將1Password 擴展集成到你的app中。
注意:Touch ID和1Password要求在真機上測試,Keychain可以在模擬器上測試。
開始
請在此下載該教程的初始工程文件。
這是一個最基本的使用Core Data存儲用戶筆記的記筆APP;storyboard中包含一個登錄界面,用戶可輸入用戶名和密碼,這個APP的其他界面已經都關聯好了,而且可以直接使用。
編譯並運行工程,查看APP在當前狀態下的展示情況:
現在,點擊Login按鈕會關閉當前界面並展示筆記的列表-你也可以在該界面上創建一個新的筆記。點擊Logout會帶你返回到Login界面。如果這個APP處於後台,它會馬上返回到login界面。這種方式保障了在沒有登錄的情況下是看不到任何數據的。將Info.plist裡面的Application does not run in background設置為YES就可以達到這個效果。
開始之前,更改Bundle Identifier,選擇一個合適的Team。
在工程導航器中選中TouchMeIn,然後選擇TouchMeIn target。在General 標簽中更改Bundle Identifier為你自己的域名-使用反向域名構造標識符規則-例如com.raywenderich.TouchMeIn.
然後,如下,在Team菜單上選擇你的開發團隊相應的賬戶。
所有配置齊全後,開始coding吧!:]
Logging? Log In.
話不多說,開始吧!現在你要增加功能來對照用戶提供的驗證信息和硬編碼的值。
打開LoginViewController.swift,在managedObjectContext變量聲明的下方添加下面的常量:
let usernameKey = "batman" let passwordKey = "Hello Bruce!"
以上是僅僅是硬編碼用戶名和密碼,用來核對用戶提供的驗證信息。
在loginAction(_:)下方添加以下函數:
func checkLogin(username: String, password: String ) -> Bool { if ((username == usernameKey) && (password == passwordKey)) { return true } else { return false } }
該方法查看用戶提供的驗證信息與你提前定義的常量是否匹配。
接下來,將loginAction(_:) 裡的內容替換為如下內容:
if (checkLogin(self.usernameTextField.text, password: self.passwordTextField.text)) { self.performSegueWithIdentifier("dismissLogin", sender: self) }
該方法調用了checkLogin(_:password:)方法,如果驗證信息正確則收起登錄界面。
編譯並運行程序,輸入用戶名batman和密碼Hello Bruce!,點擊Login按鈕,登錄界面應該按照預期被收起。
盡管這個做法可以達到驗證效果,但是非常不安全,因為以字符串形式保存驗證信息能被訓練有素的黑客使用正確的工具輕松獲取。最好的策略是,永遠不要將密碼直接保存在app裡。
為此,你應該使用Keychain 來保存密碼。查看Chris Lowe的Basic Security in iOS 5--Part 1教程來學習Keychain 的實際工作原理。
下一步將Keychain封裝添加到你的APP。雖然它是用Objective-C寫的,但也要添加一個bridging header頭文件,以便從Swift中訪問Objective-C類。
Rapper? No. Wrapper.
你可以在此下載KeychainWrapper,來自於蘋果的Keychain Services Programming Guide.
下載解壓後,將KeychainWrapper.h 和 KeychainWrapper.m拖進你的工程裡,如下所示:
在彈出窗口中,選中Copy items if needed 和TouchMeIn target。
在Swift工程中添加Objective-C文件,Xcode會為你創建一個橋接頭文件-點擊 Yes:
這時候創建了一個名為TouchMeIn-Bridging-Header.h的橋接頭文件。將所有Objective-C文件的頭文件添加到文件中,以便Swift工程可以訪問得到。
想要檢查橋接頭文件設置是否正確,在工程導航器中選擇TouchMeIn。選中Build Settings,在搜索欄中輸Swift Compiler,然後找到Objective-C Bridging Header欄,當前欄中包含TouchMeIn/TouchMeIn-Bridging-Header.h如下圖所示:
提示:你可以在Porting Your App to the iPhone 6, iPhone 6 Plus and iOS 8: Top 10 Tips(中文譯文)中了解更多關於橋接頭文件的信息。想了解更多Swift和Objective-C混編的信息,可以查看蘋果的Using Swift with Cocoa and Objective-C指南.
打開TouchMeIn-Bridging-Header.h文件,在文件開頭導入Keychain 封裝包:
#import "KeychainWrapper.h"
編譯並運行工程,以確保沒有任何錯誤。一切正常?很好—現在你可以在APP中發揮Keychain的優勢了。
提示:如果工程中存在錯誤,請查看蘋果的Using Swift with Cocoa and Objective-C指南來修復錯誤。
Keychain, Meet Password. Password, Meet Keychain
想使用Keychain,必須先保存一個用戶名和密碼。然後檢查用戶提供的驗證信息和keychain裡面保存的信息是否匹配。
你應該追蹤用戶是否已經創建了驗證信息,以便你可以把login按鈕的展示文字從“Create”更改為“Login”。你也應該存儲用戶名到user defaults當中,以便檢查驗證信息是否創建,而不是每次都訪問keychain。
打開LoginViewController.swift文件然後刪掉以下內容:
let usernameKey = "batman"
let passwordKey = "Hello Bruce!"
在以上刪除掉的地方添加下面的內容:
let MyKeychainWrapper = KeychainWrapper() let createLoginButtonTag = 0 let loginButtonTag = 1 @IBOutlet weak var loginButton: UIButton!
MyKeychainWrapper保留了到Objective-C KeychainWrapper類的引用。接下來的兩個常量會用來區分Login按鈕到底是用來創建驗證信息還是用來登錄;根據前面的兩個狀態(創建或登錄),LoginButton outlet用來更新login 按鈕的展示文字。
打開Main.storyboard,然後從Login View Controller裡執行Ctrl-drag操作,拖拉到Login按鈕,如圖所示:
在彈出框裡選擇loginButton
接下來,當按鈕被輕觸時,你要處理兩種可能情況:如果用戶還沒有創建過驗證信息時,按鈕文字應該展示“Create,否則按鈕展示“Login”。你也需要檢查輸入的驗證信息和keychain保存的信息是否匹配。
打開LoginViewController.swift,將loginAction(_:) 裡面的代碼替換為如下內容:
@IBAction func loginAction(sender: AnyObject) { // 1. if (usernameTextField.text == "" || passwordTextField.text == "") { var alert = UIAlertView() alert.title = "You must enter both a username and password!" alert.addButtonWithTitle("Oops!") alert.show() return; } // 2. usernameTextField.resignFirstResponder() passwordTextField.resignFirstResponder() // 3. if sender.tag == createLoginButtonTag { // 4. let hasLoginKey = NSUserDefaults.standardUserDefaults().boolForKey("hasLoginKey") if hasLoginKey == false { NSUserDefaults.standardUserDefaults().setValue(self.usernameTextField.text, forKey: "username") } // 5. MyKeychainWrapper.mySetObject(passwordTextField.text, forKey:kSecValueData) MyKeychainWrapper.writeToKeychain() NSUserDefaults.standardUserDefaults().setBool(true, forKey: "hasLoginKey") NSUserDefaults.standardUserDefaults().synchronize() loginButton.tag = loginButtonTag performSegueWithIdentifier("dismissLogin", sender: self) } else if sender.tag == loginButtonTag { // 6. if checkLogin(usernameTextField.text, password: passwordTextField.text) { performSegueWithIdentifier("dismissLogin", sender: self) } else { // 7. var alert = UIAlertView() alert.title = "Login Problem" alert.message = "Wrong username or password." alert.addButtonWithTitle("Foiled Again!") alert.show() } } }
上述代碼分析:
代碼裡發生了如下步驟:
如果用戶名或密碼為空,則彈出一個提示框並從該方法返回。
如果鍵盤可見則關閉它。
如果login 按鈕的tag是createLoginButtonTag,則繼續創建一個新的login。
接下來,從NSUserDefaults裡讀取hasLoginKey 的值,該值表明Keychain裡面是否已經保存過了密碼。如果username非空,而且hasLoginKey 表明沒有保存過登錄信息,則將username的值保存到NSUserDefaults。
然後使用mySetObject和writeToKeychain把密碼的值保存在Keychain中。之後將NSUserDefaults裡面hasLoginKey的值設置為true,用以表示密碼已經被保存在了keychain當中了。設置login按鈕的tag值為loginButtonTag ,這樣用戶下次開啟你的APP時會自動彈出讓用戶登錄的界面,而不是彈出讓用戶創建登錄的界面。最後,關閉loginView。
如果用戶是登錄(如loginButtonTag表明),可調用checkLogin(_:password:)方法來驗證用戶提供的登錄信息;如果驗證信息是匹配的,則關閉登錄界面。
如果驗證失敗,則彈出提示信息給用戶。
注意:為什麼不直接像用戶名那樣把密碼直接保存在NSUserDefaults裡面呢?因為直接保存後果很嚴重。因為NSUserDefaults是由plist文件存儲的。Plist文件本質上就是一個在APP的Library文件夾下的一個XML文件 ,它可以被任何可直接接觸到設備的任何人讀取。另一方面,Keychain則是利用Triple Digital Encryption Standard (3DES) 來加密數據的。
接下來,用以下內容替換checkLogin(_:password:)方法的實現:
func checkLogin(username: String, password: String ) -> Bool { if password == MyKeychainWrapper.myObjectForKey("v_Data") as NSString && username == NSUserDefaults.standardUserDefaults().valueForKey("username") as? NSString { return true } else { return false } }
改方法檢查了用戶名是否匹NSUserDefaults裡儲存的值,以及密碼是否匹配Keychain裡存的值。
現在就需要根hasLoginKey的狀態來設置正確的按鈕展示文字以及tag。
將以下內容添加到viewDidLoad()方法:
// 1. let hasLogin = NSUserDefaults.standardUserDefaults().boolForKey("hasLoginKey") // 2. if hasLogin { loginButton.setTitle("Login", forState: UIControlState.Normal) loginButton.tag = loginButtonTag createInfoLabel.hidden = true } else { loginButton.setTitle("Create", forState: UIControlState.Normal) loginButton.tag = createLoginButtonTag createInfoLabel.hidden = false } // 3. let storedUsername : NSString? = NSUserDefaults.standardUserDefaults().valueForKey("username") as? NSString usernameTextField.text = storedUsername
代碼解釋如下:
首先利用hasLoginKey的值來查看當前用戶是否已經保存過登錄信息。
如果保存過,把登錄按鈕的文字設為Login,更新按鈕的tag值為loginButtonTag,並隱藏createInfoLabel--包含“Start by creating a username and password”文本。如果該用戶沒有保存過登錄信息,則設置按鈕文字為Create,且展示createInfoLabel。
最後,把用戶名信息填入NSUserDefaults裡以便用戶更方便的進行登錄操作 ?
編譯並運行。輸入一個你選擇的用戶名和密碼,點擊Create。
注意:如果你忘記了把loginButton IBOutlet關聯起來,則可能會看見“Fatal error: unexpectedly found nil while unwrapping an Optional value”。如果看見了這個錯誤信息,則照先前步驟提示將outlet關聯起來。
現在點擊Logout,然後嘗試用相同的用戶名和密碼進行登錄-應該就能看到一串筆記列表。
點擊Logout再次登錄,這次使用另外的一個密碼,後點Login,你應該會看到如下的錯誤提示框:
恭喜!你已經成功利用Keychain進行身份驗證啦。下一步,挑戰Touch ID吧!
Touching You, Touching Me
注意:想要嘗試Touch ID,你必須在一個支持Touch ID的真機上運行APP。目前為止,支持的設備有iPhone 5s/6/6+、iPad Air 2以及iPad mini 3。
在這節中,除了使用Keychain外,你還要在工程中加入Touch ID。盡管Touch ID工作時並非一定要使用Keychain ,但保守起見最好還是實現一個備用的認證方法,例如Touch ID運行失敗,或者用戶手機不支持Touch ID。
打開 Images.xcassets.
在這裡下載該工程將使用到的圖片資源。解壓並打開文件夾,找到Touch-icon-lg.png、[email protected]和[email protected], 選中這三張圖片然後拉到Images.xcassets下。對於Xcode,這三張圖片是同一張圖片,只是分辨率不同而已。
打開Main.storyboard,然後從Object Library中拖一個按鈕到Login View Controller Scene,位於標簽下方。
按照下面操作,使用Attributes Inspector來調節按鈕的屬性:
將Type設置為Custom.
將Title設置為空.
將Image設置為Touch-icon-lg.
完成後,按鈕的屬性值應該如下圖所示:
按照如下值在Size Inspector中設置按鈕:
Show 設為 Frame Rectangle
X 設為 267
Y 設為 341
Width 設為 67
Height 設為 66
完成後,Size Inspector應該如下圖:
選中你創建的這個新按鈕,點擊storyboard畫布下方的layout bar當中的pin按鈕,按照如下信息設置約束條件:
Top Space 設為 16.5
Width 設為 67
Height 設為 67
接下來,點擊align 按鈕並在Container中檢查Horizontal Center。
最後,點擊Resolve Auto Layout Issues圖標,選中Selected Views\Update Frames,如下所示:
這會更新按鈕的frame以便匹配新的約束。
你的界面現在應該如下圖:
下一步,依然在Main.storyboard中,打開 Assistant Editor,確保已經展示LoginViewController.swift。
按住Ctrl鍵把新建的按鈕關聯到LoginViewController.swift,放置在其他的屬性下方,如下圖:
在彈出框中將其命名為touchIDButton,然後點擊Connect。
當設備不支持Touch ID時,這會創建一個outlet用來隱藏這個按鈕。
現在給這個按鈕加一個action。
按住Ctrl鍵拖拽這個按鈕到 LoginViewController.swift 裡的checkLogin(_:password:)方法上方:
在彈出框裡,將Connection更改為Action,將其命名為touchIDLoginAction,然後點擊Connect。
編譯並運行看是否存在錯誤。目前仍然可以選擇模擬器運行,因為還沒有加任何關於Touch ID的東西。但是下面就要開始引入了。
Adding Local Authentication
實現Touch ID就跟引入Local Authentication和調用一些便捷卻強大的方法一樣簡單。
以下是Local Authentication文檔內容:
“Local Authentication 框架提供了基於特定安全策略的向用戶要求身份驗證的工具。”
這裡提到的特定安全策略就是用戶的生物識別信息,也就是用戶的指紋。
打開LoginViewController.swift,在CoreData import下加入下面的import
import LocalAuthentication
現在需要一個指向LAContext 類的引用,還需要一個error屬性。
將以下代碼加入所有屬性的下方:
var error : NSError? var context = LAContext()
接下來在這個教程中,你將會用到error;context引用了一個驗證上下文環境,也就是 Local Authentication中的主要元素。
在viewDidLoad() 方法底部加入下面的代碼:
touchIDButton.hidden = true if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) { touchIDButton.hidden = false }
這裡你使用了canEvaluatePolicy(_:error:)方法來查看當前設備是否支 Touch ID 身份驗證。如果支持,則顯示 Touch ID 按鈕;如果不支持,則隱藏按鈕。
在模擬器上編譯運行示例工程,Touch ID按鈕是隱藏的。在支持Touch ID的真機上運行,Touch ID按鈕則會顯示出來。
使用Touch ID
還是在LoginViewController.swift中,用以下代碼替換touchIDLoginAction(_:)方法裡的內容:
@IBAction func touchIDLoginAction(sender: AnyObject) { // 1. if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) { // 2. context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: "Logging in with Touch ID", reply: { (success: Bool, error: NSError! ) -> Void in // 3. dispatch_async(dispatch_get_main_queue(), { if success { self.performSegueWithIdentifier("dismissLogin", sender: self) } if error != nil { var message: NSString var showAlert: Bool // 4. switch(error.code) { case LAError.AuthenticationFailed.rawValue: message = "There was a problem verifying your identity." showAlert = true case LAError.UserCancel.rawValue: message = "You pressed cancel." showAlert = true case LAError.UserFallback.rawValue: message = "You pressed password." showAlert = true default: showAlert = true message = "Touch ID may not be configured" } var alert = UIAlertView() alert.title = "Error" alert.message = message alert.addButtonWithTitle("Darn!") if showAlert { alert.show() } } }) }) } else { // 5. var alert = UIAlertView() alert.title = "Error" alert.message = "Touch ID not available" alert.addButtonWithTitle("Darn!") alert.show() } }
以下是上面代碼的解釋:
再一次使用canEvaluatePolicy(_:error:) 來檢查當前設備是否支Touch ID。
如果該設備支持Touch ID, 使用 evaluatePolicy(_:localizedReason:reply:) 方法來開始驗—也就是讓用戶利用Touch ID進行身份驗證。在驗證執行完畢後,evaluatePolicy(_:localizedReason:reply:) takes一個reply block
在reply block內部, 首先處理成功的情況。默認情況下, 驗證方法在私有線程上運行,所以代碼會跳到主線程以便更新UI。如果驗證成功,則調用segue關閉登錄界面。
如果失敗,用switch語句把每一個可能的失敗條件列舉出來,並設置相應的失敗提示信息展示給用戶。
如果canEvaluatePolicy(_:error:)方法失敗,則展示一個通用的提示框。實際應用中, 應該分析評估並解決返回的失敗錯誤代碼,錯誤碼可能包含以下的信息:
LAErrorTouchIDNotAvailable: 該設備不支持Touch ID。
LAErrorPasscodeNotSet:Touch ID要求的passcode沒啟用。
LAErrorTouchIDNotEnrolled: 沒有存儲指紋信息。
iOS會對LAErrorPasscodeNotSet和LAErrorTouchIDNotEnrolled 做出自己相對應的警告提示框。
在真機上編譯運行,然後用Touch ID測試登錄。
因為LAContext 已經處理了大多數較難的部分,所以實現Touch ID其實是個相對簡單的過程。好的地方是,你可以在一個APP裡保留keychain和Touch ID兩個驗證方法以防用戶的手機不支持Touch ID。
在這個教程的第三部分也就是最後一個部分,我們將會使用1Password 的iOS 8擴展來存儲並獲得登錄信息,以及其他的一些敏感的用戶信息。
用1Password進行控制
Agilebits開發的1Password密碼控制器可以在iOS、OS X、Windows以及Android上運行。它可以把登錄驗證信息,軟件許可證和其他敏感信息存儲在一個庫裡並且用一個PBKDF2-加密的主秘鑰進行鎖定。在這一節中,你會學到怎麼樣利用擴展(extension)把用戶驗證信息存儲在1Password裡,然後學習怎樣獲取這些信息以便驗證用戶。
注意:下面章節學習中,你需要在真機上安裝1Password。
首先,你需要加一些新的圖片,將用於1Password按鈕上。
打開 Images.xcassets. 然後,在你先前下載的資源裡,找到下面的三個文件:
onepassword-button.png
選中所有,然後把三個當成一個整體拖入到 Images.xcassets. 然後,找到這三個文件:
onepassword-button-green.png
再次,選中所有,然後把三個當成一個整體拖入到 Images.xcassets.
打開Main.storyboard,從Object Library裡面拖一個按鈕到Login View Controller Scene裡,位於的Touch ID按鈕下方。在Attributes Inspector裡,調整這個按鈕的屬性為如下值:
將Type設置為Custom
將Title設置為空
將Image設置為onepassword-button
Attributes Inspector應該如下圖所示:
使用Size Inspector調整放置信息和大小信息,如下值:
將X設置為287
將Y設置為426
將Width設置為27
將Height設置為33
你可以比對下圖來檢查大小信息和位置信息是否設置正確:
保持按鈕仍然是被選中狀態,點擊storyboard下方layout條上pin,並且設置按鈕的距離上部空間為21,寬度為27,高度為33:
接下來,點擊layout條中的align,並在Container裡面勾選上Horizontal Center。
還是在Main.storyboard裡,打開 Assistant Editor,當下會自動打開LoginViewController.swift-如果沒有打開,則在跳轉欄中選中這個文件。現在按住Ctrl鍵把按鈕拖向LoginViewController.swift中的其他屬性下方,如下圖所示:
在彈出框裡把名字設置為onepasswordSigninButton,點擊Connect:
這會創建一個IBOutlet,根據其功能是否可用,你將用它來更改1Password按鈕的圖片。
接下來為onepasswordSigninButton添加一個action。按Ctrl健並把該按鈕拖向LoginViewController.swift裡的checkLogin(_:password:)方法上方。
將Connection類型更改為Action。將名稱設置為canUse1Password,且將Arguments設置為Sender,點擊Connect。
編譯運行。如下圖,你會看到一個新的1Password按鈕展現出來,如下所示:
一個靈活的拓展
現在界面展示出來了,你可以開始實現1Password的支持了!
如下展示的那樣,找到GitHub上的1Password 擴展倉庫地址,點擊下載ZIP:
解壓下載下來的文件。打開文件夾並且把OnePasswordExtension.h和OnePasswordExtension.m文件拖到你的工程裡,如下圖:
確保勾選Copy items if needed和TouchMeIn。
回想當初添加Objective-C 文件到Swift工程裡時,Xcode自動提供了一個bridging header頭文件。鑒於最初時已經創建了這樣的一個bridging header頭文件,現在你可以直接將1Password的頭加入那個文件中。
打開TouchMeIn-Bridging-Header.h文件然後加入下面的import:
#import "OnePasswordExtension.h"
打開LoginViewController.swift然後加入下面import到文件的頂端:
import Security
這僅僅簡單地導入了1Password 擴展需要的Security框架。
現在將以下屬性添加至LoginViewController的頂端:
let MyOnePassword = OnePasswordExtension() var has1PasswordLogin: Bool = false
前一個屬性保留了1Password擴展的主類的關聯,後一個屬性追蹤了1Password記錄是否被創建過;一開始設置默認為false因為在最開始運行時肯定沒有創建過。
接下來,使用以下代碼更新viewDidLoad()裡整個if halogen 塊裡的代碼:
if hasLogin { loginButton.setTitle("Login", forState: UIControlState.Normal) loginButton.tag = loginButtonTag createInfoLabel.hidden = true onepasswordSigninButton.enabled = true } else { loginButton.setTitle("Create", forState: UIControlState.Normal) loginButton.tag = createLoginButtonTag createInfoLabel.hidden = false onepasswordSigninButton.enabled = false }
這使得onepasswordSigninButton在初始用戶名和密碼存儲進Keychain之前是無效的。
現在你需要進入1Password擴展裡測試是否安裝了這個iOS app以及其是否可用,如果是就啟用1Password按鈕。
將以下代碼加入viewDidLoad()方法:
onepasswordSigninButton.hidden = true var has1Password = NSUserDefaults.standardUserDefaults().boolForKey("has1PassLogin") if MyOnePassword.isAppExtensionAvailable() { onepasswordSigninButton.hidden = false if has1Password { onepasswordSigninButton.setImage(UIImage(named: "onepassword-button") , forState: .Normal) } else { onepasswordSigninButton.setImage(UIImage(named: "onepassword-button-green") , forState: .Normal) } }
這裡默認是隱藏了onepasswordSigninButton,並且僅當擴展被安裝後才顯示它。接下來將NSUserDefaults裡面key has1PassLogin的值賦值給has1Password來指明1Password記錄是否被創建過。
接下來,當點1Password按鈕時,更改canUse1Password以便向console輸出一個簡單的消息。
@IBAction func canUse1Password(sender: AnyObject) { println("one password") }
在已安裝1Password的模擬器和真機上編譯和運行你的APP。1Password按鈕應該在模擬器上是隱藏的,且在真機上該按鈕應該顯示為綠色。點擊1Password按鈕,以下信息將顯示在console上:
one password
深入1Password強大之處
1Password擴展裡有一些方法你可以使用:
storeLoginForURLString(_:loginDetails:passwordGenerationOptions:forViewController:sender:)方法讓你創建一些登錄驗證信息,findLoginForURLString(_:forViewController:sender:completion:)方法能讓你從1Password庫裡檢索驗證信息。
打開LoginViewController.swift,並添加以下新方法:
func saveLoginTo1Password(sender: AnyObject) { // 1. var newLoginDetails : NSDictionary = [ AppExtensionTitleKey: "Touch Me In", AppExtensionUsernameKey: usernameTextField.text, AppExtensionPasswordKey: passwordTextField.text, AppExtensionNotesKey: "Saved with the TouchMeIn app", AppExtensionSectionTitleKey: "Touch Me In app", ] // 2. var passwordGenerationOptions : NSDictionary = [ AppExtensionGeneratedPasswordMinLengthKey: 6, AppExtensionGeneratedPasswordMaxLengthKey: 10 ] // 3. MyOnePassword.storeLoginForURLString("TouchMeIn.Login", loginDetails: newLoginDetails, passwordGenerationOptions: passwordGenerationOptions, forViewController: self, sender: sender) { (loginDict : [NSObject : AnyObject]!, error : NSError!) -> Void in // 4. if loginDict == nil { if ((Int32)(error.code) != AppExtensionErrorCodeCancelledByUser) { println("Error invoking 1Password App Extension for login: \(error)") } return } // 5. var foundUsername = loginDict["username"] as String var foundPassword = loginDict["password"] as String // 6. if self.checkLogin(foundUsername, password: foundPassword) { self.performSegueWithIdentifier("dismissLogin", sender: self) } else { // 7. var alert = UIAlertView() alert.title = "Error" alert.message = "The info in 1Password is incorrect" alert.addButtonWithTitle("Darn!") alert.show() } // TODO - add NSUserDefaults check } }
以上是代碼,下面是代碼的細節:
創建了一個包含用戶提供的用戶名和密碼的dictionary,也包括1Password擴展需要的keys。當存儲密碼時,這個dictionary會被傳遞給擴展。
添加了1Password用來生成密碼的可選的參數。這裡只是簡單地聲明密碼的最小和最大長度。如果不在這裡配置的話,1Password擴展會提供默認的參數值。
你提供一個字符串--TouchMeIn.Login--來確定保存了的記錄; 你也要傳遞newLoginDetails和passwordGenerationOptions,這是你在步驟1和步驟2創建的兩個字典。
如果上述一切順利,你會收到一個loginDict返回值。這個返回值包含了用戶名和密碼;如果沒收到,你需要在console裡打印一個錯誤然後return。
從loginDict 裡提取出用戶名和密碼。
利用上一步中拿到的用戶名和密碼來調用 checkLogin(_:password:) 方法;如果驗證信息跟存儲在Keychain裡的信息匹配則該用戶登錄成功且關閉login界面;
如果登錄信息不正確,你展示一個帶有錯誤信息的提示框。
注意:1Password使用一個字符串--URLString作為庫裡記錄對應的key。你可以使用任何獨一無二的字符串,盡管使用你的Bundle ID作為這個Key是很誘人的,但是你還是應該盡量創造一個唯一的可讀性強的字符串來當做key,因為這個字符串在1Password app裡是可見的。在你的案例中,你使用了TouchMeIn.Login這個字符串。
現在你應該檢查用戶名是否被存入了NSUserDefaults。如果沒有,你應該保守的編寫代碼,把text field裡面的用戶名值保存下來 ?
在saveLoginTo1Password(_:)裡找到下面的這行代碼:
// TODO - add NSUserDefaults check
然後用下面的代碼替換它:
if NSUserDefaults.standardUserDefaults().objectForKey("username") != nil { NSUserDefaults.standardUserDefaults().setValue(self.usernameTextField.text, forKey: "username") }
在用戶名沒有被保存的情況下,這將把用戶名輸入框裡的值賦值給user defaults裡的key值為username的變量。
在saveLoginTo1Password(_:)方法的下端添加下面的代碼:
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "has1PassLogin") NSUserDefaults.standardUserDefaults().synchronize()
這會更新 has1PassLogin 以便表明該用戶已經創建了一個1Password的記錄。這是一個簡單的方法來避免在1Password庫中創建第二次記錄。後面,你將在試圖保存用戶驗證信息到1Password之前檢 has1PassLogin 。
現在你需要一個方法為你的APP查看已存儲的1Password登錄信息。
在checkLogin(_:password:)上方添加下面的方法:
@IBAction func findLoginFrom1Password(sender: AnyObject) { MyOnePassword.findLoginForURLString( "TouchMeIn.Login", forViewController: self, sender: sender, completion: { (loginDict : [NSObject: AnyObject]!, error : NSError!) -> Void in // 1. if loginDict == nil { if (Int32)(error.code) != AppExtensionErrorCodeCancelledByUser { println("Error invoking 1Password App Extension for find login: \(error)") } return } // 2. if NSUserDefaults.standardUserDefaults().objectForKey("username") == nil { NSUserDefaults.standardUserDefaults().setValue(loginDict[AppExtensionUsernameKey], forKey: "username") NSUserDefaults.standardUserDefaults().synchronize() } // 3. var foundUsername = loginDict["username"] as String var foundPassword = loginDict["password"] as String if self.checkLogin(foundUsername, password: foundPassword) { self.performSegueWithIdentifier("dismissLogin", sender: self) } else { var alert = UIAlertView() alert.title = "Error" alert.message = "The info in 1Password is incorrect" alert.addButtonWithTitle("Darn!") alert.show() } }) }
這個方法使用 1Password的findLoginForURLString(_:forViewController:sender:completion:)方法來查看傳進來URLString值對應的庫中的記錄,在這個例子裡就是TouchMeIn.Login;然後查找成功後會運行completion block,裡面包含了返回的字典。
進一步解析代碼如下:
如果 loginDict 是 nil ,那就是出錯, 你將根據提供的NSError console裡打印錯誤信息,然後return。????
現在你檢查了用戶名是否保存在了NSUserDefaultsIf裡。如果沒有,則根據1Password的返回值保存用戶名。
把得到的值傳遞給 checkLogin(_:password:)方法。如果登錄信息跟Keychain裡存儲的信息匹配,則關閉登錄界面。否則,如果不匹配,給用戶展示一個提示框。
現在你將通過調用saveLoginTo1Password(_:)方法或者findLoginFrom1Password(_:)方法結束canUse1Password(_:)方法的實現。如果第一次運行則調用saveLoginTo1Password(_:)方法,如 has1PassLogin變量表明該用戶曾經有過保存的登錄信息,則調用findLoginFrom1Password(_:)方法。
利用下面的代碼替換canUse1Password的實現:
@IBAction func canUse1Password(sender: AnyObject) { if NSUserDefaults.standardUserDefaults().objectForKey("has1PassLogin") != nil { self.findLoginFrom1Password(self) } else { self.saveLoginTo1Password(self) } }
編譯並運行。點擊1Password按鈕來保存你的登錄信息。1Password的icon會出現在Action表單,點擊展開擴展,擴展的視圖會模態地出現。你可以用系統密碼登錄,或者如果使用的是真機,則可使用Touch ID。1Password然後會展示你的APP的記錄,使用你先前設置好的標識符。
你將會看到如下展示的界面。默認情況下1Password會生成一個密碼。在這個測試當中,確保輸入的驗證信息跟先前用的Keychain的登錄信息一樣。 等到那個結束後,點擊Done按鈕,最後一張截圖展示了稍後編輯記錄時你會看到的-它有庫名、URL以及當創建記錄時你提供的筆記。
現在你已經存儲了登錄,隨後點擊1Password的按鈕會獲取庫的驗證信息,而且當這個登錄信息跟Keychain裡的匹配時會允許登錄。試著再次登錄你將會看到如下界面:
好了-你現在已經完成了1Password擴展支持- 一個強大的方法來增1Password用戶的登錄體驗。
接下來做什麼?
你可以在這裡下載教程的完整示例應。
這個教程裡你所創建的LoginViewController為任何需要管理用戶登錄信息的APP提供了一個出發點。
你也可以添加一個新的view controller,或者改變已有的LoginViewController來允許用戶時不時地改變他們的密碼。這裡Touch ID不是必須的,因為用戶的生理信息可能在整個生命周期中都不會改變多少! 但是,你可以新建一個方法來更新Keychain然後相應的更新1Password;你應該在接受用戶更新密碼之前促使用戶輸入當前的密碼。
跟往常一樣,如果你對這個教程有任何疑問和評價,歡迎加入下面的討論。