JAVA RSA 前后端代码实现践行记录

有时候后端需要通过回调来与前端交互,但回调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解密一次,并比较前后端加密串是否一致,一致后方可进行后端解密调试,否则会报错。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值