????上周在項目中需要對URL參數進行加密傳輸袜爪,實際過程中碰到了一些問題,在此對加密算法的Java實現及出現的編碼問題進行一個簡單的記錄婿屹。
一、加密算法
? ? ?這次分別對RSA(非對稱加密)和AES(對稱加密)進行了使用。這里也只對這兩種算法的Java實現進行簡單介紹,網上資料滿天飛,算法的具體內容和其他的算法自行查找吧。
? ? ?RSA承疲,通常使用公鑰加密、私鑰解密鸥咖,反之亦然燕鸽;而且大家肯定是不希望有人冒充我們發(fā)消息,可以通過只有我們自己掌握的私鑰來負責簽名扛或,公鑰負責驗證绵咱。通常私鑰長度有1024bit,2048bit熙兔,4096bit悲伶,長度越長,越安全住涉,但是生成密鑰越慢麸锉,加解密也越耗時(當然生成的加密串的長度也同所選秘鑰的長度一致)。
//生成秘鑰
public StringgenerateKey() {
try {
? ? ? ? KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");? ? ? //采用RSA算法
? ? ? ? kpg.initialize(1024);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //初始化KeyPairGenerator對象,密鑰長度采用1024bit
? ? ? ? KeyPair kp = kpg.genKeyPair();? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?????????//生成秘鑰對
? ? ? ? RSAPublicKey pbkey = (RSAPublicKey) kp.getPublic();? ? ? ? ? ?????????//獲取公鑰
? ? ? ? RSAPrivateKey prkey = (RSAPrivateKey) kp.getPrivate();? ? ? ? ????????//獲取私鑰
? ? ? ? // 通過base64編碼得到公鑰字符串
? ? ? ? String publicKeyString = org.apache.tomcat.util.codec.binary.Base64.encodeBase64String(pbkey.getEncoded());
? ? ? ? // 通過base64編碼得到私鑰字符串
? ? ? ? String privateKeyString = org.apache.tomcat.util.codec.binary.Base64.encodeBase64String(prkey.getEncoded());
? ? ? ? return "publicKeyString:"+publicKeyString+"? privateKeyString:"+privateKeyString;
? ? }catch (Exception e) {
????????return null;
? ? }
}
//我這里是將之前生成的公鑰舆声、私鑰保存在配置文件中了花沉,現在通過@Value()注解來獲取秘鑰
@Value("${active.pbkey}")private String pbkey;
@Value("${active.prkey}")private String prkey;
//使用公鑰加密
public byte[](@RequestParam String accountName)throws Exception {
? ??//將base64編碼后的公鑰字符串轉成PublicKey實例(公鑰要通過X509編碼的key來獲取)
? ? byte[] buffer = org.apache.tomcat.util.codec.binary.Base64.decodeBase64(pbkey);
? ? KeyFactory keyFactory = KeyFactory.getInstance("RSA");
? ? X509EncodedKeySpec keySpec =new X509EncodedKeySpec(buffer);
? ? RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
? ? //加密
? ? Cipher cipher =null;
? ? try {
? ? ? ? cipher = Cipher.getInstance("RSA");
? ? ? ? cipher.init(Cipher.ENCRYPT_MODE, publicKey);
? ? ? ? byte[]result = cipher.doFinal(accountName.getBytes());
? ? ? ? return result;
? ? }catch (Exception e) {
? ? ? ? log.error("參數加密失敗", e);
? ? ? ? return null;
? ? }
}
//使用私鑰進行解密
public String(@RequestParam byte[]url)throws Exception {
? ? //將base64編碼后的私鑰字符串轉成PrivateKey實例(私鑰要通過PKCS#8 編碼的key來獲认蔽铡)? ?
? ? byte[] buffer = Base64.decodeBase64(prkey);
? ? PKCS8EncodedKeySpec keySpec =new PKCS8EncodedKeySpec(buffer);
? ? KeyFactory keyFactory = KeyFactory.getInstance("RSA");
? ? RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
? ? //解密
? ? Cipher cipher =null;
? ? try {
? ? ? ? cipher = Cipher.getInstance("RSA");
? ? ? ? cipher.init(Cipher.DECRYPT_MODE, privateKey);
? ? ? ? String accountName=new String(cipher.doFinal(url));
? ? ? ? return accountName;
? ? }catch (NoSuchPaddingException e) {
? ? ? ? log.error("參數解密失敗", e);
? ? ? ? return null;
? ? }
}
? ? ?AES碱屁,密鑰最長只有256個bit,執(zhí)行速度快蛾找。由于是對稱加密娩脾,是沒有公鑰和私鑰的區(qū)分的,雙方使用同一秘鑰進行加密打毛、解密柿赊,安全度相對非對稱加密較低俩功。基于以上特點碰声,通常使用RSA來首先傳輸AES的密鑰給對方(速度慢诡蜓,安全性高),然后再使用AES來進行加密通訊(速度快胰挑,安全性較低)蔓罚。
//生成AES秘鑰,AES沒有秘鑰對瞻颂,直接生成秘鑰即可
public StringgenerateKey() {
try {
????????KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
? ? ? ? keyGenerator.init(128);
? ? ? ? SecretKey secretKey = keyGenerator.generateKey();? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//生成秘鑰
? ? ? ? StringKeyString= Base64.encodeBase64String(secretKey.getEncoded());? ??????????????????????????????????????// 得到密鑰字符串
? ? ? ? return "KeyString:"+KeyString;
? ? }catch (Exception e) {
????????return null;
? ? }
}
//獲取存儲在配置文件中的秘鑰
@Value("${active.key}")
private String key;
// 加密.
public byte[]encrypt(String refer) {
????byte[] buffer = Base64.decodeBase64(key);
? ? SecretKey key=new SecretKeySpec(buffer, "AES");
????Cipher cipher =null;
? ? try {
????????cipher = Cipher.getInstance("AES");
? ? ? ? cipher.init(Cipher.ENCRYPT_MODE, key);
? ? ? ? byte[]result = cipher.doFinal(refer.getBytes("UTF-8"));
? ? ? ? return result;
? ? }catch (Exception e) {
????????log.error("參數加密失敗", e);
????????return null;
? ? }
}
//解密.
public String decrypt(byte[]refer) {
????byte[] buffer = Base64.decodeBase64(key);
? ? SecretKey key=new SecretKeySpec(buffer, "AES");
? ? Cipher cipher =null;
? ? try {
????????cipher = Cipher.getInstance("AES");
? ? ? ? cipher.init(Cipher.DECRYPT_MODE, key);
? ? ? ? String url = new String(cipher.doFinal(refer),"UTF-8");
? ? ? ? return url;
? ? }catch (Exception e) {
????????log.error("參數解密失敗脚粟,錯誤refer:"+refer, e);
????????return null;
? ? }
}
二、常見問題
? ? ?因為生成的密文為byte[ ]類型蘸朋,如果使用上面的代碼,直接對加密后的byte[ ]密文進行解密是完全沒有問題的扣唱。但我們實際使用中經常需要以Strring類型進行傳輸藕坯,需要通過url傳輸后再解密,這種情況下會出現很多問題噪沙。
? ? ?我們可以明顯的看出,經過String轉換得到的result已與初始的bytes不同了正歼。原因是轉換為String時是根據當前默認編碼類型(UTF-8)來生成的辐马,UTF-8是可變長度的編碼,有的字符需要用多個字節(jié)來表示局义,所以也就出現了在轉換之后byte[]數組長度喜爷、內容不一致的情況。
解決方案:
(1)Base64
?????Base64?是一種將二進制數據編碼的方式萄唇,正如UTF-8和UTF-16是將文本數據編碼的方式一樣檩帐,我們可以通過Base64將二進制數據編碼為文本數據。
//加密后將byte[ ]密文通過Base64轉為String
String str = Base64.encodeBase64String(bytes)另萤;
//解密前將String再通過Base64解碼為byte[ ]
byte[ ] bytes = Base64.decodeBase64(str)湃密;
***需要注意的是,Base64編碼后可能出現字符+和/四敞,在URL中就不能直接作為參數泛源,因為在urlEcode編碼中 “+” 會被解碼成空格。
解決方案一:拿到數據時將空格替換回“+”
解決方案二:預先進行urlEncode(但是如果該編碼后的密文在服務端獲取到之前經過微信忿危、QQ轉發(fā)或在瀏覽器中重定向后會被提前decode达箍,服務端拿到后仍不能正常解析)
//加密、Base64編碼后先encode再通過URL傳輸
String str = URLEncoder.encode(Base64.encodeBase64String(bytes),"UTF-8");
//解密前直接Base64解碼即可癌蚁,經過URL傳輸后獲得的鏈接已decode
byte[ ] bytes = Base64.decodeBase64(str)幻梯;
解決方案三:使用URL安全的Base64編碼兜畸,會把字符+和/分別變成-和_
//加密后使用URLSafeBase64
String str =?Base64.encodeBase64URLSafeString(bytes);
//解密前先解碼
byte[ ] bytes = Base64.decodeBase64(str)碘梢;
(2)轉換進制
? ? ?為了防止二進制直接轉為字符串String類型時出現數據缺失的現象咬摇,先byte[ ]密文轉換為十六進制,解密前再將十六進制轉回二進制煞躬。
? ? ?Java中的String對象是不需要指定編碼表的肛鹏,因為String里的字符信息是用UNICODE編碼的,并且Java使用char數據類型來對應UNICODE的字符恩沛,其大小為固定的兩個8位16進制數字在扰。Java中byte用二進制表示占用8位,而我們知道16進制的每個字符需要用4位二進制位來表示雷客。所以我們就可以把每個byte轉換成兩個相應的16進制字符芒珠,即把byte的高4位和低4位分別轉換成相應的16進制字符H和L,并組合起來得到byte轉換到16進制字符串的結果new String(H) + new String(L)搅裙。同理皱卓,相反的轉換也是將兩個16進制字符轉換成一個byte,原理同上部逮。根據以上原理娜汁,我們就可以將byte[] 數組轉換為16進制字符串了,當然也可以將16進制字符串轉換為byte[]數組了兄朋。
//加密后使用轉為十六進制
String str =??XXClass.parseByte2HexStr(?bytes?)掐禁;
//解密前先轉回二進制
byte[ ] bytes =?XXClass?.parseHexStr2Byte(str);
//2轉16
public static StringparseByte2HexStr(byte buf[]) {
????StringBuffer sb =new StringBuffer();
? ? for (int i =0; i < buf.length; i++) {
????????String hex = Integer.toHexString(buf[i] &0xFF);
? ? ? ? if (hex.length() ==1) {
????????hex ='0' + hex;
? ? ????}
????????sb.append(hex.toUpperCase());
? ? }
????return sb.toString();
}
//16轉2
public static byte[]parseHexStr2Byte(String hexStr) {
????if (hexStr.length() <1){
????????return null;
? ? }
????byte[] result =new byte[hexStr.length()/2];
? ? for (int i =0;i< hexStr.length()/2; i++) {
????????int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
? ? ? ? int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
? ? ? ? result[i] = (byte) (high *16 + low);
? ? }
????return result;
}
***附String的轉換使用:
public static void main(String[] args){
????String str ="ccha1994";
? ? byte[] strbyte = str.getBytes();
? ? System.out.println("toString:"+strbyte.toString());
? ? System.out.println("new String:"+new String(strbyte));
}
運行結果:
? ? ?toString():顯示的結果用的是父類Object的toString()方法,通常默認返回當前對象(c)的內存地址颅和,即hashCode傅事。
? ? ?new String():通過字節(jié)數組byte[]調用String對象中的toString(),是根據parameter是一個字節(jié)數組,使用java虛擬機默認的編碼格式或者參數指定的編碼格式峡扩,將這個字節(jié)數組decode為對應的字符享完。
使用:
? ? ?new?String()一般使用字符轉碼的時候,byte[ ]數組的時候。
? ? ?toString()將對象打印的時候使用 有额。