给你的 App 添加 1Password 支持

1Password 是来自加拿大开发商 agilebits 的一款跨平台(Windows,Mac,Android,iPhone,iPad)密码管理应用,但是国内的App 添加 1Password 支持的非常的少,国外的更多一些,由于我本人是 1Password 的用户,所以去找了相关资料,要在自己的App中引入1Password 其实很简单,1Password 在 GitHub 上开源了 App 与 1Password 交互的扩展1PasswordExtension,我们把这个扩展集成到 App 的登录逻辑中,就可以使自己的App支持1Password 的密码管理了

本文介绍的是如何在开发中添加1Password的支持,关于1Password的日常使用的可以阅读以下文章:

一个密码的另一种生活:1Password 完全评测

1Password for Mac 使用指南

1Password 这个超赞的密码管理器该怎么用?

导入 1PasswordExtension 项目

1PasswordExtension 支持 CocoaPods 导入,在Podfile 文件里面添加 pod '1PasswordExtension' 即可,扩展同时支持 OC 和 Swift,除了项目中导入1Password 扩展库之外还需要在 iOS 设备里安装最新版本的 1Password1PasswordExtension里面的内容很少,只有三个文件

使用1Password 进行 App 内填充登录

首先添加一个 UIButton 在登录页面,为了使用户容易识别,按钮的图片可以使用1PasswordExtension 扩展里 1Password.xcassets 里面的图片,并且给这个按钮增加一个点击实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@IBAction func findLoginFrom1Password(_ sender:AnyObject) {
// URLString 唯一标识符,用于识别,和后面使用 1Password 保存账号密码对应
OnePasswordExtension.shared().findLogin(forURLString: "https://www.acme.com", for: self, sender: sender, completion: { (loginDictionary, error) in
guard let loginDictionary = loginDictionary else {
if let error = error as NSError?, error.code != AppExtensionErrorCodeCancelledByUser {// 用户取消
print("Error invoking 1Password App Extension for find login: \(String(describing: error))")
}
return
}
// 通过扩展调用 1Password App 从 App 里面获取到 账号和密码,填充到账户和密码输入框
self.usernameTextField.text = loginDictionary[AppExtensionUsernameKey] as? String
self.passwordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
if let generatedOneTimePassword = loginDictionary[AppExtensionTOTPKey] as? String {
// 使用一次性密码(如果存在)填充密码框
self.oneTimePasswordTextField.isHidden = false
self.oneTimePasswordTextField.text = generatedOneTimePassword
// Important: It is recommended that you submit the OTP/TOTP to your validation server as soon as you receive it, otherwise it may expire.
let delayTime: DispatchTime = .now() + DispatchTimeInterval.milliseconds(500)
DispatchQueue.main.asyncAfter(deadline: delayTime) { self.performSegue(withIdentifier: "showThankYouViewController", sender: self)
}
}
// 这里可以直接进行登录请求的操作
})
}

有的时候用户可能并没有安装1Password,我们可以通过isAppExtensionAvailable()方法判断用户是否安装了1Password 从而决定是否显示调用1Password的按钮

1
onepasswordButton.isHidden = (false == OnePasswordExtension.shared().isAppExtensionAvailable())

为了使这个方法能够准确判断以及和1Password 之间的跳转,我们需要在info.plist 里面增加一个 URL schemeorg-appextension-feature-password-management

使用 1Password 保存注册信息

在App的注册页面可以直接调用1Password生成随机密码以及保存注册的账号和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@IBAction func saveLoginTo1Password(_ sender:AnyObject) {
// 构建注册的相关信息
let newLoginDetails:[String: Any] = [
AppExtensionTitleKey: "ACME",
// 账号
AppExtensionUsernameKey: usernameTextField.text ?? "",
// 密码
AppExtensionPasswordKey: passwordTextField.text ?? "",
AppExtensionNotesKey: "Saved with the ACME app",
AppExtensionSectionTitleKey: "ACME Browser",
// 扩展字段,可以自己添加键值,其他的注册信息(用户的姓名、性别等)
AppExtensionFieldsKey: [
"firstname" : firstnameTextField.text ?? "",
"lastname" : lastnameTextField.text ?? ""
// Add as many string fields as you please.
]
]
// 设置密码生成规则,可选的,如果对密码长度,符号和数字有严格的规则可以使用这个
let passwordGenerationOptions:[String: AnyObject] = [
// 密码最小长度(至少4位)
AppExtensionGeneratedPasswordMinLengthKey: (8 as NSNumber),
// 密码最大长度(最多50位)
AppExtensionGeneratedPasswordMaxLengthKey: (30 as NSNumber),
// 密码中是否必须包含数字
AppExtensionGeneratedPasswordRequireDigitsKey: (true as NSNumber),
// 密码是否包含必须特殊字符
AppExtensionGeneratedPasswordRequireSymbolsKey: (true as NSNumber),
// 需要排除的字符
AppExtensionGeneratedPasswordForbiddenCharactersKey: "!@#$%/0lIO" as NSString
]
// 调用 扩展 储存注册信息
// URLString 和登录场景中使用URLString是一致的,URLString作为唯一标识使用户在下次需要登录时快速找到1Password为用户保存的登录信息
// 如果APP没有网站可以使用budndlId,像这样:app:// bundleIdentifier(例如app://com.acme.awesome-app)
OnePasswordExtension.shared().storeLogin(forURLString: "https://www.acme.com", loginDetails: newLoginDetails, passwordGenerationOptions: passwordGenerationOptions, for: self, sender: sender) { (loginDictionary, error) in
guard let loginDictionary = loginDictionary else {
if let error = error as NSError?, error.code != AppExtensionErrorCodeCancelledByUser {
print("Error invoking 1Password App Extension for find login: \(String(describing: error))")
}
return
}
// 1Password 保存注册信息成功之后,会返回注册信息,可以在这里使用注册信息进入App的注册流程
self.usernameTextField.text = loginDictionary[AppExtensionUsernameKey] as? String
self.passwordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
// 其他的注册信息
if let returnedLoginDictionary = loginDictionary[AppExtensionReturnedFieldsKey] as? [String: Any] {
self.firstnameTextField.text = returnedLoginDictionary["firstname"] as? String
self.lastnameTextField.text = returnedLoginDictionary["lastname"] as? String
}
}
}

