TextWatcher 實時查詢歷史賬號;username → password 到 SP;TextWatcher 讀取 SP 所有 Key 作為提示數據源;afterTextChanged 回調讀取密碼自動填充。<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView android:text="簡易登錄 DEMO"
android:textSize="25sp"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout android:layout_width="250dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="16dp">
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView android:text="用戶名:"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:textSize="15sp"/>
<AutoCompleteTextView android:id="@+id/cardNumAuto"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView android:text="密碼:"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:textSize="15sp"/>
<EditText android:id="@+id/passwordET"
android:layout_width="match_parent"
android:layout_height="40dp"
android:inputType="textPassword"/>
</LinearLayout>
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<CheckBox android:id="@+id/savePasswordCB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:text="記住密碼"/>
<Button android:id="@+id/logBT"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:text="登錄"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
public class RememberPwdActivity extends Activity {
private AutoCompleteTextView cardNumAuto;
private EditText passwordET;
private CheckBox savePasswordCB;
private Button logBT;
private SharedPreferences sp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
cardNumAuto = findViewById(R.id.cardNumAuto);
passwordET = findViewById(R.id.passwordET);
savePasswordCB = findViewById(R.id.savePasswordCB);
logBT = findViewById(R.id.logBT);
sp = getSharedPreferences("passwordFile", MODE_PRIVATE);
savePasswordCB.setChecked(true); // 默認勾選
cardNumAuto.setThreshold(1); // 輸入 1 字符觸發提示
passwordET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
// 1. 用戶名輸入監聽:動態加載歷史賬號下拉
cardNumAuto.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
// 讀取 SP 所有鍵作為提示源
String[] allUsers = sp.getAll().keySet().toArray(new String[0]);
ArrayAdapter<String> adapter = new ArrayAdapter<>(
RememberPwdActivity.this,
android.R.layout.simple_dropdown_item_1line,
allUsers
);
cardNumAuto.setAdapter(adapter);
}
@Override public void afterTextChanged(Editable s) {
// 選中用戶名後自動填充密碼
String username = cardNumAuto.getText().toString();
passwordET.setText(sp.getString(username, ""));
}
});
// 2. 登錄按鈕點擊
logBT.setOnClickListener(v -> {
String username = cardNumAuto.getText().toString().trim();
String password = passwordET.getText().toString().trim();
// 演示用固定賬號密碼
if (!"test".equals(username) || !"test".equals(password)) {
Toast.makeText(this, "密碼錯誤,請重新輸入", Toast.LENGTH_SHORT).show();
return;
}
// 登錄成功,記住密碼
if (savePasswordCB.isChecked()) {
sp.edit().putString(username, password).apply(); // apply() 異步寫入
}
Toast.makeText(this, "登錄成功", Toast.LENGTH_SHORT).show();
// startActivity(new Intent(this, MainActivity.class)); // 跳轉主頁
});
}
}
文件位置:/data/data/包名/shared_prefs/passwordFile.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="test">test</string>
<string name="test2">test</string>
<string name="test1">test</string>
</map>
setThreshold(1):輸入 1 字符即觸發過濾提示;setAdapter():動態更新適配器數據源;TextWatcher.afterTextChanged:在用戶選擇下拉項後觸發,適合做密碼回填。// 隱藏密碼(默認)
passwordET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
// 顯示明文
passwordET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
| 風險 | 解決方案 |
|---|---|
| 明文存儲密碼 | AES 加密存儲,Key 存入 Android Keystore(硬件隔離) |
| Root 設備讀取 SP | 不存儲密碼,存儲 Token(JWT/Session ID),過期自動失效 |
| 數據傳輸裸奔 | 全站 HTTPS,密碼傳輸前端加密(RSA 公鑰) |
| 暴力破解 | 登錄失敗計數 + 驗證碼 + 賬號鎖定策略 |
// 加密工具類
public class EncryptUtils {
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "login_password_key";
// 初始化 Key
public static void initKey(Context context) throws Exception {
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // 需用戶認證(指紋/密碼)
.build();
KeyGenerator kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
kg.init(spec);
kg.generateKey();
}
// 加密
public static String encrypt(String plaintext) throws Exception {
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
ks.load(null);
KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) ks.getEntry(KEY_ALIAS, null);
SecretKey key = entry.getSecretKey();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = cipher.getIV();
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// 合併 IV + 密文,Base64 編碼存儲
byte[] combined = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
return Base64.encodeToString(combined, Base64.NO_WRAP);
}
// 解密
public static String decrypt(String combinedBase64) throws Exception {
byte[] combined = Base64.decode(combinedBase64, Base64.NO_WRAP);
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
ks.load(null);
KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) ks.getEntry(KEY_ALIAS, null);
SecretKey key = entry.getSecretKey();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
byte[] iv = Arrays.copyOfRange(combined, 0, 12); // GCM IV 12 bytes
byte[] ciphertext = Arrays.copyOfRange(combined, 12, combined.length);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
}
}
「記住密碼」看似簡單,實則涉及 UI 交互、數據持久化、安全加固多個維度。Demo 級代碼可用明文 SP + AutoCompleteTextView 快速實現,但上線項目務必:不存明文密碼、改存 Token、啟用 Keystore 加密、全鏈路 HTTPS。掌握這套完整鏈路,才能交付既好用又安全的登錄體驗。