JAVA RSA 前後端程式碼實現踐行記錄

2020-08-12 14:43:41

有時候後端需要通過回撥來與前端互動,但回撥url上往往有關鍵性的資訊例如使用者的token,爲了防止此鏈接被惡意攔截反覆 反復使用,有必要將關鍵參數加上時間戳並用加密演算法加密與前端互動。前端可以控制時間戳大於多少分鐘則忽略此token,攔截者不知道金鑰情況下無法僞造加密文,就可以避免此鏈接反覆 反復被使用。因爲前端程式碼能被破解故而使用非對稱加密演算法RSA。【當然,前端手機使用者可以通過修改系統時間來破解此判斷,但可以往所有與後端介面互動中後端加入時間戳判斷,一樣可以解決此問題。】

後端RSA加解密演算法

網上有些演算法是隻支援加密117位解密128位元限制的寫法,以下參考網際網路的一些程式碼,變爲不限制,金鑰長度爲1024位元,若改爲2048位元則需要變更解密演算法的長度爲256。

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import sun.misc.BASE64Decoder;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;

/**
 * @author katasea
 * 2020/8/11 11:01
 */
public class RSAUtils4Mall {


	private static final String RSA_ALGORITHM = "RSA";
	private static final int MAX_DECRYPT_BLOCK = 128;
	private static final int MAX_ENCRYPT_BLOCK = 117;
	private static RSAPublicKey publicKey;
	private static RSAPrivateKey privateKey;

	public RSAPublicKey getPublicKey() {
		return RSAUtils4Mall.publicKey;
	}
	public RSAPrivateKey getPrivateKey() {
		return RSAUtils4Mall.privateKey;
	}

	public void getKeys() throws Exception {
		// 從 公鑰儲存的檔案 讀取 公鑰的Base64文字
		String pubKeyBase64 = "你自己的公鑰,可以放此處方便固定,若不需要固定則使用geneKeys() 方法";
// 把 公鑰的Base64文字 轉換爲已編碼的 公鑰bytes
		byte[] encPubKey = new BASE64Decoder().decodeBuffer(pubKeyBase64);

// 建立 已編碼的公鑰規格
		X509EncodedKeySpec encPubKeySpec = new X509EncodedKeySpec(encPubKey);

// 獲取指定演算法的金鑰工廠, 根據 已編碼的公鑰規格, 生成公鑰物件
		publicKey = (RSAPublicKey)KeyFactory.getInstance("RSA").generatePublic(encPubKeySpec);

		// 從 私鑰儲存的檔案 讀取 私鑰的base文字
		String priKeyBase64 = "你自己的私鑰,可以放此處方便固定,若不需要固定則使用geneKeys() 方法";

// 把 私鑰的Base64文字 轉換爲已編碼的 私鑰bytes
		byte[] encPriKey = new BASE64Decoder().decodeBuffer(priKeyBase64);

// 建立 已編碼的私鑰規格
		PKCS8EncodedKeySpec encPriKeySpec = new PKCS8EncodedKeySpec(encPriKey);

// 獲取指定演算法的金鑰工廠, 根據 已編碼的私鑰規格, 生成私鑰物件
		privateKey = (RSAPrivateKey)KeyFactory.getInstance("RSA").generatePrivate(encPriKeySpec);
	}

