1. 程式人生 > >Android登入記住密碼,AES加密儲存密碼

Android登入記住密碼,AES加密儲存密碼

Android登入記住密碼,最常見的方式是用SharedPreferences。

SharedPreference是Android提供的一種輕量級的資料儲存方式,主要用來儲存一些簡單的配置資訊,例如,預設歡迎語,登入使用者名稱和密碼等。其以鍵值對的方式儲存,使得我們能很方便進行讀取和存入。

文章中的記住密碼功能,也是用的SharedPreference實現的,其中儲存的密碼用AES演算法加密。不多說,頁面如下圖:


先來看佈局檔案,佈局中有很多string資源和drawable資源的引用,可在完整程式碼檔案中找到。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/padding1"
    android:gravity="center"
    android:orientation="vertical"
    android:background="@color/login_bg"
    tools:context="com.example.remenberpassword.MainActivity" >
    
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:orientation="vertical">

        <LinearLayout
	        android:layout_width="fill_parent"
	        android:layout_height="wrap_content"
	        android:orientation="horizontal"
	        android:padding="@dimen/padding2"
	        android:gravity="center_vertical">

            <ImageView
                android:id="@+id/img_username"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:src="@drawable/username" />

            <EditText
                android:id="@+id/edit_username"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginLeft="@dimen/margin1"
                android:layout_weight="7"
                android:hint="@string/username"
                android:background="@null"/>
	        
	    </LinearLayout>
    
        <View 
            android:layout_width="fill_parent"
            android:layout_height="1dp"
            android:background="@color/gainsboro"/>
        
        <LinearLayout
	        android:layout_width="fill_parent"
	        android:layout_height="wrap_content"
	        android:orientation="horizontal"
	        android:padding="@dimen/padding2"
	        android:gravity="center_vertical">

            <ImageView
                android:id="@+id/img_password"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:src="@drawable/password" />

            <EditText
                android:id="@+id/edit_password"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="7"
                android:layout_marginLeft="@dimen/margin1"
                android:inputType="textPassword"
                android:hint="@string/password"
                android:background="@null"/>
            
	    </LinearLayout> 
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="@dimen/margin1"
        android:gravity="center_vertical">
        
	    <CheckBox
	        android:id="@+id/check_remenber_password"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:textColor="@color/white"
	        android:checked="true"
	        android:text="@string/remember_password" />
    </LinearLayout>

    <Button
        android:id="@+id/btn_login"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/margin1"
        android:textSize="@dimen/btn_textsize1"
        android:textColor="@color/white"
        android:background="@drawable/btn_selector"
        android:text="@string/login" />

</LinearLayout>
下面是Activity程式碼。
package com.example.remenberpassword;

import com.example.common.CommonUtils;
import com.example.common.EncrypAES;
import com.example.common.User;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;

public class MainActivity extends ActionBarActivity {
	/**
	 * 用了儲存使用者名稱和密碼的檔名,檔案是xml檔案,但是不用加字尾,系統會自動加上。。
	 */
	private static final String USER_INFO="User_Info";
	
	/**
	 * 檔案中記錄使用者名稱的關鍵字。
	 */
	private static final String USER_NAME="User_Name";
	
	/**
	 * 檔案中記錄密碼的關鍵字。
	 */
	private static final String PASSWORD="Password";
	
	/**
	 * 檔案中記錄是否選擇記住密碼功能的關鍵字
	 */
	private static final String IS_REMENBER_PASSWORD="Is_Remenber_Password";
	
	private User mUser;
	
	private EditText editUsername;
	private EditText editPassword;
	private CheckBox checkRememberPassword;
	private Button btnLogin;
	private SharedPreferences mSharedPreferences;
	
