在项目中,为了保证数据的安全,我们常常会对传递的数据进行加密。常用的加密算法有对称加密(AES)和非对称加密(RSA)。

AES

AES加密算法

对称加密就是指,加密和解密使用同一个密钥的加密方式;加密计算量小、速度块,适合对大量数据进行加密的场景。AES取代DES成为加密标准(微信小程序加密传输就是用这个加密算法的)

AES加密的五个关键词

  • 分组密码体制

    AES采用分组密码体制,即AES加密会首先把明文切成一段一段的,而且每段数据的长度要求必须是128位16个字节,如果最后一段不够16个字节了,就需要用Padding来把这段数据填满16个字节,然后分别对每段数据进行加密,最后再把每段加密数据拼起来形成最终的密文。

  • Padding

    Padding就是用来把不满16个字节的分组数据填满16个字节用的,它有三种模式PKCS5、PKCS7和NOPADDING。解密端需要使用和加密端同样的Padding模式,才能准确的识别有效数据和填充数据。开发通常采用PKCS7 Padding模式。

  • 初始向量IV

    初始向量IV的作用是使加密更加安全可靠,我们使用AES加密时需要主动提供初始向量,而且只需要提供一个初始向量就够了,后面每段数据的加密向量都是前面一段的密文。初始向量IV的长度规定为128位16个字节,初始向量的来源为随机生成。

  • 密钥

    开发通常采用128位16个字节的密钥,我们使用AES加密时需要主动提供密钥,而且只需要提供一个密钥就够了,每段数据加密使用的都是这一个密钥,密钥来源为随机生成。

  • 四种加密模式

    AES一共有四种加密模式,分别是ECB(电子密码本模式)、CBC(密码分组链接模式)、CFB、OFB,一般使用的是ECB、CBC模式

注意事项

  1. 服务端和我们客户端必须使用一样的密钥和初始向量IV。
  2. 服务端和我们客户端必须使用一样的加密模式。
  3. 服务端和我们客户端必须使用一样的Padding模式。
    以上三条有一个不满足,双方就无法完成互相加解密。

RSA

RSA加密算法

RSA加密是一种非对称加密。可以在不直接传递密钥的情况下,完成解密。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。
RSA是由一对密钥来进行加解密的过程,分别称为公钥和私钥。两者之间有数学相关,该加密算法的原理就是对一极大整数做因数分解的困难性来保证安全性。通常个人保存私钥,公钥是公开的(可能同时多人持有)。

RSA签名

加密和签名都是为了安全性考虑,但略有不同。常有人问加密和签名是用私钥还是公钥?其实都是对加密和签名的作用有所混淆。简单的说,加密是为了防止信息被泄露,而签名是为了防止信息被篡改。这里举2个例子说明。

第一个场景

B(客户端)要给A(服务器)传递一条重要消息。

RSA的加密过程如下:

  1. A生成一对密钥(公钥和私钥),私钥不公开,A自己保留。公钥为公开的,任何人可以获取。
  2. A传递自己的公钥给B,B用A的公钥对消息进行加密。
  3. A接收到B加密的消息,利用A自己的私钥对消息进行解密。

在整个过程中,只有2次传递过程,第一次是A传递公钥给B,第二次是B用公钥加密消息传递给A,即使都被敌方截获,也没有危险性,因为只有A的私钥才能对消息进行解密,防止了消息内容的泄露。

第二个场景

A(服务器)收到B(客户端)发的消息后,需要进行回复“收到”

RSA签名的过程如下:

  1. A生成一对密钥(公钥和私钥),私钥不公开,A自己保留。公钥为公开的,任何人可以获取。
  2. A用自己的私钥对消息加签,形成签名,并将加签的消息和消息(加密后的信息)本身一起传递给B。
  3. B收到消息后,在获取A的公钥进行验签,如果验签出来的内容与消息本身一致,证明消息是A回复的。

在整个过程中,只有2次传递过程,第一次是A传递加签的消息和消息(加密后的信息)本身给B,第二次是B获取A的公钥,即使都被敌方截获,也没有危险性,因为只有A的私钥才能对消息进行签名;即使知道了消息内容(加密后的信息),也无法伪造带签名的回复给B,防止了消息内容的篡改。