	public void geneKeys() throws NoSuchAlgorithmException {
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM, new BouncyCastleProvider());
		keyPairGenerator.initialize(1024);
		KeyPair keyPair = keyPairGenerator.generateKeyPair();
		privateKey = (RSAPrivateKey) keyPair.getPrivate();
		publicKey = (RSAPublicKey) keyPair.getPublic();
	}


	public String encodeByPrivateKey(String body) throws Exception {
		Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
		cipher.init(Cipher.ENCRYPT_MODE, privateKey);
		byte[] inputArray = body.getBytes();
		int inputLength = inputArray.length;
		System.out.println("加密位元組數:" + inputLength);
		// 標識
		int offSet = 0;
		byte[] resultBytes = {};
		byte[] cache = {};
		while (inputLength - offSet > 0) {
			if (inputLength - offSet > MAX_ENCRYPT_BLOCK) {
				cache = cipher.doFinal(inputArray, offSet, MAX_ENCRYPT_BLOCK);
				offSet += MAX_ENCRYPT_BLOCK;
			} else {
				cache = cipher.doFinal(inputArray, offSet, inputLength - offSet);
				offSet = inputLength;
			}
			resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
			System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
		}
		return Base64.getEncoder().encodeToString(resultBytes);
	}

	public String encodeByPublicKey(String body) throws Exception {
		Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
		cipher.init(Cipher.ENCRYPT_MODE, publicKey);
		byte[] inputArray = body.getBytes();
		int inputLength = inputArray.length;
		System.out.println("加密位元組數:" + inputLength);
		// 標識
		int offSet = 0;
		byte[] resultBytes = {};
		byte[] cache = {};
		while (inputLength - offSet > 0) {
			if (inputLength - offSet > MAX_ENCRYPT_BLOCK) {
				cache = cipher.doFinal(inputArray, offSet, MAX_ENCRYPT_BLOCK);
				offSet += MAX_ENCRYPT_BLOCK;
			} else {
				cache = cipher.doFinal(inputArray, offSet, inputLength - offSet);
				offSet = inputLength;
			}
			resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
			System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
		}
		return Base64.getEncoder().encodeToString(resultBytes);
	}

	public String decodeByPublicKey(String body) throws Exception {
		Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
		cipher.init(Cipher.DECRYPT_MODE, publicKey);
		return decryptByPublicKey(body);
	}

	public String decodeByPrivateKey(String body) throws Exception {
		Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
		cipher.init(Cipher.DECRYPT_MODE, privateKey);
		return decryptByPrivateKey(body);
	}

	public String decryptByPublicKey(String encryptedStr) {
		try {
			// 對公鑰解密
			byte[] privateKeyBytes = publicKey.getEncoded();
			// 獲得公鑰
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(privateKeyBytes);
			// 獲得待解密數據
			byte[] data = decryptBase64(encryptedStr);
			KeyFactory factory = KeyFactory.getInstance("RSA");
			PublicKey publicKey = factory.generatePublic(keySpec);
			// 對數據解密
			Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
			cipher.init(Cipher.DECRYPT_MODE, publicKey);
			// 返回UTF-8編碼的解密資訊
			int inputLen = data.length;
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			int offSet = 0;
			byte[] cache;
			int i = 0;
			// 對數據分段解密
			while (inputLen - offSet > 0) {
				if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
					cache = cipher.doFinal(data, offSet, MAX_DECRYPT_BLOCK);
				} else {
					cache = cipher.doFinal(data, offSet, inputLen - offSet);
				}
				out.write(cache, 0, cache.length);
				i++;
				offSet = i * MAX_DECRYPT_BLOCK;
			}
			byte[] decryptedData = out.toByteArray();
			out.close();
			return new String(decryptedData, "UTF-8");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}



	/**
	 * 私鑰解密
	 *
	 * @param encryptedStr
	 * @return
	 */
	public String decryptByPrivateKey(String encryptedStr) {
		try {
			// 對私鑰解密
			byte[] privateKeyBytes = privateKey.getEncoded();
			// 獲得私鑰
			PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
			// 獲得待解密數據
			byte[] data = decryptBase64(encryptedStr);
			KeyFactory factory = KeyFactory.getInstance("RSA");
			PrivateKey privateKey = factory.generatePrivate(keySpec);
			// 對數據解密
			Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			// 返回UTF-8編碼的解密資訊
			int inputLen = data.length;
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			int offSet = 0;
			byte[] cache;
			int i = 0;
			// 對數據分段解密
			while (inputLen - offSet > 0) {
				if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
					cache = cipher.doFinal(data, offSet, MAX_DECRYPT_BLOCK);
				} else {
					cache = cipher.doFinal(data, offSet, inputLen - offSet);
				}
				out.write(cache, 0, cache.length);
				i++;
				offSet = i * MAX_DECRYPT_BLOCK;
			}
			byte[] decryptedData = out.toByteArray();
			out.close();
			return new String(decryptedData, "UTF-8");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * BASE64 解碼
	 *
	 * @param key 需要Base64解碼的字串
	 * @return 位元組陣列
	 */
	public static byte[] decryptBase64(String key) {
		return Base64.getDecoder().decode(key);
	}


	public static void main(String[] args) throws Exception {
		RSAUtils4Mall rsaUtils = new RSAUtils4Mall();
		//使用固定key
		rsaUtils.getKeys();
		//使用自動生成key 一臺機器會生成一次並固定,後續可以後端開放獲取公鑰介面給前端,用來解決公鑰變化問題。也可以直接使用上面固定公私鑰
//		rsaUtils.geneKeys();
		String plain = "{\"Authorization\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTY3OTA5NjYsInRlcm1fdHlwZSI6bnVsbCwidXNlcl9uYW1lIjoiMTI4Nzc1Njc1NTIxMTY0OTA2NiIsImp0aSI6IjM5NjM2ZDBiLTcxOGUtNGIxYS04ZTZiLWExNDc4ZGMyYThhZCIsImNsaWVudF9pZCI6ImZyb250ZW5kIiwic2NvcGUiOlsiYWxsIl19.WRD-T4tpXLpAA7VaSNCdmVdh0cjqVlLI-Vq0lJw9QZI\",\"timestamp\":\"20200811 135244\"}";
		String encryptData = rsaUtils.encodeByPublicKey(plain);
		String encryptData2 = rsaUtils.encodeByPrivateKey(plain);
		System.out.println("公鑰加密:"+encryptData);
		System.out.println("私鑰加密:"+encryptData2);
		System.out.println("私鑰解密後:" + rsaUtils.decodeByPrivateKey(encryptData));
		System.out.println("公鑰解密後:" + rsaUtils.decodeByPublicKey(encryptData2));
		String encodes = rsaUtils.encodeByPrivateKey("123");
		System.out.println("加密前:" + 123);
		System.out.println("公鑰:" + Base64.getEncoder().encodeToString(RSAUtils4Mall.publicKey.getEncoded()));
		System.out.println("私鑰:" + Base64.getEncoder().encodeToString(RSAUtils4Mall.privateKey.getEncoded()));
		System.out.println("私鑰加密後:" + encodes);
		System.out.println("公鑰加密後:" + rsaUtils.encodeByPublicKey("123"));
		System.out.println("公鑰解密後:" + rsaUtils.decodeByPublicKey(encodes));
		String plain2 = "fELmp2H3m%2BhY9DnZHj6QmgxVqVXGTDDeCXxfYNDM2ow6E0hVGQ%2FjT%2FiSMKTxJoXTJS1I2XouybUczzBppF6fDUTwlyNIFViI9wO2ErfEEnikwc9O%2Bgt1SuOScZjLVpkvrw0RrcXzhg1n2rqqJuzYwG0lvrpIAg2haJmyzgiCn4oRiBHexLNQ%2BLcJdYu%2FN9BTndk1ytuPX4osiue1kGBrqKMW3zX97m7%2FRdqeS90OyW29C5tcDq80eWQQXteh0B2L%2B2wgEwiGMLnKw4EOdTYSyJ1k9tQQ90JA4gj%2BlwEgyQ3yB7Gj0ZrqplxoSxJ8NnNbNtHRfGKbB60rvMIHD4Z7SpQZRHsZyihT3CdAdwOzvu91ZSq52EQOLPE0EkMfHDtent1ExOliB950YLJFn%2BHKQTuixDyUusUzABhpOJfp1bxjJDcJPlWoMWoe%2B8%2B2mzMIiVsvKKsgIZivsLm%2FWnNkKr0F9wBJ1RS6yuWr0z7yzg3%2BxfUQyNyPSJXdv2cOq%2B3G";
		plain2 = URLDecoder.decode(plain2,"UTF-8");
		System.out.println(plain2);
		System.out.println(rsaUtils.decodeByPublicKey(plain2));
	}
}

