有时候后端需要通过回调来与前端交互,但回调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
请注意!!!!!!
实际操作的时候需要注意,前端加密串是否后端接收的是正确的加密串,url传输中会减少 + / =等特殊符号。
如果需要前端加密,后端解密,前端需要使用
encodeURIComponent(encodeURIComponent(encrypt.encryptLong(params_str)))
encodeURIComponent加密两次,后端使用URLDecoder.decoder解密一次,并比较前后端加密串是否一致,一致后方可进行后端解密调试,否则会报错。