使用 1Password 修改密码

1Password 既然可以用来登录和注册用户信息,当然也可以修改和更新密码,更新成功之后旧的和新生成的密码都会返回用于UI以及完成密码更改的过程,如果在1Password中找不到匹配的登录账号,则会提示用户保存新的账号信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@IBAction func changePasswordIn1Password(_ sender:AnyObject) {
guard let changedPassword = freshPasswordTextField.text,
let oldPassword = oldPasswordTextField.text,
let confirmationPassword = confirmPasswordTextField.text else {
return
}
// 验证新旧密码是否相同
if (oldPassword.count > 0 && oldPassword == changedPassword) {
showChangePasswordFailedAlertWithMessage(message: "The old and the new password must not be the same")
return
}
// 验证新密码和验证新密码师傅一致
if (changedPassword.count > 0 && changedPassword != confirmationPassword) {
showChangePasswordFailedAlertWithMessage(message: "The new passwords and the confirmation password must match")
return
}
// 构建新的用户信息
let newLoginDetails:[String : Any] = [
AppExtensionTitleKey: "ACME", // Optional, used for the third schenario only
AppExtensionUsernameKey: "aUsername", // Optional, used for the third schenario only
// 新密码
AppExtensionPasswordKey: changedPassword,
// 旧密码
AppExtensionOldPasswordKey: oldPassword,
AppExtensionNotesKey: "Saved with the ACME app", // Optional, used for the third schenario only
]
// 设置密码生成规则 同 保存注册信息
let passwordGenerationOptions:[String : AnyObject] = [
AppExtensionGeneratedPasswordMinLengthKey: (8 as NSNumber),
AppExtensionGeneratedPasswordMaxLengthKey: (30 as NSNumber),
AppExtensionGeneratedPasswordRequireDigitsKey: (true as NSNumber),
AppExtensionGeneratedPasswordRequireSymbolsKey: (true as NSNumber),
AppExtensionGeneratedPasswordForbiddenCharactersKey: "!@#$%/0lIO" as NSString
]
// 调用扩展信息,更新密码
OnePasswordExtension.shared().changePasswordForLogin(forURLString: "https://www.acme.com", loginDetails: newLoginDetails, passwordGenerationOptions: passwordGenerationOptions, for: self, sender: sender) { (loginDictionary, error) in
guard let loginDictionary = loginDictionary else {
if let error = error as NSError?, error.code != AppExtensionErrorCodeCancelledByUser {
print("Error invoking 1Password App Extension for find login: \(String(describing: error))")
}
return
}
// 更新成功后返回新旧密码,这里完成 App 内部的更新密码操作
self.oldPasswordTextField.text = loginDictionary[AppExtensionOldPasswordKey] as? String
self.freshPasswordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
self.confirmPasswordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
}
}

使用 1Password 填充 WebView

使用1Password 不仅可以填充原生App页面内的账号和密码,可以用在 UIWebViews 和 WKWebViews里面,只需在网页视图的 UIViewController 中添加一个调用1Password 扩展的按钮,使用起来和原生App调用一样,1Password会自己找到账号和密码框,并且自动填充

1
2
3
4
5
6
7
8
@IBAction func fillUsing1Password(_ sender: AnyObject) {
OnePasswordExtension.shared().fillItem(intoWebView: webView, for: self, sender: sender, showOnlyLogins: false) { (success, error) -> Void in
if success == false {
print("Failed to fill into webview: <\(String(describing: error))>")
}
}
}

上面就是 App内添加1Password支持的核心代码,我基本上就是翻译了1PasswordExtension里面 README.md的内容,README.md里面介绍的非常的详细,还可以下载官方的 Demo 查看

以上です