前提:所有所有代码都是基于iOS9及以上。
最近app的登录部分需要重新梳理,而且产品爸爸提了些要求。代码已经提测空出一点时间自己整理一下。需要实现的需求如下:
- 首次登录使用密码或第三方登录
- 登录完成后开启生物验证
- 对已开启生物验证的用户可免密登录
- 可使用多设备登录
- 用户token密文传输(RSA加密)
- 可在无网络的情况下个人信息的展示
需求整理完之后我想说产品你(n)真(m)棒(m)呀(l)。登录模块一次到位也好,我们对需求进一步分析。涉及到知识点:
- 接入三方登录(友盟等平台有成熟的方案,不是本文讨论的重点)
- touchID、faceID
- keychain
- RSA加密
- 本地缓存,安全起见我选择NSKeyedArchiver(归档)
明确了目标就开始搞,我打算先一部分一部分搞,最后在把逻辑串起来,这操作也完全符合面向对象的思路~
接入三方登录
推荐友盟:https://www.umeng.com/
简单介绍一下流程:
- 三方登录成功后会返回一个uid、access token;
- 若已经绑定过手机号则返回用户token;
- 若首次登录则需要绑定手机号,绑定成功后返回用户token;
- 往后的流程和密码登录相同,开启生物验证,完成登录;
touchID、faceID
FaceID和TouchID本身代码很简单,使用起来也很容易,主要是逻辑的嵌套相对复杂。
代码实现篇幅较长,详细的介绍在这ios swift版touchID&faceID
- 这就放一点核心代码
let authContent = LAContext()
//如果为空不展示输入密码的按钮
authContent.localizedFallbackTitle = strTips
var error: NSError?
if authContent.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics, error: &error)
{ authContent.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: strTips) {(success,error) in
if success {
//evaluatedPolicyDomainState 只有生物验证成功才会有值
if let _ = authContent.evaluatedPolicyDomainState {
//如果不放在主线程回调可能会有5-6s的延迟
DispatchQueue.main.async {
print("验证成功")
block(.success, error)
}
}else{
DispatchQueue.main.async {
print("设备密码输入正确")
}
}
}else{
guard let laError = error as? LAError else{
DispatchQueue.main.async {
print("touchID不可用")
block(.touchidNotAvailable,error)
}
return
}
switch laError.code {
case .authenticationFailed:
DispatchQueue.main.async {
print("连续三次输入错误,身份验证失败")
block(.failed, error)
}
。。。。。。还有很多其他类型
keychain
首先在xCode中打开对应选项。
本方案中只涉及到了keychain的基本使用,所以在这里就不多介绍。在代码中有一个keychain的工具类可以直接使用。
NSKeyedArchiver
这个也是iOS中比较基础的数据本地化方案,因为Apple对数据会做加密然后写成文件,安全性相对较高,个人信息推荐使用这种方式来缓存。(注:有些极少数用户会选择越狱,你保存的信息就会成明文信息),
- 使用有一点需要注意,所缓存的对象一定要遵守NSCoding协议,且该协议无法在extension中使用.如果大量使用归档建议使用runtime来进行归解档
public var userId:Int?
func encode(with aCoder: NSCoder) {
aCoder.encode(self.userId, forKey: "userId")
}
required init(coder aDecoder: NSCoder) {
self.userId = aDecoder.decodeInteger(forKey: "userId")
}
在iOS11以后API有个比较大的修改需要适配。而且使用了新的API归档就必须使用新的API解档。一定要注意这个坑我补了好久,放出最基本的代码
//归档
if #available(iOS 11.0, *) {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: objc, requiringSecureCoding: false)
do {
try data.write(to: URL(fileURLWithPath: path))
} catch {
assert(true, "无法写入path")
return false
}
} catch {
assert(true, "无法生成归档数据")
return false
}
}else{
return NSKeyedArchiver.archiveRootObject(objc, toFile: path)
}
//解档
if #available(iOS 11.0, *) {
do{
let data = try Data.init(contentsOf: URL(fileURLWithPath: path))
do{
return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as AnyObject
} catch {
assert(true, "用户数据解档失败")
}
} catch {
assert(true, "用户数据解档路径错误")
}
}else{
return NSKeyedUnarchiver.unarchiveObject(withFile: path) as AnyObject?
}
RSA加密
由于涉及到了密码登录,按要求密码不能明文传输。最终选择使用RSA来进行加解密。现在很多APP都是手机验证码登录应该就不涉及到加密问题,就可以跳过了,按需索取吧。
本人试过很多版本,也尝试着去了解RSA这个非对称加密,在iOS系统中用Security库来实现加密算法。
其实只要了解该加密算法是使用一对秘钥,公钥是用来加密的,私钥是用来解密的就好了。
只要使用pod来引入就可以了,该库也是使用swift编写的。
pod 'SwiftyRSA'
依旧是核心代码,没什么好解释的.
- 需要注意秘钥字符串必须使用base64编码。
let publicKeyRef = try PublicKey(base64Encoded: publicKey)
let clear = try ClearMessage(string:passWord, using: .utf8)
//PKCS1 / PKCS8 与秘钥生成时相同
let encrypted = try clear.encrypted(with: publicKeyRef, padding: .PKCS1)
let RSAPassWord = encrypted.base64String
这是工具类的准备,逻辑实现由于篇幅的原因就新开一篇了。