存在的缺陷

综合两个场景你会发现:

  1. 第一个场景虽然被截获的消息没有泄露,但是黑客可以利用截获的公钥,将假信息进行加密,然后传递给A。
  2. 第二个场景虽然截获的消息不能被篡改,但是消息的内容可以利用公钥解密来获得,并不能防止泄露。

所以在实际应用中,要根据情况灵活使用,可以同时使用加密和签名,也可以A和B都有一套自己的公钥和私钥(这里的A、B指服务端,前端的话不安全),当A要给B发送消息时,先用B的公钥对消息加密,再对加密的消息使用A的私钥来加签名,达到既不泄露也不被篡改,更能保证消息的安全性。

项目详情

依赖文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-boot-project</artifactId>
<groupId>cn.goitman</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>rsa-encrypt-demo</artifactId>

<dependencies>
<!--Web 项目开发starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--RSA加解密依赖-->
<dependency>
<groupId>cn.shuibo</groupId>
<artifactId>rsa-encrypt-body-spring-boot</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>

<!--lombok 简化插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

配置文件

server:
port: 8080

rsa:
encrypt:
open: true # 是否开启加密 true or false
showLog: true # 是否打印加解密log true or false
# RSA公钥 java生成
publicKey:
# RSA私钥 java生成
privateKey:

RSA公私钥生成工具在后续工具类中

引导类

package cn.goitman;

import cn.shuibo.annotation.EnableSecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author Nicky
* @version 1.0
* @className RsaEncryptApplication
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 启动引导类
* @date 2020/11/2 16:31
*/
@SpringBootApplication
@EnableSecurity // 启用加解密注解
public class RsaEncryptApplication {
public static void main(String[] args) {
SpringApplication.run(RsaEncryptApplication.class, args);
}
}

实体类

package cn.goitman.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author Nicky
* @version 1.0
* @className Consumer
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 实体类
* @date 2020/11/2 16:40
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Consumer {

private String name;
private String passWord;

}

工具类

由于RSA的特性,一个1024位的密钥只能加密117位字节数据和解密128位字节数据,当数据量超过117位字节或128位字节的时候,程序就会抛出异常

package cn.goitman.util;

import org.apache.commons.codec.binary.Base64;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
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.HashMap;
import java.util.Map;

/**
* @author Nicky
* @version 1.0
* @className CreateRsaSecrteKeyUtil
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 生成RSA公钥私钥,RSA与AES加解密
* @date 2020/11/2 15:51
*/

public class CreateRsaSecrteKeyUtil {

private static final String PUBLIC_KEY = "RSAPublicKey";
private static final String PRIVATE_KEY = "RSAPrivateKey";

// RSA算法名称
public static final String RSA_ALGORITHM = "RSA";

// RSA填充算法
public static final String RSA_PADDING_ALGORITHM = "RSA/ECB/PKCS1Padding";
// public static final String RSA_PADDING_ALGORITHM = "RSA/CBC/PKCS1Padding";

// RSA加签算法
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";

// RSA最大加密明文大小
private static final int MAX_ENCRYPT_BLOCK = 117;

// RSA最大解密密文大小
private static final int MAX_DECRYPT_BLOCK = 128;

// 编码方式
public static final String UTF_8 = "UTF-8";

// AES算法名称
public static final String AES_ALGORITHM = "AES";

// AES填充算法
// public static final String AES_PADDING_ALGORITHM = "AES/ECB/PKCS5Padding";
public static final String AES_PADDING_ALGORITHM = "AES/CBC/PKCS5Padding";

/**
只有在填充算法模式为CBC时才使用
初始向量,不可以为32位,只能为数字或字母
否则异常java.security.InvalidAlgorithmParameterException: Wrong IV length: must be 16 bytes long
*/
private final static String iv = "初始向量";


/**
* map对象中存放公私钥
*/
public static Map<String, Object> initKey() throws Exception {
//获得对象 KeyPairGenerator 参数 RSA
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
// keysize:表示的是生成key的长度,单位字节(bits,64的整数倍,最小512位)
// 如果采用2048,上面最大加密和最大解密则为:245、256
keyPairGen.initialize(1024);
//通过对象 KeyPairGenerator 获取对象KeyPair
KeyPair keyPair = keyPairGen.generateKeyPair();

//通过对象 KeyPair 获取RSA公私钥对象RSAPublicKey RSAPrivateKey
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
//公私钥对象存入map中
Map<String, Object> keyMap = new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}

/**
* 获得公钥
*/
public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
//获得map中的公钥对象 转为key对象
Key key = (Key) keyMap.get(PUBLIC_KEY);
return encryptBASE64(key.getEncoded());
}