具體使用樣例如下,由於放置與url跳轉到前端,故而需要對加密後的密文做url 編碼,否則+號等會丟失。

try {
				log.info("RSA加密前報文:{}", JSONObject.toJSONString(paramMap));
				RSAUtils4Mall rsaUtils = new RSAUtils4Mall();
				rsaUtils.getKeys();
				encryptData = rsaUtils.encodeByPublicKey(JSONObject.toJSONString(paramMap));
				log.info("RSA加密後報文:{}", encryptData);
				encryptData = URLEncoder.encode(encryptData,"UTF-8");
			} catch (Exception e) {
				e.printStackTrace();
				log.error("返回前端token時候加密失敗了!{}", e.getMessage());
			}

URLEncoder.encode 編碼對應前端的 decodeURIComponent() 來解碼

前端RSA加解密演算法

注意、由於前端使用jsencrypt.js實現加解密,其中它預設公鑰加密,私鑰解密,故而伺服器端如果加密務必使用公鑰加密,前端這邊用私鑰解密。反之無法測試成功。一般公鑰加密,私鑰用於簽名。因爲公鑰加密只有私鑰才能 纔能解密,而如果私鑰加密知道公鑰的都可以解密。這裏雖然私鑰放於前端會被人截獲,但是公鑰這邊並未對外開放。這裏穿插一個個人的理解,金鑰對其實是一個長一個短,一般短的用來加密,長的用來簽名效率會比反之來的快。所謂公鑰私鑰只是一個概念,對外開放的就是公鑰。不管對不對,反正工具是死的。