	/**
	 *對使用者名稱和密碼進行加解密 
	 */
	private EncrypAES mAes;
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        
        InitData();
        InitWidget();
    }

	public void InitData(){
    	mUser=new User();
    	
    	//取得儲存使用者名稱和密碼的xml檔案,如果檔案不存在,系統會自動建立
    	mSharedPreferences=getSharedPreferences(USER_INFO, Context.MODE_PRIVATE);
    	
		mAes=new EncrypAES();
	}
    
    public void InitWidget(){
    	editUsername=(EditText)findViewById(R.id.edit_username);
    	editPassword=(EditText)findViewById(R.id.edit_password);
    	checkRememberPassword=(CheckBox)findViewById(R.id.check_remenber_password);
    	btnLogin=(Button)findViewById(R.id.btn_login);
    	
    	//如果選中了記住密碼,則從記住的密碼中獲取使用者名稱和密碼
    	if(mSharedPreferences.getBoolean(IS_REMENBER_PASSWORD, true)){
    		editUsername.setText(mSharedPreferences.getString(USER_NAME, ""));
    		
    		//對讀取到的密碼進行解密 
    		String password=mSharedPreferences.getString(PASSWORD, "");
    		if(0!=password.length()){
    			password=mAes.DecryptorString(password);
    		}
    		editPassword.setText(password);
    	}
    	
    	//記住密碼CheckBox監聽函式
    	checkRememberPassword.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			@Override
			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
				mSharedPreferences.edit().putBoolean(IS_REMENBER_PASSWORD, isChecked).commit();
			}
		});
    	
    	//登入按鈕監聽函式
    	btnLogin.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				//判斷是否開啟網路連線
				if(!CommonUtils.CheckNetwork(MainActivity.this)){
					CommonUtils.ShowToast(MainActivity.this, R.string.network_hint);
					return ;
				}
				
				if(!CheckUserInfoInput()){
					return ;
				}
				
				savaUserNameAndPassword(mUser);
				
				Intent intent=new Intent(MainActivity.this,SecondActivity.class);
				startActivity(intent);
			}
		});
    }
    
    
    /**
     * 檢查使用者名稱和密碼輸入
     * @return
     */
    public boolean CheckUserInfoInput(){
    	String strUserName;
    	String strPassword;
    	
    	strUserName=editUsername.getText().toString();
    	strPassword=editPassword.getText().toString();
    	if(0==strUserName.length() | 0==strPassword.length()){
    		CommonUtils.ShowToast(MainActivity.this,R.string.username_password_inputhint);
    		return false;
    	}
    	
    	//設定使用者名稱和密碼
    	mUser.setUserName(strUserName);
    	mUser.setPassword(strPassword);

    	return true;
    }

	/**
	 * 如果選中了“記住密碼”功能,則儲存使用者名稱和密碼
	 * @param user 包含了使用者名稱和密碼
	 */
	public void savaUserNameAndPassword(User user){
		if(checkRememberPassword.isChecked()){
			Editor editor=mSharedPreferences.edit();
			editor.putBoolean(IS_REMENBER_PASSWORD, true);
			editor.putString(USER_NAME, user.getUserName());
			
			//對密碼進行加密儲存
			String password=mAes.EncryptorString(user.getPassword());
			editor.putString(PASSWORD, password);
			editor.commit();
		}
	}
 
}
接下來是AES演算法加密程式碼。

程式碼中,SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");這一條語句是需要特別注意的,在程式碼中有詳細的說明。

另外,用c.doFinal(buff)函式加解密得到的結果都是byte[]型的,不能對加密得到的byte[]型結果直接new String(byte[]);儲存,然後在解密時用str.getBytes();轉換。

所以,借用了網上其他網友的方法,有toHex、fromHex、toByte、toHex、appendHex。可以確保byte[]和String之間的轉換不出問題。

package com.example.common;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.Security;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import android.util.Log;

/**
 * AES演算法加密類
 * @author Administrator
 *
 */
public class EncrypAES {
	private static final String TAG="EncryAES";

	/**
	 * Cipher負責完成加密或解密工作
	 */
	private Cipher c;
	
	/**
	 * 該位元組陣列負責儲存加密的結果
	 */
	private byte[] cipherByte;
	
	/**
	 * 用於生產金鑰
	 */
	private static final String SECRETKET="AESDemo";
	private SecretKeySpec deskey;
	
	public EncrypAES(){
		Security.addProvider(new com.sun.crypto.provider.SunJCE());
		
		try {
			deskey = new SecretKeySpec(getRawKey(SECRETKET.getBytes()),"AES");
			
			//生成Cipher物件,指定其支援的DES演算法
			c = Cipher.getInstance("AES");
		} catch (Exception e) {
			Log.e(TAG, "EnrypAES construct failed.",e);
		}
	}
	
