天天看點

在Spring中如何使用加密外部屬性檔案

 等占位符引用屬性檔案的屬性值,這種配置方式有兩個明顯的好處:

- 減少維護的工作量:資源的配置資訊可以多應用共享,在多個應用使用同一資源的情況下,如果資源的位址、使用者名等配置資訊發生了更改,你隻要調整屬性檔案就可以了;

- 使部署更簡單:Spring配置檔案主要描述應用程式中的Bean,這些配置資訊在開發完成後,應該就固定下來了,在部署應用時,需要根據部署環境調整是就是資料源,郵件伺服器的配置資訊,将它們的配置資訊獨立到屬性檔案中,應用部署人員隻需要調整資源屬性檔案即可,根本不需要關注内容複雜的Spring配置檔案。不僅給部署和維護帶來了友善,也降低了出錯的機率。 

    Spring為我們提供了一個BeanFactoryPostProcessorBean工廠後置處理器接口的實作類:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer,它的主要功能是對引用了外部屬性值的<bean>進行處理,将其翻譯成真實的配置值。 

    一般的屬性資訊以明文的方式存放在屬性檔案中并沒有什麼問題,但如果是資料源或郵件伺服器使用者名密碼等重要的資訊,在某些場合,我們可能需要以密文的方式儲存。雖然Web應用的用戶端使用者看不到配置檔案的,但有時,我們隻希望特定的維護人員掌握重要資源的配置資訊,而不是毫無保留地對所有可以進入部署機器的使用者開放。 

    對于這種具有高度安全性要求的系統(如電信、銀行、重點人口庫等),我們需要對資源連接配接等屬性配置檔案中的配置資訊加密存放。然後讓Spring容器啟動時,讀入配置檔案後,先進行解密,然後再進行占位符的替換。 

    很可惜,PropertyPlaceholderConfigurer隻支援明文的屬性檔案。但是,我們可以充分利用Spring架構的擴充性,通過擴充PropertyPlaceholderConfigurer類來達到我們的要求。本文将講解使用加密屬性檔案的原理并提供具體的實作。 

    以傳統的方式使用屬性檔案 

    一般情況下,外部屬性檔案用于定義諸如資料源或郵件伺服器之類的配置資訊。這裡,我們通過一個簡單的例子,講解使用屬性檔案的方法。假設有一個car.properties屬性檔案,檔案内容如下: 

    brand=紅旗CA72 

    maxSpeed=250 

    price=20000.00 

    該檔案放在類路徑的com/baobaotao/目錄下,在Spring配置檔案中利用PropertyPlaceholderConfigurer引入這個配置檔案,并通過占位符引用屬性檔案内的屬性項,如代碼清單 1所示:

代碼清單 1 使用外部屬性檔案進行配置

① 引入外部屬性檔案
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
       <list>
          <value>classpath:com/baobaotao/car.properties</value> ② 指定屬性檔案位址
       </list>
    </property>
    <property name="fileEncoding" value="utf-8"/>
</bean>
③ 引用外部屬性的值,對car進行配置
<bean id="car" class="com.baobaotao.place.Car">
    <property name="brand" value="${brand}" />
    <property name="maxSpeed" value="${maxSpeed}" />
    <property name="price" value="${price}" />
</bean>
  
      

    在①處,我們通過PropertyPlaceholderConfigurer這個BeanFactoryPostProcessor實作類引用外部的屬性檔案,通過它的locations屬性指定Spring配置檔案中引用到的屬性檔案,在PropertyPlaceholderConfigurer内部,locations是一個Resource數組,是以你可以在位址前添加資源類型字首,如②處所示。如果需要引用多個屬性檔案,隻需要在②處添加相應<value>配置項即可。 

    分析PropertyPlaceholderConfigurer結構 

    我們知道Spring通過PropertyPlaceholderConfigurer提供對外部屬性檔案的支援,為了使用加密的屬性檔案,我們就需要分析該類的工作機理,再進行改造。是以我們先來了解一下該類的結構:

在Spring中如何使用加密外部屬性檔案

    其中PropertiesLoaderSupport類有一個重要的protected void loadProperties(Properties props)方法,檢視它的注釋,可以知道該方法的作用是将PropertyPlaceholderConfigurer 中locations屬性所定義的屬性檔案的内容讀取到props入參對象中。這個方法比較怪,Java很少通過入參承載傳回值,但這個方法就是這樣。 

    是以,我們隻要簡單地重載這個方法,在将資源檔案的内容轉換為Properties之前,添加一個解密的步驟就可以了。但是,PropertiesLoaderSupport的設計有一個很讓人遺憾的地方,它的locations屬性是private的,隻提供setter沒有提供getter。是以,無法在子類中擷取PropertiesLoaderSupport中的locations(資源位址),是以我們得在子類重新定義locations屬性并覆寫PropertiesLoaderSupport中的setLocations()方法。

    編寫支援加密屬性檔案的實作類 

    通過以上分析,我們設計一個支援加密屬性檔案的增強型PropertyPlaceholderConfigurer,其代碼如所示:

