有時候後端需要通過回撥來與前端互動,但回撥url上往往有關鍵性的資訊例如使用者的token,爲了防止此鏈接被惡意攔截反覆 反復使用,有必要將關鍵參數加上時間戳並用加密演算法加密與前端互動。前端可以控制時間戳大於多少分鐘則忽略此token,攔截者不知道金鑰情況下無法僞造加密文,就可以避免此鏈接反覆 反復被使用。因爲前端程式碼能被破解故而使用非對稱加密演算法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() 來解碼
注意、由於前端使用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