前端測試頁面程式碼

<script>
    //獲取url後面的密文參數 
    var encryptData = getQueryVariable("encryptData")
    // var publicKey= "相對短的公鑰";
    var privateKey="你的長長的私鑰";
    // var plain = "{\"Authorization\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTY3OTA5NjYsInRlcm1fdHlwZSI6bnVsbCwidXNlcl9uYW1lIjoiMTI4Nzc1Njc1NTIxMTY0OTA2NiIsImp0aSI6IjM5NjM2ZDBiLTcxOGUtNGIxYS04ZTZiLWExNDc4ZGMyYThhZCIsImNsaWVudF9pZCI6ImZyb250ZW5kIiwic2NvcGUiOlsiYWxsIl19.WRD-T4tpXLpAA7VaSNCdmVdh0cjqVlLI-Vq0lJw9QZI\",\"timestamp\":\"20200811 135244\"}";
    var encrypt = new JSEncrypt();
    // encrypt.setPublicKey(publicKey);
    encrypt.setPrivateKey(privateKey);
    // alert(encrypt.encryptLong(plain));
    if(encryptData!=null) {
        alert(encryptData);
		//URL解碼 將 %2 解碼 + 等
        encryptData = decodeURIComponent(encryptData);
        alert(encryptData);
        alert(encrypt.decryptLong(encryptData));
    }

	/**
	 *  這裏將密文放於url後面,通過這個方法可以獲取url後面參數的具體值。
	 *  eg : http://a.b.com?param=xxxx    傳入param 獲取 xxxx
	 */
    function getQueryVariable(variable)
    {
        var query = window.location.search.substring(1);
        var vars = query.split("&");
        for (var i=0;i<vars.length;i++) {
            var pair = vars[i].split("=");
            if(pair[0] == variable){return pair[1];}
        }
        return(false);
    }
</script>

網上找的 jsencrypt.js基本無法使用,尤其是長度超過預設限制的加密117 解密128 的新增方法段,更是各種抄來抄去,後面尋的一個可以使用的。建議大家偵錯的時候先指令碼加密看看與後臺加密的密文是否一致。由簡到難

這裏貼出關鍵方法