	/**
	 * 對字串加密
	 * 
	 * @param str
	 * @return
	 * @throws InvalidKeyException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */
	private byte[] Encrytor(String str) throws InvalidKeyException,
			IllegalBlockSizeException, BadPaddingException {
		
		// 根據金鑰,對Cipher物件進行初始化,ENCRYPT_MODE表示加密模式
		c.init(Cipher.ENCRYPT_MODE, deskey);
		byte[] src = str.getBytes();
		
		cipherByte = c.doFinal(src);		// 加密,結果儲存進cipherByte
		return cipherByte;
	}

	/**
	 * 對字串解密
	 * 
	 * @param buff
	 * @return
	 * @throws InvalidKeyException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */
	private byte[] Decryptor(byte[] buff) throws InvalidKeyException,
			IllegalBlockSizeException, BadPaddingException {
		
		// 根據金鑰,對Cipher物件進行初始化,DECRYPT_MODE表示加密模式
		c.init(Cipher.DECRYPT_MODE, deskey);
		cipherByte = c.doFinal(buff);
		
		return cipherByte;
	}
	
	/**
	 * 對字串進行加密
	 * @param string 要加密的字串
	 * @return 加密後的字串
	 */
	public String EncryptorString(String string){
		String result =null;
		byte[] encontent;
		try {
			encontent = Encrytor(string);
			result=toHex(encontent);
		} catch (InvalidKeyException e) {
			Log.e(TAG, "EncryptorString",e);
		} catch (IllegalBlockSizeException e) {
			Log.e(TAG, "EncryptorString",e);
		} catch (BadPaddingException e) {
			Log.e(TAG, "EncryptorString",e);
		}
		
		return result;
	}
	
	/**
	 * 對字串進行解密
	 * @param string 要解密的字串
	 * @return 解密後的字串
	 */
	public String DecryptorString(String string){
		byte[] cryptcontent=toByte(string);
		byte[] decontent;
		String result=null;
		
		try {
			decontent=Decryptor(cryptcontent);
			result=new String(decontent);
			
		} catch (InvalidKeyException e) {
			Log.e(TAG,"DecryptorString failed.",e);
		} catch (IllegalBlockSizeException e) {
			Log.e(TAG,"DecryptorString failed.",e);
		} catch (BadPaddingException e) {
			Log.e(TAG,"DecryptorString failed.",e);
		}
		
		return result;
	}

	private static byte[] getRawKey(byte[] seed) throws Exception {
		KeyGenerator kgen = KeyGenerator.getInstance("AES");
		
		/**
		 * 這一句很關鍵。
		 * <br>網上有的程式碼是這一句SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
		 * <br>在getInstance函式中少了一個String引數。
		 * 這個引數是不能少的,因為如果少了這個引數的話,生成的金鑰是隨機的,而加密和解密必須是用同樣的金鑰的
		 * 所以會出現不能解密的問題(總是丟擲BadPaddingException異常)。而且這種方法中,對比少了一個引數的加密結果,
		 * 會發現每一次加密的結果都是不一樣的。
		 * <br>而用下面的語句得到的金鑰加密,同樣的字串任何時候得到的加密結果都是一樣的。
		 */
		SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");
		sr.setSeed(seed);
		kgen.init(128, sr); // 192 and 256 bits may not be available
		SecretKey skey = kgen.generateKey();
		byte[] raw = skey.getEncoded();
		return raw;
	}
	
	public static String toHex(String txt) {  
        return toHex(txt.getBytes());  
    }
	
    public static String fromHex(String hex) {  
        return new String(toByte(hex));  
    }  
      
    public static byte[] toByte(String hexString) {  
        int len = hexString.length()/2;  
        byte[] result = new byte[len];  
        for (int i = 0; i < len; i++)  
            result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();  
        return result;  
    }  
 
    public static String toHex(byte[] buf) {  
        if (buf == null)  
            return "";  
        StringBuffer result = new StringBuffer(2*buf.length);  
        for (int i = 0; i < buf.length; i++) {  
            appendHex(result, buf[i]);  
        }  
        return result.toString();  
    }
    
    private final static String HEX = "0123456789ABCDEF";
    private static void appendHex(StringBuffer sb, byte b) {  
        sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));  
    }
}


至於程式碼中其他的內容,比如把使用者名稱和密碼封裝在了User物件中,大家自己去看完整的程式碼。