/**
* 获得私钥
*/
public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
//获得map中的私钥对象 转为key对象
Key key = (Key) keyMap.get(PRIVATE_KEY);
return encryptBASE64(key.getEncoded());
}

/**
* @param [key 字符串]
* @return byte[]
* @method decryptBASE64
* @description BASE64字符串解码为二进制数据
*/
public static byte[] decryptBASE64(String key) throws Exception {
return (new BASE64Decoder()).decodeBuffer(key);
}

/**
* @param [key 字节数组]
* @return java.lang.String
* @method encryptBASE64
* @description 二进制数据编码为BASE64字符串
*/
public static String encryptBASE64(byte[] key) throws Exception {
return (new BASE64Encoder()).encodeBuffer(key);
}

/**
* @param [data 需加密数据, publicKey 公钥]
* @return java.lang.String
* @method encryptRSADatePub
* @description 分段公钥加密数据
*/
public static String encryptRSADate(String data, String publicKey) {
try {
byte[] dataByte = data.getBytes();
byte[] keyBytes = decryptBASE64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(RSA_PADDING_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicK);

// IvParameterSpec ivp = new IvParameterSpec(iv.getBytes());
// cipher.init(Cipher.ENCRYPT_MODE, publicK, ivp);

// 分段加密
int inputLen = dataByte.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(dataByte, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataByte, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptBASE64(encryptedData);
} catch (Exception e) {
return e.getMessage();
}
}

/**
* @param [data 需解密数据, privateKey 私钥]
* @return java.lang.String
* @method decryptRSADate
* @description 分段解密数据
*/
public static String decryptRSADate(String data, String privateKey) {
try {
byte[] dataByte = decryptBASE64(data);
byte[] keyBytes = decryptBASE64(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(RSA_PADDING_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateK);

// IvParameterSpec ivp = new IvParameterSpec(iv.getBytes());
// cipher.init(Cipher.DECRYPT_MODE, privateK, ivp);

// 分段解密
int inputLen = dataByte.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(dataByte, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher
.doFinal(dataByte, 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);
} catch (Exception e) {
return e.getMessage();
}
}

/**
* @param [data 签名数据, privateKey 私钥]
* @return String
* @method sign
* @description 签名
*/
public static String sign(String data, String privateKey) {
try {
// String转私钥对象
byte[] keyBytes = decryptBASE64(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PrivateKey privateK = keyFactory.generatePrivate(keySpec);

// 获取加签对象
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initSign(privateK);
sig.update(data.getBytes(UTF_8));
return encryptBASE64(sig.sign());
} catch (Exception e) {
return e.getMessage();
}
}

/**
* @param [data 验签数据, sign 签名, publicKey 公钥]
* @return boolean
* @method verify
* @description 验签
*/
public static boolean verify(String data, String sign, String publicKey) {
try {
// String转公钥对象
byte[] keyBytes = decryptBASE64(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);

// 获取加签对象
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicK);
sig.update(data.getBytes(UTF_8));
return sig.verify(decryptBASE64(sign));
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

/**
* @method encryptAES
* @description AES 加密
* @param [context 明文, key 密钥8位的倍数,建议16位]
* @return java.lang.String
*/
public static String encryptAES(String context, String key) {
try {
byte[] decode = context.getBytes(UTF_8);
Key secretKeySpec = new SecretKeySpec(key.getBytes(UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_PADDING_ALGORITHM);
// cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);

IvParameterSpec ivp = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivp);

return Base64.encodeBase64String(cipher.doFinal(decode));
} catch (Exception e) {
return e.getMessage();
}
}

/**
* @method decryptAES
* @description AES 解密
* @param [context 密文, key 密钥8位的倍数,建议16位]
* @return java.lang.String
*/
public static String decryptAES(String context, String key) {
try {
byte[] decode = Base64.decodeBase64(context);
Key secretKeySpec = new SecretKeySpec(key.getBytes(UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_PADDING_ALGORITHM);
// cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);

IvParameterSpec ivp = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivp);
return new String(cipher.doFinal(decode), UTF_8);
} catch (Exception e) {
return e.getMessage();
}
}

/**
* @method randomKey
* @description 创建字符串密钥
*/
public static String randomKey(int lenght) {
String str = "";
char[] c = null;
for (short i = '0'; i <= '9'; i++) {
str += (char) i;
}
for (short i = 'A'; i <= 'Z'; i++) {
str += (char) i;
}
for (short i = 'a'; i <= 'z'; i++) {
str += (char) i;
}

c = new char[lenght];
for (int i = 0; i < c.length; i++) {
int index = (int) (Math.random() * str.length());
c[i] = str.charAt(index);
}
System.out.println("密钥:" + new String(c) + "\n");

return new String(c);
}


public static void main(String[] args) {
Map<String, Object> keyMap;
try {
keyMap = initKey();
String publicKey = getPublicKey(keyMap);
System.out.println("公钥:" + publicKey + "\n");
String privateKey = getPrivateKey(keyMap);
System.out.println("私钥:" + privateKey + "\n");

System.out.println("===================先加密再加签===================");
String str = "签名内容!";
// 公钥加密
String encryptData = encryptRSADate(str, publicKey);
System.out.println("公钥加密:" + encryptData + "\n");
// 私钥对密文加签
String sign = sign(encryptData, privateKey);
System.out.println("加签后:" + sign + "\n");
// 公钥对密文验签
boolean verify = verify(encryptData, sign, publicKey);
System.out.println("验签情况:" + verify + "\n");

// 私钥解密
String decryptData = decryptRSADate("WXpNTk5yTU90NHJiQWwyZlBzNnJKdC9ZWk80YUJyWiswS1orUjR1clYxa2JuVVJRNnN0eUh4ekNVQlM0cFUxK3R6dU1PYVlsZkZQM2ZHaHB4VXNPbFRRbXZwWithUjVVZWtLVWRic2hqV0FweUpkNDZaLy9XTnVLMk9uYmhTMGdWMkZIK1NHUlc4L0dnR2ZPdmpOVjJrYmkzS25aQWdkUFRKT3NwTlowQ0owPQ==", privateKey);
System.out.println("私钥解密:" + decryptData);

System.out.println(randomKey(16));

} catch (Exception e) {
e.printStackTrace();
}
}
}

控制层类

package cn.goitman.controller;

import cn.goitman.pojo.Consumer;
import cn.goitman.util.CreateRsaSecrteKeyUtil;
import cn.shuibo.annotation.Decrypt;
import cn.shuibo.annotation.Encrypt;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
* @author Nicky
* @version 1.0
* @className RsaCotroller
* @blog goitman.cn | blog.csdn.net/minkeyto
* @date 2020/11/2 16:43
*/
@RestController
public class RsaCotroller {

@Value("${rsa.encrypt.privateKey}")
private String privateKey;

@Value("${rsa.encrypt.publicKey}")
private String publicKey;

/**
* 注解加密
*/
@Encrypt
@GetMapping("/encrypt")
public String encrypt() {
Consumer consumer = new Consumer("注解加密用户", "注解加密密码");
return consumer.toString();
}

/**
* 注解解密
*/
@Decrypt
@PostMapping("/decrypt")
public String decrypt(@RequestBody Consumer consumer) {
return consumer.toString();
}

/**
* 手工加密
*/
@GetMapping("/encryption")
public String encryption() {
Consumer consumer = new Consumer("手工加密用户", "手工加密密码");
// 因前端传送的是JSON数据,所以需先将对象转为JSON后,再加密
return CreateRsaSecrteKeyUtil.encryptRSADate(JSON.toJSONString(consumer), publicKey);
}

/**
* 手动解密
*/
@PostMapping("/decryption")
public String decryption(@RequestBody String str) {
Consumer consumer = JSON.parseObject(CreateRsaSecrteKeyUtil.decryptRSADate(str, privateKey), Consumer.class);
return consumer.toString();
}

/**
* 手工加密加签
*/
@GetMapping("/signature")
public String signature() {
Consumer consumer = new Consumer("手工加密加签", "手工加密加签");
// 因前端传送的是JSON数据,所以需先将对象转为JSON后,再加密
String encryptDate = CreateRsaSecrteKeyUtil.encryptRSADate(JSON.toJSONString(consumer), publicKey);
String sign = CreateRsaSecrteKeyUtil.sign(encryptDate, privateKey);
System.out.println("密文:" + encryptDate + "\n");
System.out.println("加签:" + sign);
return sign + "&" + encryptDate;
}

/**
* 手动解签解密
*/
@PostMapping("/verify")
public String verify(@RequestBody String str) {
String[] data = str.split("&");
boolean verify = CreateRsaSecrteKeyUtil.verify(data[1], data[0], publicKey);
if (verify) {
Consumer consumer = JSON.parseObject(CreateRsaSecrteKeyUtil.decryptRSADate(data[1], privateKey), Consumer.class);
return "验签成功:" + consumer.toString();
} else {
return "验签失败!";
}
}

/**
* AES加密,前端解密
*/
@GetMapping("/encodeAES")
public String encodeAES() {
Consumer consumer = new Consumer("手工加密用户", "手工加密密码");
String key = CreateRsaSecrteKeyUtil.randomKey(16);
String data = CreateRsaSecrteKeyUtil.encryptAES(consumer.toString(), key);
try {
return data + "&" + key;
} catch (Exception e) {
e.printStackTrace();
}
return "加密失败!";
}

/**
* AES解密,前端加密
*/
@PostMapping("/decodeAES")
public String decodeAES(@RequestBody String str, HttpServletRequest req) {
// 密钥用rsa加密过
String encodeKey = req.getHeader("encodeKey");
try {
String key = CreateRsaSecrteKeyUtil.decryptRSADate(encodeKey, privateKey).replace("\"", "");
return CreateRsaSecrteKeyUtil.decryptAES(str, key);
} catch (Exception e) {
return "解密失败!";
}
}
}

注解加解密不能和手工加解密签一起使用,会有冲突,后面测试章节会介绍

请求页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>非对称加解密加验签与对称加解密</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>

<!--非对称、对称加解密-->
<script src="js/jsencrypt.js"></script>
<!--非对称加验签-->
<script src="js/jsrsasign-all-min.js"></script>
<!--对称加解密-->
<script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script>
<!-- <script src="js/crypto-js.js"></script>-->

</head>
<body>
<button id="jiami" onclick="zhujie1()">前端解密,后端 注解 加密</button>
<button id="jiami1" onclick="zhujie2()">前端加密,后端 注解 解密</button>

<button id="jiami3" onclick="shoudong1()">前端解密,后端 手动 加密</button>
<button id="jiami4" onclick="shoudong2()">前端加密,后端 手动 解密</button>

<button id="jiami5" onclick="jiaqian1()">前端验签解密,后端 手动 加密加签</button>
<button id="jiami6" onclick="jiaqian2()">前端加密加签,后端 手动 验签解密</button>

<button id="jiami7" onclick="aesDecode()">AES后端加密,前端解密</button>
<button id="jiami8" onclick="aesEncode()">AES前端加密,后端解密</button>

<script>

var PUBLIC_KEY = '';

var PRIVATE_KEY = ''

var ALGORITHM = 'MD5withRSA';

/**
* 加密方法
*/
function RSA_encryption(jsonData) {
var encrypt = new JSEncrypt();
encrypt.setPublicKey(PUBLIC_KEY);
var encrypted = encrypt.encryptLong(JSON.stringify(jsonData));
console.log('加密前数据:%o', jsonData);
console.log('加密后数据:%o', encrypted);
console.log('加密前数据:%o', jsonData.length + " "+encrypted.length);
return encrypted;
}

/**
* 私钥解密方法
*/
function RSA_decryption(jsonData) {
var decrypt = new JSEncrypt();
decrypt.setPrivateKey(PRIVATE_KEY);
var decrypted = decrypt.decryptLong(jsonData);
console.log('解密前数据:%o', jsonData);0
console.log('解密后数据:%o', decrypted);
return decrypted;
}

/**
* 私钥加签
*/
function RSA_sign(encryptDate) {
// prvkeypem:传入pem标准格式的秘钥字符串, 解析生成秘钥实例: RSAKey. 标准的pem格式秘钥含有开始标记和结束标记, 如本文使用的秘钥: -----BEGIN xxx-----, -----END xxx-----. 至于xxx的具体内容不是太重要, 代码里自动通过正则清洗掉头和尾标记
var signature = new KJUR.crypto.Signature({alg: ALGORITHM, prvkeypem: '-----BEGIN PRIVATE KEY-----' + PRIVATE_KEY + '-----END PRIVATE KEY-----'});
signature.updateString(encryptDate); // 传入待签明文
var sign = hextob64(signature.sign()); // 签名, hextob64得到16进制字符结果
console.log('私钥加签数据:%o', sign);
return sign + "&" + encryptDate;
}


/**
* 公钥验签
*/
function RSA_verify(encryptDate, sign) {
var signature = new KJUR.crypto.Signature({alg: ALGORITHM, prvkeypem: '-----BEGIN PUBLIC KEY-----' + PUBLIC_KEY + '-----END PUBLIC KEY-----'});
signature.updateString(encryptDate); // 传入签密文
var result = signature.verify(b64tohex(sign));// 解签, b64tohex得到字符结果
console.log('加签:%o', sign);
console.log('验签结果:%o', result);
return result;
}

// ====================================================================================================
/**
* 前端解密,后端 注解 加密
*/
function zhujie1() {
$.ajax({
url: "/encrypt",
type: "GET",
contentType: "application/json;charset=utf-8",
success: function (reslut) {
var res = RSA_decryption(reslut)
alert(res);
}
})
}

/**
* 前端加密,后端 注解 解密
*/
function zhujie2() {
var str = {
"name": "前端加密用户",
"passWord": "前端加密密码"
};
$.ajax({
url: "/decrypt",
type: "POST",
contentType: "application/json;charset=utf-8",
data: RSA_encryption(str),
success: function (data) {
alert(data);
}
})
}

// ====================================================================================================
/**
* 前端解密,后端 手动 加密
*/
function shoudong1() {
$.ajax({
url: "/encryption",
type: "GET",
contentType: "application/json;charset=utf-8",
success: function (reslut) {
var res = RSA_decryption(reslut)
alert(res);
}
})
}

/**
* 前端加密,后端 手动 解密
*/
function shoudong2() {
var str = {
"name": "加密用户1",
"passWord": "加密密码1"
};
$.ajax({
url: "/decryption",
type: "POST",
contentType: "application/json;charset=utf-8",
data: RSA_encryption(str),
success: function (data) {
alert(data);
}
})
}

// ====================================================================================================
/**
* 前端验签解密,后端 手动 加密加签
*/
function jiaqian1() {
$.ajax({
url: "/signature",
type: "GET",
contentType: "application/json;charset=utf-8",
success: function (reslut) {
var res = reslut.split("&");
var re = RSA_verify(res[1], res[0]);
var encryptDate = JSON.stringify(RSA_decryption(res[1]));
alert(re + ": " + encryptDate);
}
})
}

/**
* 前端加密加签,后端 手动 验签解密
*/
function jiaqian2() {
var str = {
"name": "手动解签解密1",
"passWord": "手动解签解密1"
};
var encryptDate = RSA_encryption(str);
var sign = RSA_sign(encryptDate)
$.ajax({
url: "/verify",
type: "POST",
contentType: "application/json;charset=utf-8",
data: sign,
success: function (data) {
alert(data);
}
})
}

// ====================================================================================================

/**
* 生成随机密钥
*/
function randomString(length) {
var str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
var result = '';
for (var i = length; i > 0; --i)
result += str[Math.floor(Math.random() * str.length)];
return result.toString();
}

//aes加密
function encryptAES(context, key) {
var encrypted = '';
if (typeof (context) == 'string') {

} else if (typeof (context) == 'object') {
context = JSON.stringify(context);
}
var srcs = CryptoJS.enc.Utf8.parse(context);
encrypted = CryptoJS.AES.encrypt(srcs, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse("初始向量"),
// mode: CryptoJS.mode.ECB,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}

// aes解密
function decryptAES(context, key) {
var decrypt = CryptoJS.AES.decrypt(context, CryptoJS.enc.Utf8.parse(key), {
iv:CryptoJS.enc.Utf8.parse("初始向量"),
// mode: CryptoJS.mode.ECB,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
}

/**
* AES后端加密,前端解密
*/
function aesDecode() {
$.ajax({
url: "/encodeAES",
type: "GET",
contentType: "application/json;charset=utf-8",
success: function (reslut) {
var res = reslut.split("&");
var key = res[1];
var encryptDate = decryptAES(res[0], key);
alert(encryptDate + " : " + key);
}
})
}

/**
* AES前端加密,后端解密
*/
function aesEncode() {
var k = randomString(16);
var str = {
"name": "AES加解密",
"passWord": "AES加解密"
};
var encryptDate = encryptAES(str,k)
$.ajax({
url: "/decodeAES",
type: "POST",
contentType: "application/json;charset=utf-8",
headers: {
"encodeKey": RSA_encryption(k)
},
data: encryptDate,
success: function (data) {
alert(data);
}
})
}
</script>
</body>
</html>

注意事项:

  1. 第36行加验签算法需跟工具类中的加验签算法一致,否则解签失败
  2. 第68、80行加签验签方法中参数prvkeypem,必须传入pem标准格式的秘钥字符串,否则加验签失败;标准的pem格式含有开始标记和结束标记,如本文使用的秘钥:—–BEGIN xxx—–,—–END xxx—–。至于xxx的具体内容不是太重要,代码里自动通过正则清洗掉头和尾标记
  3. 第220、231行的iv初始向量必须与CreateRsaSecrteKeyUtil工具类中第64行中定义的iv初始向量一致,否则加解密失败

测试

注解加解密和手工加解密签一起使用产生冲突问题

按照1、2、3的步骤来测试验证下效果,首先请求后端注解解密接口

毫无疑问,参数解密成功,直接返回前端,并弹框显示

第2步,请求后端手动解密接口;咦!接收参数怎么就已经解密啦?来debug

原来第1步调起的解密注解,既然还在工作,喝,好家伙把第2步的请求参数也顺带解密啦,导致工具类解密时报出JSON数据异常

好吧,接着第3步,请求后端手动解签解密接口;我去,数据呢?怎么空字符串啦?

前端明明正常发送请求数据了啊,上debug

还是解密注解造的孽,它无法解密加签的数据(加签算法和加密算法不一样,看工具类详情可知),所以直接报解密失败,并返回个空字符串

结论:只要开启了解密注解,它就会一直保持开启解密状态,从而影响手动解密方法的正常使用

小程序非对称加密

在上请求页面中RSA加解密使用了jsencrypt.js,其中有如下一段代码:

因为微信小程序的js中不能使用window对象,会导致报undefined错误(难道window对象真的在小程序中不存在吗?事实并非如此,有兴趣的可以去研究下,在这不作扩展),所以需换个思路来解决问题,使用wx_rsa.js(源码中已附文件)来实现非对称加密,使用方法如下

JS引用

var RSA = require('../wx_rsa.js')

加密

function wx_RSA_encrypto(jsonData) {
var encrypt_rsa = RSA.KEYUTIL.getKey("-----BEGIN PUBLIC KEY-----公钥-----END PUBLIC KEY-----");
var encStr = encrypt_rsa.encrypt(jsonData);
return RSA.hex2b64(encStr);
}

解密

客户端一般不推荐保存私钥,此为示例

function wx_RSA_decrypto(encStr) {
decrypt_rsa = RSA.KEYUTIL.getKey("-----BEGIN PRIVATE KEY-----私钥-----END PRIVATE KEY-----");
encStr = RSA.b64tohex(encStr);
var decStr = decrypt_rsa.decrypt(encStr);
return decStr;
}

至于加签验签可用回上节请求页面中的方法,当然也可用wx_rsa.js中的方法,在此不做扩展,自行研究调用

源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/rsa-encrypt-demo