首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Android 登錄頁「記住密碼」功能完整實現:SharedPreferences + AutoCompleteTextView 實戰

     編輯:Java 相關知識

設計思路

  • 數據持久化:SharedPreferences 輕量級 Key-Value 存儲;
  • 用戶名下拉提示:AutoCompleteTextView + TextWatcher 實時查詢歷史賬號;
  • 密碼自動填充:選中用戶名後,從 SP 讀取對應密碼回填;
  • 安全警示:明文存儲密碼極不安全(Root 設備可讀),生產環境必須加密(AES/加鹽 Hash/Keystore)。

核心功能流程

  1. 默認勾選「記住密碼」;
  2. 登錄成功 → 若勾選,保存 username → password 到 SP;
  3. 輸入用戶名時,TextWatcher 讀取 SP 所有 Key 作為提示數據源;
  4. 選中用戶名 → 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>

Activity 邏輯

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)); // 跳轉主頁
        });
    }
}

關鍵技術點解析

SharedPreferences 存儲結構

文件位置:/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>

AutoCompleteTextView 關鍵設置

  • 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。掌握這套完整鏈路,才能交付既好用又安全的登錄體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved