添加记住密码功能
MoMo Lv5

操作

编写界面交互代码

res/layout/activity_login.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<CheckBox
android:id="@+id/remember_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:minHeight="36dp"
android:layout_gravity="right"
android:text="@string/text_remember_me"
android:onClick="@{viewModel::onClickRememberPassword}"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_password"/>

image

记住密码框架封装

com/danale/edge/coreimpl/usersdk/PvStoreImpl.kt

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
package com.danale.edge.coreimpl.usersdk

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.danale.edge.foundation.log.Logger
import com.danale.edge.usersdk.di.PvStoreRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class PvStoreImpl @Inject constructor(
@ApplicationContext private val context: Context
) : PvStoreRepository {

companion object {
const val TAG = "PvStoreImpl"
const val SP_NAME = "com.danale.edge.coreimpl.usersdk.PvStoreImpl"
}

// 使用EncryptedSharedPreferences通过 Security 库以更安全的方式修改用户的一组共享偏好设置
private fun getEncryptedSp(): SharedPreferences {
val mainKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

return EncryptedSharedPreferences.create(
context,
SP_NAME,
mainKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

@Synchronized
override fun store(key: String, value: String) {
with(getEncryptedSp().edit()) {
putString(key, value)
apply()
}
}

@Synchronized
override fun read(key: String): String? {
return getEncryptedSp().getString(key, null)
}
}

添加点击checkbox时的绑定函数

获取checkbox的选中状态

com/danale/edge/ui/account/LoginViewModel.kt

1
2
3
4
5
6
7
8
private var acceptRememberMeState: Boolean = false

@Suppress("UNUSED_PARAMETER")
fun onClickRememberPassword(view: View) {
(view as? CheckBox)?.let {
acceptRememberMeState = it.isChecked
}
}

在登录函数中添加逻辑

如果checkbox被选中,就将用户名和密码存起来;否则置空

com/danale/edge/ui/account/LoginViewModel.kt

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
// 初始化用户名和密码
private var userName: String? = pvStoreImpl.read("userName")
private var password: String? = pvStoreImpl.read("password")


fun performLogin(licenseState: Boolean = acceptLicenseState) {
// 验证是否同意隐私条款
// ...

viewModelScope.launch {
// 验证用户名和密码的正确性
val success = withContext(Dispatchers.IO) {
try { } catch (ex: Exception) { }
}

// 验证成功
if (success) {
// 如果checkbox被选中则记住密码
if(acceptRememberMeState){
pvStoreImpl.store("userName", userName!!)
pvStoreImpl.store("password", password!!)
}else{
// 否则置空
pvStoreImpl.store("userName", "")
pvStoreImpl.store("password", "")
}
// 页面跳转
navigationRequiredEvent.postValue( )
}

// 用户名改变时监听状态
fun afterUsernameChanged(editable: Editable) {
userName = editable.toString()
updateButtonEnable()
}

// 密码改变时监听状态
fun afterPasswordChanged(editable: Editable) {
password = editable.toString()
updateButtonEnable()
}

在初始化时显示存储的用户信息

com/danale/edge/ui/account/LoginActivity.kt

1
2
3
4
5
6
7
if(pvStoreImpl.read("userName") != "" && pvStoreImpl.read("password") != ""){
dataBinding.editUserName.setText(pvStoreImpl.read("userName"))
dataBinding.editPassword.setText(pvStoreImpl.read("password"))
dataBinding.rememberPassword.isChecked = true

viewModel.updateButtonEnable()
}

如果pvStoreImpl中有用户信息,将用户名和密码显示在editText内,同时设置checkbox的选中状态为true

updateButtonEnable用来更新登录按钮的状态,如果存在用户名和密码有为空的情况,则设置登录按钮不可点击状态

1
2
3
4
fun updateButtonEnable() {
val isEmail = observableIsEmailUserName.get()
buttonEnable.set(!TextUtils.isEmpty(userName) && !TextUtils.isEmpty(password) && !isEmail)
}

结果

用到的库

Security 库

Security 库版本 1.1.0

API 参考文档,可以安全地管理密钥并对文件和 sharedpreferences 进行加密
androidx.security.crypto

Security 库使用一个由两部分组成的密钥管理系统:

  • 包含一个或多个密钥的密钥集,用于对文件或共享偏好设置数据进行加密。密钥集本身存储在 SharedPreferences 中。

  • 用于加密所有密钥集的主 (master) 密钥。此密钥使用 Android 密钥库系统进行存储。

  • 库中包含的类

    Security 库包含以下类,旨在提供更安全的静态数据机制:

    • EncryptedFile

      提供 FileInputStreamFileOutputStream 的自定义实现,为您的应用赋予更安全的流式读写操作。为了提供安全的文件流读写操作,Security 库使用了流式 AEAD(带关联数据的认证加密)基元。如需详细了解该基元,请参阅 GitHub 上的 Tink 库文档

    • EncryptedSharedPreferences

      封装 SharedPreferences 类,并使用双重方案方法自动加密密钥和值:

      密钥使用确定性加密算法进行加密,这样便可以加密并正确查找密钥。

      使用 AES-256 GCM 加密,并且具有不确定性。

读取文件

使用 EncryptedFile 通过 Security 库以更安全的方式读取文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val mainKey = MasterKey.Builder(applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

val fileToRead = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(applicationContext,
File(DIRECTORY, fileToRead),
mainKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

val inputStream = encryptedFile.openFileInput()
val byteArrayOutputStream = ByteArrayOutputStream()
var nextByte: Int = inputStream.read()
while (nextByte != -1) {
byteArrayOutputStream.write(nextByte)
nextByte = inputStream.read()
}

val plaintext: ByteArray = byteArrayOutputStream.toByteArray()

写入文件

使用 EncryptedFile 通过 Security 库以更安全的方式写入文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val mainKey = MasterKey.Builder(applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

// 使用此名称创建文件,或替换具有相同名称的现有文件。 请注意,文件名不能包含路径分隔符。
val fileToWrite = File(DIRECTORY, "my_sensitive_data.txt")
val encryptedFile = EncryptedFile.Builder(applicationContext,
fileToWrite,
mainKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

// 使用 openFileOutput 之前文件不能存在
if (fileToWrite.exists()) {
fileToWrite.delete()
}

val fileContent = "MY SUPER-SECRET INFORMATION".toByteArray(StandardCharsets.UTF_8))
encryptedFile.openFileOutput().apply {
write(fileContent)
flush()
close()
}

修改共享偏好设置

使用 EncryptedSharedPreferences 通过 Security 库以更安全的方式修改用户的一组共享偏好设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val context = applicationContext
val mainKey = MasterKey.Builder(applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

val sharedPreferences = EncryptedSharedPreferences.create(
applicationContext,
FILE_NAME,
mainKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

with (sharedPreferences.edit()) {
// 修改共享偏好设置
apply()
}

MasterKey.Builder

用于生成 MasterKey

Summary

Public constructors
Builder(context: Context) 使用默认别名 MasterKeyDEFAULT_MASTER_KEY_ALIAS创建一个构建器
Builder(context: Context, keyAlias: String) .MasterKey 创建构建器
Public functions
MasterKey build()
MasterKey.Builder @RequiresApi(value = Build.VERSION_CODES.M)setKeyGenParameterSpec(keyGenParameterSpec: KeyGenParameterSpec)
设置自定义用作主密钥的基础KeyGenParameterSpec
MasterKey.Builder setKeyScheme(keyScheme: MasterKey.KeyScheme)
设置用于主密钥的KeyScheme
MasterKey.Builder setRequestStrongBoxBacked(requestStrongBoxBacked: Boolean)
设置是否请求此密钥是强框支持的
MasterKey.Builder setUserAuthenticationRequired( authenticationRequired: Boolean, userAuthenticationValidityDurationSeconds: @IntRange(from = 1) Int)
设置构建的主密钥在解锁之前应要求用户进行身份验证,可能使用 androidx.biometric 库,并且密钥应在提供的持续时间内保持解锁状态.setKeyScheme
MasterKey.Builder setUserAuthenticationRequired(authenticationRequired: Boolean)
设置构建的主密钥在解锁之前应要求用户进行身份验证,可能使用 androidx.biometric library.setKeyScheme

Public constructors

Builder

1
Builder(context: Context)

使用默认别名 .MasterKeyDEFAULT_MASTER_KEY_ALIAS 创建一个构建器

Parameters
context: Context 与此主密钥一起使用的上下文

Builder

1
Builder(context: Context, keyAlias: String)

.MasterKey 创建构建器

Parameters
context: Context 与此主密钥一起使用的上下文

Public functions

build

1
fun build(): MasterKey

builder.MasterKey构建

Returns
MasterKey 主key
Throws
java.security.GeneralSecurityException: java.security.GeneralSecurityException
java.io.IOException: java.io.IOException

setKeyGenParameterSpec

1
2
@RequiresApi(value = Build.VERSION_CODES.M)
fun setKeyGenParameterSpec(keyGenParameterSpec: KeyGenParameterSpec): MasterKey.Builder

设置自定义用作主密钥的基础。注意:应使用此方法或设置用于构建主密钥的参数。在另一个函数之后调用任一函数将抛出 .KeyGenParameterSpecsetKeySchemeIllegalArgumentException

Parameters
keyGenParameterSpec: KeyGenParameterSpec 要使用的关键规范。
Returns
MasterKey.Builder 构建器

setKeyScheme

1
fun setKeyScheme(keyScheme: MasterKey.KeyScheme): MasterKey.Builder

设置要用于主密钥的 a。这使用与提供的关联的默认值。注意:应使用此方法或设置用于构建主密钥的参数。在另一个函数之后调用任一函数将抛出KeyScheme``KeyGenParameterSpec``KeyScheme``setKeyGenParameterSpec``IllegalArgumentException

Parameters
keyScheme: MasterKey.KeyScheme The KeyScheme to use.
Returns
MasterKey.Builder This builder.

setRequestStrongBoxBacked

1
fun setRequestStrongBoxBacked(requestStrongBoxBacked: Boolean): MasterKey.Builder

设置是否请求此密钥是强框支持的。此设置仅适用于以上版本,且仅适用于支持 Strongbox.P 的设备

Parameters
requestStrongBoxBacked: Boolean Whether to request to use strongbox
Returns
MasterKey.Builder This builder.

setUserAuthenticationRequired

1
2
3
4
fun setUserAuthenticationRequired(
authenticationRequired: Boolean,
userAuthenticationValidityDurationSeconds: @IntRange(from = 1) Int
): MasterKey.Builder

与 一起使用时,设置构建的主密钥在解锁之前应要求用户进行身份验证,可能使用 androidx.biometric 库,并且密钥应在提供的持续时间内保持解锁状态setKeyScheme

Parameters
authenticationRequired: Boolean Whether user authentication should be required to use the key.
userAuthenticationValidityDurationSeconds: @IntRange(from = 1) Int Duration in seconds that the key should remain unlocked following user authentication.
Returns
MasterKey.Builder This builder.

setUserAuthenticationRequired

1
fun setUserAuthenticationRequired(authenticationRequired: Boolean): MasterKey.Builder

与 一起使用时,设置构建的主密钥在解锁之前应要求用户进行身份验证,可能使用 androidx.biometric 库。该方法设置key的有效期为.setKeyScheme getDefaultAuthenticationValidityDurationSeconds

Parameters
authenticationRequired: Boolean 是否需要用户身份验证才能使用密钥
Returns
MasterKey.Builder 构造器

其他

指纹登录

安全样本/生物识别登录科特林在大师 ·安卓/安全示例 (github.com)

Powered by Hexo & Theme Keep
Unique Visitor Page View