//十六進制轉位元組
    function hexToBytes(hex) {
        for (var bytes = [], c = 0; c < hex.length; c += 2)
            bytes.push(parseInt(hex.substr(c, 2), 16));
        return bytes;
    }

    // 位元組轉十六進制
    function bytesToHex(bytes) {
        for (var hex = [], i = 0; i < bytes.length; i++) {
            hex.push((bytes[i] >>> 4).toString(16));
            hex.push((bytes[i] & 0xF).toString(16));
        }
        return hex.join("");
    }


  //先增加上面兩個方法 , 尋找原始檔裏面的 加解密演算法,在旁邊增加


  JSEncrypt.prototype.decryptLong = function (string) {
            var k = this.getKey();
            // var MAX_DECRYPT_BLOCK = ((k.n.bitLength()+7)>>3);
            var MAX_DECRYPT_BLOCK = 128;
            try {
                var ct = "";
                var t1;
                var bufTmp;
                var hexTmp;
                var str = b64tohex(string);
                var buf = hexToBytes(str);
                var inputLen = buf.length;
                //開始長度
                var offSet = 0;
                //結束長度
                var endOffSet = MAX_DECRYPT_BLOCK;

                //分段加密
                while (inputLen - offSet > 0) {
                    if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                        bufTmp = buf.slice(offSet, endOffSet);
                        hexTmp = bytesToHex(bufTmp);
                        t1 = k.decrypt(hexTmp);
                        ct += t1;

                    } else {
                        bufTmp = buf.slice(offSet, inputLen);
                        hexTmp = bytesToHex(bufTmp);
                        t1 = k.decrypt(hexTmp);
                        ct += t1;

                    }
                    offSet += MAX_DECRYPT_BLOCK;
                    endOffSet += MAX_DECRYPT_BLOCK;
                }
                return ct;
            } catch (ex) {
                return ex;
            }
        };


JSEncrypt.prototype.encryptLong = function (string) {
            var k = this.getKey();
            //var MAX_ENCRYPT_BLOCK = (((k.n.bitLength() + 7) >> 3) - 11);
            var MAX_ENCRYPT_BLOCK = 117;

            try {
                var lt = "";
                var ct = "";
                //RSA每次加密117bytes,需要輔助方法判斷字串擷取位置
                //1.獲取字串擷取點
                var bytes = new Array();
                bytes.push(0);
                var byteNo = 0;
                var len, c;
                len = string.length;

                var temp = 0;
                for (var i = 0; i < len; i++) {
                    c = string.charCodeAt(i);
                    if (c >= 0x010000 && c <= 0x10FFFF) {
                        byteNo += 4;
                    } else if (c >= 0x000800 && c <= 0x00FFFF) {
                        byteNo += 3;
                    } else if (c >= 0x000080 && c <= 0x0007FF) {
                        byteNo += 2;
                    } else {
                        byteNo += 1;
                    }
                    if ((byteNo % MAX_ENCRYPT_BLOCK) >= 114 || (byteNo % MAX_ENCRYPT_BLOCK) == 0) {
                        if (byteNo - temp >= 114) {
                            bytes.push(i);
                            temp = byteNo;
                        }
                    }
                }

                //2.擷取字串並分段加密
                if (bytes.length > 1) {
                    for (var i = 0; i < bytes.length - 1; i++) {
                        var str;
                        if (i == 0) {
                            str = string.substring(0, bytes[i + 1] + 1);
                        } else {
                            str = string.substring(bytes[i] + 1, bytes[i + 1] + 1);
                        }
                        var t1 = k.encrypt(str);
                        ct += t1;
                    }
                    ;
                    if (bytes[bytes.length - 1] != string.length - 1) {
                        var lastStr = string.substring(bytes[bytes.length - 1] + 1);
                        ct += k.encrypt(lastStr);
                    }
                    return hex2b64(ct);
                }
                var t = k.encrypt(string);
                var y = hex2b64(t);
                return y;
            } catch (ex) {
                return ex;
            }
        };


完整的jsencrypt.js 上傳到百度雲盤了。
鏈接: https://pan.baidu.com/s/1Savj-671W8dOZzbNDRxtyA 提取碼: 9t9e