代碼清單 2 DecryptPropertyPlaceholderConfigurer

package com.baobaotao;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.Key;
import java.util.Properties;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
public class DecryptPropertyPlaceholderConfigurer
        extends PropertyPlaceholderConfigurer …{
    private Resource[] locations;   ① 重新定義父類中的這個同名屬性
    private Resource keyLocation; ② 用于指定密鑰檔案
    public void setKeyLocation(Resource keyLocation) …{
        this.keyLocation = keyLocation;
    }
    public void setLocations(Resource[] locations) …{
        this.locations = locations;
    }
    public void loadProperties(Properties props) throws IOException …{
        if (this.locations != null) …{
            PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
            for (int I = 0; I < this.locations.length; i++) …{
                Resource location = this.locations[i];
                if (logger.isInfoEnabled()) …{
                    logger.info("Loading properties file from " + location);
                }
                InputStream is = null;
                try …{
                    is = location.getInputStream();
                        ③ 加載密鑰
                    Key key = DESEncryptUtil.getKey(keyLocation.getInputStream());
                    ④ 對屬性檔案進行解密
is = DESEncryptUtil.doDecrypt(key, is);
⑤ 将解密後的屬性流裝載到props中
                    if(fileEncoding != null)…{
                        propertiesPersister.load(props,
        new InputStreamReader(is,fileEncoding));
                    }else…{
                        propertiesPersister.load(props ,is);
                    }
                } finally …{
                    if (is != null)
                        is.close();
                }
            }
        }
    }
    }
}
  
      

    對locations指定的屬性檔案流資料進行額外的解密工作,解密後再裝載到props中。比起PropertyPlaceholderConfigurer,我們隻做了額外的一件事:裝載前對屬性資源進行解密。 

    在代碼清單 2的③和④處,我們使用了一個DES解密的工具類對加密的屬性檔案流進行解密。

    加密解密工具類DESEncryptUtil 

    對檔案進行對稱加密的算法很多,一般使用DES對稱加密算法,因為它速度很快,破解困難,DESEncryptUtil不但提供了DES解密功能,還提供了DES加密的功能,因為屬性檔案在部署前必須經常加密: 

    圖 2 加密解密工具類

package com.baobaotao.place;
…
public class DESEncryptUtil …{
    public static Key createKey() throws NoSuchAlgorithmException …{//建立一個密鑰
        Security.insertProviderAt(new com.sun.crypto.provider.SunJCE(), 1);
        KeyGenerator generator = KeyGenerator.getInstance("DES");
        generator.init(new SecureRandom());
        Key key = generator.generateKey();
        return key;
    }
    public static Key getKey(InputStream is) …{
        try …{
            ObjectInputStream ois = new ObjectInputStream(is);
            return (Key) ois.readObject();
        } catch (Exception e) …{
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    private static byte[] doEncrypt(Key key, byte[] data) …{//對資料進行加密
        try …{
            Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] raw = cipher.doFinal(data);
            return raw;
        } catch (Exception e) …{
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public static InputStream doDecrypt(Key key, InputStream in) …{//對資料進行解密
        try …{
            Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, key);
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            byte[] tmpbuf = new byte[1024];
            int count = 0;
            while ((count = in.read(tmpbuf)) != -1) …{
                bout.write(tmpbuf, 0, count);
                tmpbuf = new byte[1024];
            }
            in.close();
            byte[] orgData = bout.toByteArray();
            byte[] raw = cipher.doFinal(orgData);
            ByteArrayInputStream bin = new ByteArrayInputStream(raw);
            return bin;
        } catch (Exception e) …{
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws Exception …{//提供了Java指令使用該工具的功能
        if (args.length == 2 && args[0].equals("key")) …{// 生成密鑰檔案
            Key key = DESEncryptUtil.createKey();
            ObjectOutputStream oos = new ObjectOutputStream(
                    new FileOutputStream(args[1]));
            oos.writeObject(key);
            oos.close();
            System.out.println("成功生成密鑰檔案。");
        } else if (args.length == 3 && args[0].equals("encrypt")) …{//對檔案進行加密
            File file = new File(args[1]);
            FileInputStream in = new FileInputStream(file);
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            byte[] tmpbuf = new byte[1024];
            int count = 0;
            while ((count = in.read(tmpbuf)) != -1) …{
                bout.write(tmpbuf, 0, count);
                tmpbuf = new byte[1024];
            }
            in.close();
            byte[] orgData = bout.toByteArray();
            Key key = getKey(new FileInputStream(args[2]));
            byte[] raw = DESEncryptUtil.doEncrypt(key, orgData);
            file = new File(file.getParent() + "//en_" + file.getName());
            FileOutputStream out = new FileOutputStream(file);
            out.write(raw);
            out.close();
            System.out.println("成功加密,加密檔案位于:"+file.getAbsolutePath());
        } else if (args.length == 3 && args[0].equals("decrypt")) …{//對檔案進行解密
            File file = new File(args[1]);
            FileInputStream fis = new FileInputStream(file);
            Key key = getKey(new FileInputStream(args[2]));
            InputStream raw = DESEncryptUtil.doDecrypt(key, fis);
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            byte[] tmpbuf = new byte[1024];
            int count = 0;
            while ((count = raw.read(tmpbuf)) != -1) …{
                bout.write(tmpbuf, 0, count);
                tmpbuf = new byte[1024];
            }
            raw.close();
            byte[] orgData = bout.toByteArray();
            file = new File(file.getParent() + "//rs_" + file.getName());
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(orgData);
            System.out.println("成功解密,解密檔案位于:"+file.getAbsolutePath());
        }
    }
}    
  
      

    解密工作主要涉及到兩個類Cipher和Key,前者是加密器,可以通過init()方法設定工作模式和密鑰,在這裡,我們設定為解密工作模式:Cipher.DECRYPT_MODE。Cipher通過doFinal()方法對位元組數組進行加密或解密。關于加密,解密更詳細的知識,感興趣的讀者可以參閱相關的文章。

    屬性檔案加密解密工具類使用 

    要完成屬性檔案的加密工作,首先,必須擷取一個密鑰檔案,然後才能對明文的屬性檔案進行加密。如果需要調整屬性檔案的資訊,你必須執行相反的過程,即用密鑰對加密後的屬性檔案進行解密,調整屬性資訊後,再将其加密。

DESEncryptUtil 工具類可以完成以上所提及的三個工作:

 生成一個密鑰檔案

java com.baobaotao.DESEncryptUtil key D:/key.dat

第一個參數為key,表示建立密鑰檔案,第二個參數為生成密鑰檔案的儲存位址。

 用密鑰檔案對屬性檔案進行加密

java com.baobaotao.DESEncryptUtil encrypt d:/test.properties d:/key.dat

第一個參數為encrypt,表示加密,第二個參數為需要加密的屬性檔案,第三個參數為密鑰檔案。如果加密成功,将生成en_test.properties的加密檔案。

 用密鑰檔案對加密後的屬性檔案進行解密

java com.baobaotao.DESEncryptUtil decrypt d:/test.properties d:/key.dat

第一個參數為decrypt,表示解密,第二個參數為需要解密的屬性檔案,第三個參數為密鑰檔案。如果加密成功,将生成rs_test.properties的解密檔案。 

    在Spring中配置加密屬性檔案 

    假設我們通過DESEncryptUtil 工具類建立了一個key.bat密鑰,并對car.properties屬性進行加密,生成加密檔案en_car.properties。下面,我們通過DecryptPropertyPlaceholderConfigurer增強類進行配置,讓Spring容器支援加密的屬性檔案: 

    假設我們通過DESEncryptUtil 工具類建立了一個key.bat密鑰,并對car.properties屬性進行加密,生成加密檔案en_car.properties。下面,我們通過DecryptPropertyPlaceholderConfigurer增強類進行配置,讓Spring容器支援加密的屬性檔案:

<bean class="com.baobaotao.place.DecryptPropertyPlaceholderConfigurer"> ①
    <property name="locations">
        <list>
            <value>classpath:com/baobaotao/en_car.properties</value>
        </list>
    </property>
    <property name="keyLocation" value="classpath:com/baobaotao/key.dat" />
    <property name="fileEncoding" value="utf-8" />
</bean>
<bean id="car" class="com.baobaotao.place.Car"> ②
    <property name="brand" value="${brand}" />
    <property name="maxSpeed" value="${maxSpeed}" />
    <property name="price" value="${price}" />
</bean>
  
      

    注意①處的配置,我們使用自己編寫的DecryptPropertyPlaceholderConfigurer替代Spring的PropertyPlaceholderConfigurer,由于前者對屬性檔案進行了特殊的解密處理,是以②處的car Bean也可以引用到加密檔案en_car.properties中的屬性項。 

    小結 

    要Spring配置時,将一些重要的資訊獨立到屬性檔案中是比較常見的做法,Spring隻支援明文存放的屬性檔案,在某些場合下,我們可以希望對屬性檔案加密儲存,以保證關鍵資訊的安全。通過擴充PropertyPlaceholderConfigurer,在屬性檔案流加載後應用前進行解密就可以很好地解決這個問題了。