前后端数据加密传输基于AES+RSA实现

前后端数据加密传输基于AES+RSA实现

什么是AES和RSA

AES

AES(Advanced Encryption Standard)是一种对称加密算法,它的加密速度快,安全性也比较高,是目前广泛使用的加密算法之一。AES的密钥长度可以选择128位、192位和256位,其中128位和192位的安全性略低,但加密速度更快,而256位的安全性最高,但加密速度相对较慢。AES的加密过程是将明文数据通过密钥进行“混淆”处理,使其变成无法被识别的密文数据。

RSA

RSA(Rivest-Shamir-Adleman)是一种非对称加密算法,它的加密速度慢,但安全性极高,是用于保护敏感数据的常用算法。RSA的加密过程是先使用一个公钥(public key)将明文数据进行加密,然后再使用一个私钥(private key)将加密后的密文进行解密。由于公钥是公开的,因此可以提供给任何人使用,而私钥是需要保密的,只有私钥的持有者才能够解密数据。RSA的密钥长度通常为2048位或更长,可以提供足够的安全性。

前后端加密实现

这里前后加解密过程是以vue+springBoot为例实现的

  • 案例只针对post请求

  • 这里使用’Content-Type’: ‘application/x-www-form-urlencoded; charset=UTF-8’;为键值对的形式(非json)

  • AES加密数据,RAS加密AES的key

实现思路

  1. 前台首先请求非加密接口获取后台的公钥
  2. 前台在请求前生成自己的公钥和私钥,以及AES对称加密的key
  3. 使用前台生成的aeskey对数据进行加密
  4. 在请求前使用后台的公钥对前台的aeskey进行加密
  5. 将前台加密的data、aeskey和前台公钥一起传递给后台
  6. 后台使用私钥对前台的aeskey进行解密,再用这个aeskey去解密data
  7. 后台如果需要返回数据,这时使用后台生成的aeskey对数据进行加密
  8. 后端使用前台的公钥对aeskey进行加密
  9. 将aeskey和加密后的数据一起返还给前台,由前台使用私钥解密获得后端的aeskey
  10. 再使用后端的aeskey解密数据

通过这种方式,前后端交互的数据在传输过程中都经过了加密和解密的过程,保证了数据的安全性。

后台(Springboot)

在实际开发中,我们不应该在每一个接口都单独调用加密解密方法,这样太臃肿了。我们应该将重复代码进行抽离(事不过三,三则重构),这里我们可以使用AOP(切面)来进行处理。比如,我们可以定义一个切面类来统一处理加密解密的逻辑,然后在需要加密解密的方法上面声明该切面类,即可自动在方法执行前后执行加密解密的逻辑,避免了重复的代码。

maven依赖

在springboot项目中使用AOP只要引入aop-starter依赖就行:

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

加解密用的依赖:

        <dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.56</version></dependency>

测试项目完整pom.xml依赖如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.sun</groupId><artifactId>springboot</artifactId><version>0.0.1-SNAPSHOT</version><name>gis</name><description>Demo project for Spring Boot</description><properties><java.version>8</java.version><log4j2.version>2.17.0</log4j2.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- Spring Boot AOP Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>19.0</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.76</version></dependency><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.56</version></dependency><dependency><groupId>org.apache.directory.studio</groupId><artifactId>org.apache.commons.codec</artifactId><version>1.8</version></dependency></dependencies><repositories><repository><id>alimaven</id><name>aliyun maven</name><url>http://maven.aliyun.com/nexus/content/groups/public/</url><snapshots><enabled>false</enabled></snapshots></repository></repositories><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><!-- 打成jar包自动排除yml配置 可在jar同级目录下(同级目录/config下) 配置yml --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><excludes><!--不打包的内容文件--><exclude>*.yml</exclude></excludes></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.0.0-M3</version><configuration><!-- 设置默认跳过测试 --><skip>true</skip><includes><include>**/*Tests.java</include></includes><excludes><exclude>**/Abstract*.java</exclude></excludes><systemPropertyVariables><java.security.egd>file:/dev/./urandom</java.security.egd><java.awt.headless>true</java.awt.headless></systemPropertyVariables></configuration></plugin></plugins></build></project>

加解密工具类

这里封装几个常用工具类:

AES工具类:AesUtil
package com.sun.springboot.util;import org.apache.tomcat.util.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Random;/*** @author sungang* @date 2021/10/15 1:58 下午* AES加、解密算法工具类* 对称加密*/
public class AesUtil {/*** 加密算法AES*/private static final String KEY_ALGORITHM = "AES";/*** key的长度,Wrong key size: must be equal to 128, 192 or 256* 传入时需要16、24、36*/private static final int KEY_LENGTH = 16 * 8;/*** 算法名称/加密模式/数据填充方式* 默认:AES/ECB/PKCS5Padding*/private static final String ALGORITHMS = "AES/ECB/PKCS5Padding";/*** 后端AES的key,由静态代码块赋值*/public static String key;/*** 不能在代码中创建* JceSecurity.getVerificationResult 会将其put进 private static final Map<Provider,Object>中,导致内存缓便被耗尽*/private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();static {key = getKey();}/*** 获取key*/public static String getKey() {int length = KEY_LENGTH / 8;StringBuilder uid = new StringBuilder(length);//产生16位的强随机数Random rd = new SecureRandom();for (int i = 0; i < length; i++) {//产生0-2的3位随机数switch (rd.nextInt(3)) {case 0://0-9的随机数uid.append(rd.nextInt(10));break;case 1://ASCII在65-90之间为大写,获取大写随机uid.append((char) (rd.nextInt(26) + 65));break;case 2://ASCII在97-122之间为小写,获取小写随机uid.append((char) (rd.nextInt(26) + 97));break;default:break;}}return uid.toString();}/*** 加密** @param content    加密的字符串* @param encryptKey key值*/public static String encrypt(String content, String encryptKey) throws Exception {//设置Cipher对象Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));//调用doFinal// 转base64return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));}/*** 解密** @param encryptStr 解密的字符串* @param decryptKey 解密的key值*/public static String decrypt(String encryptStr, String decryptKey) throws Exception {//base64格式的key字符串转bytebyte[] decodeBase64 = Base64.decodeBase64(encryptStr);//设置Cipher对象Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));//调用doFinal解密return new String(cipher.doFinal(decodeBase64));}}
RSA工具类:RsaUtil
package com.sun.springboot.util;import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
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.LinkedHashMap;
import java.util.Map;/*** @author sungang* @date 2021/10/15 2:04 下午* RSA加、解密算法工具类* 非对称加密*/
@Slf4j
public class RsaUtil {/*** 加密算法AES*/private static final String KEY_ALGORITHM = "RSA";/*** 算法名称/加密模式/数据填充方式* 默认:RSA/ECB/PKCS1Padding*/private static final String ALGORITHMS = "RSA/ECB/PKCS1Padding";/*** Map获取公钥的key*/private static final String PUBLIC_KEY = "publicKey";/*** Map获取私钥的key*/private static final String PRIVATE_KEY = "privateKey";/*** RSA最大加密明文大小*/private static final int MAX_ENCRYPT_BLOCK = 117;/*** RSA最大解密密文大小*/private static final int MAX_DECRYPT_BLOCK = 128;/*** RSA 位数 如果采用2048 上面最大加密和最大解密则须填写:  245 256*/private static final int INITIALIZE_LENGTH = 1024;/*** 后端RSA的密钥对(公钥和私钥)Map,由静态代码块赋值*/private static Map<String, Object> genKeyPair = new LinkedHashMap<>(2);static {try {genKeyPair.putAll(genKeyPair());} catch (Exception e) {//输出到日志文件中log.error(ErrorUtil.errorInfoToString(e));}}/*** 生成密钥对(公钥和私钥)*/private static Map<String, Object> genKeyPair() throws Exception {KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);keyPairGen.initialize(INITIALIZE_LENGTH);KeyPair keyPair = keyPairGen.generateKeyPair();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();Map<String, Object> keyMap = new HashMap<String, Object>(2);//公钥keyMap.put(PUBLIC_KEY, publicKey);//私钥keyMap.put(PRIVATE_KEY, privateKey);return keyMap;}/*** 私钥解密** @param encryptedData 已加密数据* @param privateKey    私钥(BASE64编码)*/public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {//base64格式的key字符串转Key对象Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));//设置加密、填充方式/*如需使用更多加密、填充方式,引入<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk16</artifactId><version>1.46</version></dependency>并改成Cipher cipher = Cipher.getInstance(ALGORITHMS ,new BouncyCastleProvider());*/Cipher cipher = Cipher.getInstance(ALGORITHMS);cipher.init(Cipher.DECRYPT_MODE, privateK);//分段进行解密操作return encryptAndDecryptOfSubsection(encryptedData, cipher, MAX_DECRYPT_BLOCK);}/*** 公钥加密** @param data      源数据* @param publicKey 公钥(BASE64编码)*/public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {//base64格式的key字符串转Key对象Key publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(publicKey)));//设置加密、填充方式/*如需使用更多加密、填充方式,引入<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk16</artifactId><version>1.46</version></dependency>并改成Cipher cipher = Cipher.getInstance(ALGORITHMS ,new BouncyCastleProvider());*/Cipher cipher = Cipher.getInstance(ALGORITHMS);cipher.init(Cipher.ENCRYPT_MODE, publicK);//分段进行加密操作return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);}/*** 获取私钥*/public static String getPrivateKey() {Key key = (Key) genKeyPair.get(PRIVATE_KEY);return Base64.encodeBase64String(key.getEncoded());}/*** 获取公钥*/public static String getPublicKey() {Key key = (Key) genKeyPair.get(PUBLIC_KEY);return Base64.encodeBase64String(key.getEncoded());}/*** 分段进行加密、解密操作*/private static byte[] encryptAndDecryptOfSubsection(byte[] data, Cipher cipher, int encryptBlock) throws Exception {int inputLen = data.length;ByteArrayOutputStream out = new ByteArrayOutputStream();int offSet = 0;byte[] cache;int i = 0;// 对数据分段加密while (inputLen - offSet > 0) {if (inputLen - offSet > encryptBlock) {cache = cipher.doFinal(data, offSet, encryptBlock);} else {cache = cipher.doFinal(data, offSet, inputLen - offSet);}out.write(cache, 0, cache.length);i++;offSet = i * encryptBlock;}out.close();return out.toByteArray();}
}
加解密方法工具类:ApiSecurityUtil
package com.sun.springboot.util;import com.sun.springboot.response.AjaxJson;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** API接口 加解密工具类* @author sungang*/
@Slf4j
public class ApiSecurityUtil {/*** API解密*/public static String decrypt(){try {//从RequestContextHolder中获取request对象ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();//AES加密后的数据String data = request.getParameter("data");//后端RSA公钥加密后的AES的keyString aesKey = request.getParameter("aesKey");//后端私钥解密的到AES的keybyte[] plaintext = RsaUtil.decryptByPrivateKey(Base64.decodeBase64(aesKey), RsaUtil.getPrivateKey());aesKey = new String(plaintext);//AES解密得到明文data数据return AesUtil.decrypt(data, aesKey);} catch (Throwable e) {//输出到日志文件中log.error(ErrorUtil.errorInfoToString(e));throw new RuntimeException("ApiSecurityUtil.decrypt:解密异常!");}}/*** API加密*/public static AjaxJson encrypt(Object object){try {//从RequestContextHolder中获取request对象ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();//前端公钥String publicKey = request.getParameter("publicKey");//随机获取AES的key,加密data数据String key = AesUtil.getKey();String dataString;if(object instanceof String){dataString = String.valueOf(object);}else{dataString = JsonUtil.stringify(object);}//随机AES的key加密后的密文String data = AesUtil.encrypt(dataString, key);//用前端的公钥来解密AES的key,并转成Base64String aesKey = Base64.encodeBase64String(RsaUtil.encryptByPublicKey(key.getBytes(), publicKey));return AjaxJson.getSuccessData(JsonUtil.parse("{\"data\":\"" + data + "\",\"aesKey\":\"" + aesKey + "\"}", Object.class));} catch (Throwable e) {//输出到日志文件中log.error(ErrorUtil.errorInfoToString(e));throw new RuntimeException("ApiSecurityUtil.encrypt:加密异常!");}}
}
报错工具类:ErrorUtil
package com.sun.springboot.util;import java.io.PrintWriter;
import java.io.StringWriter;/*** @author sungang* @date 2021/10/15 2:54 下午* 捕获报错日志处理工具类*/
public class ErrorUtil {/*** Exception出错的栈信息转成字符串* 用于打印到日志中*/public static String errorInfoToString(Throwable e) {//try-with-resource语法糖 处理机制try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {e.printStackTrace(pw);pw.flush();sw.flush();return sw.toString();} catch (Exception ignored) {throw new RuntimeException(ignored.getMessage(), ignored);}}
}
JSON工具类:JsonUtil
package com.sun.springboot.util;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;import java.text.SimpleDateFormat;/*** Json工具类* @author sungang*/
@Slf4j
public class JsonUtil {private static ObjectMapper mapper;static{//jacksonmapper = new ObjectMapper();//设置日期格式mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));//禁用空对象转换jsonmapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);//设置null值不参与序列化(字段不被显示)
//        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);}/*** json字符串转对象*/public static <T> T parse(String jsonStr,Class<T> clazz){try {return mapper.readValue(jsonStr, clazz);} catch (Exception e) {//输出到日志文件中log.error(ErrorUtil.errorInfoToString(e));}return null;}/*** 对象转json字符串*/public static String stringify(Object obj){try {return mapper.writeValueAsString(obj);} catch (Exception e) {//输出到日志文件中log.error(ErrorUtil.errorInfoToString(e));}return null;}
}

自定义返回类

AjaxJson.class

package com.sun.springboot.response;import java.io.Serializable;
import java.util.List;/*** ajax请求返回Json格式数据的封装*/
public class AjaxJson implements Serializable{// 序列化版本号private static final long serialVersionUID = 1L;// 成功状态码public static final int CODE_SUCCESS = 200;// 错误状态码public static final int CODE_ERROR = 500;// 警告状态码public static final int CODE_WARNING = 501;// 无权限状态码public static final int CODE_NOT_JUR = 403;// 未登录状态码public static final int CODE_NOT_LOGIN = 401;// 无效请求状态码public static final int CODE_INVALID_REQUEST = 400;// 状态码public int code;// 描述信息public String msg;// 携带对象public Object data;// 数据总数,用于分页public Long dataCount;/*** 返回code* @return*/public int getCode() {return this.code;}/*** 给msg赋值,连缀风格*/public AjaxJson setMsg(String msg) {this.msg = msg;return this;}public String getMsg() {return this.msg;}/*** 给data赋值,连缀风格*/public AjaxJson setData(Object data) {this.data = data;return this;}/*** 将data还原为指定类型并返回*/@SuppressWarnings("unchecked")public <T> T getData(Class<T> cs) {return (T) data;}// ============================  构建  ==================================public AjaxJson(int code, String msg, Object data, Long dataCount) {this.code = code;this.msg = msg;this.data = data;this.dataCount = dataCount;}// 返回成功public static AjaxJson getSuccess() {return new AjaxJson(CODE_SUCCESS, "ok", null, null);}public static AjaxJson getSuccess(String msg) {return new AjaxJson(CODE_SUCCESS, msg, null, null);}public static AjaxJson getSuccess(String msg, Object data) {return new AjaxJson(CODE_SUCCESS, msg, data, null);}public static AjaxJson getSuccessData(Object data) {return new AjaxJson(CODE_SUCCESS, "ok", data, null);}public static AjaxJson getSuccessArray(Object... data) {return new AjaxJson(CODE_SUCCESS, "ok", data, null);}// 返回失败public static AjaxJson getError() {return new AjaxJson(CODE_ERROR, "error", null, null);}public static AjaxJson getError(String msg) {return new AjaxJson(CODE_ERROR, msg, null, null);}// 返回警告public static AjaxJson getWarning() {return new AjaxJson(CODE_ERROR, "warning", null, null);}public static AjaxJson getWarning(String msg) {return new AjaxJson(CODE_WARNING, msg, null, null);}// 返回未登录public static AjaxJson getNotLogin() {return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);}// 返回没有权限的public static AjaxJson getNotJur(String msg) {return new AjaxJson(CODE_NOT_JUR, msg, null, null);}// 返回一个自定义状态码的public static AjaxJson get(int code, String msg){return new AjaxJson(code, msg, null, null);}// 返回分页和数据的public static AjaxJson getPageData(Long dataCount, Object data){return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);}// 返回,根据受影响行数的(大于0=ok,小于0=error)public static AjaxJson getByLine(int line){if(line > 0){return getSuccess("ok", line);}return getError("error").setData(line);}// 返回,根据布尔值来确定最终结果的  (true=ok,false=error)public static AjaxJson getByBoolean(boolean b){return b ? getSuccess("ok") : getError("error");}/* (non-Javadoc)* @see java.lang.Object#toString()*/@SuppressWarnings("rawtypes")@Overridepublic String toString() {String data_string = null;if(data == null){} else if(data instanceof List){data_string = "List(length=" + ((List)data).size() + ")";} else {data_string = data.toString();}return "{"+ "\"code\": " + this.getCode()+ ", \"message\": \"" + this.getMsg() + "\""+ ", \"data\": " + data_string+ ", \"dataCount\": " + dataCount+ "}";}}

自定义注解

  • 加密注解:Encrypt
import java.lang.annotation.*;/*** @author sungang* @date 2021/10/15 5:14 下午* 加密注解*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypt {}
  • 解密注解:Decrypt
/*** @author sungang* @date 2021/10/15 5:13 下午* 解密注解*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {}

AOP切面

  • Decrypt
package com.sun.springboot.aspect;import java.lang.annotation.*;/*** @author sungang* @date 2021/10/15 5:13 下午* 解密注解*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {}
  • Encrypt
package com.sun.springboot.aspect;import java.lang.annotation.*;/*** @author sungang* @date 2021/10/15 5:14 下午* 加密注解*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypt {}
  • SafetyAspect
package com.sun.springboot.aspect;import com.sun.springboot.util.ApiSecurityUtil;
import com.sun.springboot.util.JsonUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;/*** @author sungang* @date 2021/10/15 5:15 下午*/
@Aspect
@Component
public class SafetyAspect {/*** Pointcut 切入点* 匹配com.zykj.heliu.controller包下面的所有方法*/@Pointcut("execution(* com.sun.springboot.controller..*.*(..))")public void safetyAspect() {}/*** 环绕通知*/@Around(value = "safetyAspect()")public Object around(ProceedingJoinPoint pjp) throws Throwable {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert attributes != null;//request对象HttpServletRequest request = attributes.getRequest();//http请求方法  post getString httpMethod = request.getMethod().toLowerCase();//method方法Method method = ((MethodSignature) pjp.getSignature()).getMethod();//method方法上面的注解Annotation[] annotations = method.getAnnotations();//方法的形参参数Object[] args = pjp.getArgs();//是否有@Decryptboolean hasDecrypt = false;//是否有@Encryptboolean hasEncrypt = false;for (Annotation annotation : annotations) {if (annotation.annotationType() == Decrypt.class) {hasDecrypt = true;}if (annotation.annotationType() == Encrypt.class) {hasEncrypt = true;}}//执行方法之前解密,且只拦截post请求if ("post".equals(httpMethod) && hasDecrypt) {//api解密String decrypt = ApiSecurityUtil.decrypt();//注:参数最好用Vo对象来接参,单用String来接,args有长度但获取为空,很奇怪不知道为什么if(args.length > 0){args[0] = JsonUtil.parse(decrypt, args[0].getClass());}}//执行并替换最新形参参数   PS:这里有一个需要注意的地方,method方法必须是要public修饰的才能设置值,private的设置不了Object o = pjp.proceed(args);//返回结果之前加密if (hasEncrypt) {//api加密,转json字符串并转成Object对象,设置到Result中并赋值给返回值oo = ApiSecurityUtil.encrypt(o);}//返回return o;}
}

测试接口和实体类

  • 公钥获取接口
package com.sun.springboot.controller;import com.sun.springboot.component.MemoryDataTools;
import com.sun.springboot.constant.RsaConstant;
import com.sun.springboot.util.RsaUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.PostConstruct;
import javax.annotation.Resource;/*** @author sunbt* @date 2023/8/31 21:48*/
@Api(tags = "加密方法")
@RestController
@RequestMapping(value = "rsa")
public class RsaController {@ResourceMemoryDataTools memoryDataTools;@ApiOperation("获取后台公钥")@GetMapping("getPublicKey")public String getPublicKey() {return memoryDataTools.get(RsaConstant.RSA_PUBLIC_KEY).toString();}@PostConstructprivate void initRsaKey() {String publicKey = RsaUtil.getPublicKey();String privateKey = RsaUtil.getPrivateKey();memoryDataTools.put(RsaConstant.RSA_PUBLIC_KEY, publicKey);memoryDataTools.put(RsaConstant.RSA_PRIVATE_KEY, privateKey);}
}
  • 实体类

定义一个VO

package com.sun.aop.entiy;import lombok.Data;/*** @author sung*/
@Data
public class LoginVo {private String username;private String password;}
  • 加解密测试接口
package com.sun.springboot.controller;import com.sun.springboot.aspect.Decrypt;
import com.sun.springboot.aspect.Encrypt;
import com.sun.springboot.response.AjaxJson;
import com.sun.springboot.vo.LoginVo;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;/*** @author sung* 测试aop方式加解密* application/x-www-form-urlencoded 方式*/
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping(value = "ed")
public class EdController {@Decrypt@Encrypt@PostMapping("login")public AjaxJson login(LoginVo loginVo) {System.out.println(loginVo.getUsername() + "---" + loginVo.getPassword());HashMap<String, Object> res = new HashMap<>();res.put("username", loginVo.getUsername());res.put("password", loginVo.getPassword());res.put("token", "token");return  AjaxJson.getSuccessData(res);}}

到这里后台配置完成

前台(VUE)

前台使用是vue项目,请求使用axios

封装一个自定义的axios请求

request_post_aop.js

import axios from 'axios'
import aes from "@/util/aes";
import rsa from "@/util/rsa";
import qs from "qs";// 我们通过这个实例去发请求,把需要的配置配置给这个实例来处理
//针对post请求,application/x-www-form-urlencoded
const request_post_aop = axios.create({baseURL: '/api', // 请求的基础路径timeout: 30000,// 定义后端返回的原始数据的处理// 参数 data 就是后端返回的原始数据(未经处理的 JSON 格式字符串)transformResponse: [function (data) {return data}]
})// 请求拦截器(在请求之前进行一些配置)
request_post_aop.interceptors.request.use(// 任何所有请求会经过这里// config 是当前请求相关的配置信息对象// config 是可以修改的function (config) {// const user = JSON.parse(window.sessionStorage.getItem('token'))// // 如果有登录用户信息,则统一设置 token// if (user) {//     config.headers.Authorization = `Bearer ${user}`// }//获取前端RSA公钥密码、AES的key,并放到windowlet genKeyPair = rsa.genKeyPair();window.jsPublicKey = genKeyPair.publicKey;window.jsPrivateKey = genKeyPair.privateKey;var javaPublicKey = window.sessionStorage.getItem("javaPublicKey");let aesKey = aes.genKey();console.log(aesKey);let aesKeyRes = rsa.rsaEncrypt(aesKey, javaPublicKey);console.log("后端公钥:" + javaPublicKey);console.log("使用后端公钥加密的前端aes:" + aesKeyRes);let data = config.data;console.log("config:" + data)let dataRes = aes.encrypt(data, aesKey);console.log("使用前端AES加密的data:" + dataRes);console.log("前端公钥:" + window.jsPublicKey);console.log("前端私钥:" + window.jsPrivateKey);let jsPrivateKey = window.jsPrivateKey;jsPrivateKey = jsPrivateKey.replace("-----BEGIN RSA PRIVATE KEY-----\n", "");jsPrivateKey = jsPrivateKey.replace("\n-----END RSA PRIVATE KEY-----", "");console.log("前端私钥+new:" + jsPrivateKey);window.jsPrivateKey=jsPrivateKey;let jsPublicKey = window.jsPublicKey;jsPublicKey = jsPublicKey.replace("-----BEGIN PUBLIC KEY-----\n", "");jsPublicKey = jsPublicKey.replace("\n-----END PUBLIC KEY-----", "");console.log("前端公钥+new:" + jsPublicKey);window.jsPublicKey=jsPublicKey;let dataVo = {data: dataRes,aesKey: aesKeyRes,//后端RSA公钥加密后的AES的keypublicKey: jsPublicKey//前端公钥,};config.data = qs.stringify(dataVo);console.log("config+data:" + config.data)return config},// 请求失败,会经过这里function (error) {return Promise.reject(error)}
)//响应了拦截器(在响应之后对数据进行一些处理)
request_post_aop.interceptors.response.use(res=>{console.log(res)let parse = JSON.parse(res.data);console.log(parse.data);let bkAes = rsa.rsaDecrypt(parse.data.aesKey, window.jsPrivateKey);console.log("使用前端私钥获取后端aesKey:" + bkAes);console.log(parse.data.data)return aes.decrypt(parse.data.data, bkAes)
})// 导出请求方法
export {request_post_aop}
  • request请求:用于获取后台的公钥
// 我们通过这个实例去发请求,把需要的配置配置给这个实例来处理
import axios from "axios";const request = axios.create({baseURL: 'http://localhost:8081', // 请求的基础路径timeout: 30000,// 定义后端返回的原始数据的处理// 参数 data 就是后端返回的原始数据(未经处理的 JSON 格式字符串)transformResponse: [function (data) {return data}]
})// 导出请求方法
export {request}

对axios请求再封装

import {request} from "@/network/request";
import {request_post_aop} from "@/network/request_post_aop";//加解密接口封装
export const post_aop = data => {return request_post_aop({headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},method: 'POST',url: '/ed/login',data})
}//获取后台公钥
export const getPublicKey = data => {return request({method: 'GET',url: 'rsa/getPublicKey',params: data})
}

像后台加密接口请求

可以先获取后台公钥,并存储在window对象中

import {post_aop, getPublicKey} from "@/api/api";export default {name: 'HelloWorld',props: {msg: String},mounted() {let data = {"username": "admin","password": "adminpwd"};let javaPublicKey = "";getPublicKey().then(res => {//获取公钥javaPublicKey = res.data;window.sessionStorage.setItem("javaPublicKey", javaPublicKey);console.log(javaPublicKey);//数据加解密post_aop(data).then(res => {console.log(res.data);}).catch(err => {console.log(err)})})}
}

效果如下图:

image-20230903231354587

仓库代码地址

代码地址

请点个star关注一下,后面还会持续分享干货的。

image-20230903231919479

后记

使用RSA+AES进行加密只能保证数据在加密过程中不会被明文获取,但还是会有漏洞,避免不了中间人攻击这种方式:

中间人攻击(Man-in-the-Middle Attack)是一种网络攻击形式,攻击者在通信双方之间插入自己,以获取通信双方之间的信息。在这种攻击中,攻击者可以拦截、窃取、篡改通信双方之间的数据,从而破坏通信的安全性。中间人攻击可以通过对网络数据包进行篡改、窃取等方式来实现,通常需要攻击者拥有一定的技术能力和对网络协议的理解。为了防范中间人攻击,通信双方可以采用加密、数字签名等手段来保护数据的安全性。

参考资料

这里可以使用https来避免中间人攻击:

使用HTTPS可以有效地避免中间人攻击。HTTPS是一种基于SSL/TLS协议的安全通信方式,它可以确保通信双方之间的数据传输是加密的,从而防止攻击者窃取或篡改数据。在HTTPS中,通信双方通过密钥交换协商一个加密算法和密钥,然后使用该密钥对数据进行加密和解密。由于HTTPS使用了加密通信方式,因此可以有效地防止中间人攻击。另外,为了确保通信双方的身份真实可靠,HTTPS还可以使用数字签名来验证通信双方的身份。

参考资料

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/69619.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NetSuite海鲜书 - 知识会汇编 用户篇 2023

NetSuite2021年初夏&#xff0c;NetSuite知识会成立。它由本人&#xff0c;上海德之匠信息技术有限公司的毛岩喆&#xff08;江湖人称Rick&#xff09;发起建立。建立的初衷秉承Rick个人博客“学问思辨&#xff0c;企业信息化路上的行者”的理念&#xff0c;期望能够在NetSuite…

Flink基础

Flink architecture job manager is master task managers are workers task slot is a unit of resource in cluster, number of slot is equal to number of cores(超线程则slot2*cores), slot一组内存一些线程共享CPU when starting a cluster,job manager will allocate a …

docker快速安装-docker一键安装脚本

1.下载/配置安装脚本 touch install-docker.sh #!/bin/bash #mail:ratelcloudqq.com #system:centos7 #integration: docker-latestclear echo "######################################################" echo "# Auto Install Docker …

大数据组件-Flume集群环境搭建

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 个人主页&#xff1a;beixi 本文章收录于专栏&#xff08;点击传送&#xff09;&#xff1a;【大数据学习】 &#x1f493;&#x1f493;持续更新中&#xff0c;感谢各位前辈朋友们支持…

AOP到底是啥

AOP到底是啥 前言面向切面编程到底是啥意思那么要怎么实现面向切面编程呢&#xff1f;成果 前言 回忆起来&#xff0c;第一次听到这三字母是博主在上大二的时候&#xff0c;那时候看的一脸懵逼&#xff0c;现在马上研二了才想起来回顾下。 只记得当时面向对象编程还没整明白&…

TDengine(2):wsl2+ubuntu20.04+TDengine安装

一、ubuntu系统下提供了三种安装TDengine的方式&#xff1a; 二、通过 apt 指令安装失败 因为是linux初学者&#xff0c;对apt 指令较为熟悉&#xff0c;因此首先使用了该方式进行安装。 wget -qO - http://repos.taosdata.com/tdengine.key | sudo apt-key add -echo "…

【Linux】文件

Linux 文件 什么叫文件C语言视角下文件的操作文件的打开与关闭文件的写操作文件的读操作 & cat命令模拟实现 文件操作的系统接口open & closewriteread 文件描述符进程与文件的关系重定向问题Linux下一切皆文件的认识文件缓冲区缓冲区的刷新策略 stuout & stderr 什…

STM32 硬件IIC 控制OLED I2C卡死问题

#更新通知&#xff1a;2023-09-06 STM32L151 固件库 使用I2C 太难了&#xff0c;又宕机了&#xff0c;建议不要在固件库版本上尝试硬件IIC 了&#xff0c;一般人真用不了&#xff0c;直接使用软件模拟的&#xff0c;或者不要使用固件库了&#xff0c;用HAL 库吧&#xff0c;据说…

既要炫酷好看,又要出图快?可视化大屏模板了解下!

可视化大屏模板可以在很大程度上满足炫酷好看和出图快的需求。使用模板可以节约制作时间&#xff0c;像奥威BI系统就上线了大量的可视化大屏模板。这些模板实际上都是一张张完整的可视化大屏报表&#xff0c;从数据源到数据分析模型&#xff0c;再到数据可视化图表和智能分析功…

《vue3实战》运用push()方法实现电影评价系统的添加功能

目录 前言 电影评价系统的添加功能是什么&#xff1f; 电影评价系统的添加功能有什么作用&#xff1f; 一、push&#xff08;&#xff09;方法是什么&#xff1f;它有什么作用&#xff1f; 含义&#xff1a; 作用&#xff1a; 二、功能实现 这段是添加开始时点击按钮使…

支持CAN FD的Kvaser PCIEcan 4xCAN v2编码: 73-30130-01414-5如何应用?

这里是引用 Kvaser PCIEcan 4xCAN v2&#xff08;编码: 73-30130-01414-5&#xff09;是一款小巧而先进的多通道实时CAN接口&#xff0c;可发送和接收CAN总线上的标准和扩展CAN消息&#xff0c;时间戳精度高。其与所有使用Kvaser CANlib的应用程序兼容。 主要特性 PCI Express…

spring boot项目上传头像

应用还是验证码使用的原理&#xff1b;但是代码逻辑却有所不同。 逻辑前端传给后端&#xff0c;然后写入本机磁盘去&#xff0c;文件名用uuid避免重复。写完就可以顺带把文件名保存到数据库里。上传就这样子。 怎么取用的&#xff1b;还是通过配置映射的方式&#xff1b;通过sr…

vue3升级了些什么

Vue 3 升级了以下几个方面的内容&#xff1a; 响应式系统&#xff1a;Vue 3 使用了 Proxy 对象来替代 Vue 2 中的 Object.defineProperty&#xff0c;这使得响应式系统更加高效和灵活。Vue 3 的响应式系统可以追踪更细粒度的依赖关系&#xff0c;提供了更好的性能和更细致的响应…

Bootstrap的行、列布局设计(网络系统设计)

目录 00-基础知识01-等宽列布局02-指定某一列的宽度03-根据内容自动改变列的宽度04-五种预定义列宽度 .col、.col-sm-*、.col-md-*、.col-lg-*、.col-xl-*05-不同视口宽度按不同的分列方案划分06-删除列内容的盒模型的外边距07-超过12列怎么办&#xff1f;08-重新排列各列的顺序…

继承(个人学习笔记黑马学习)

1、基本语法 #include <iostream> using namespace std; #include <string>//普通实现页面//Java页面 //class Java { //public: // void header() { // cout << "首页、公开课、登录、注册...(公共头部)" << endl; // } // void footer() …

【精品】NLP自然语言处理学习路线(知识体系)

当前&#xff0c;大规模预训练语言模型的强大对话问答、文本生成能力&#xff0c;将自然语言处理&#xff08;NLP&#xff09;的研究和应用推向了新一轮的热潮。NLP是计算机科学、人工智能和语言学等学科交叉的前沿领域。NLP的应用和研究范围非常的广泛&#xff0c;个人是没有找…

利用GitHub实现域名跳转

利用GitHub实现域名跳转 一、注册一个 github账号 你需要注册一个 github账号,最好取一个有意义的名字&#xff0c;比如姓名全拼&#xff0c;昵称全拼&#xff0c;如果被占用&#xff0c;可以加上有意义的数字. 本文中假设用户名为 UNIT-wuji(也是我的博客名) 地址: https:/…

Git常用命令用法

参考视频&#xff1a;真的是全能保姆 git、github 保姆级教程入门&#xff0c;工作和协作必备技术&#xff0c;github提交pr - pull request_哔哩哔哩_bilibili 1.Git初始化 首先设置名称和邮箱。然后初始化一下&#xff0c;然后就创建了一个空的Git仓库。 PS D:\golang\oth…

node socket.io

装包&#xff1a; yarn add socket.io node后台&#xff1a; const express require(express) const http require(http) const socket require(socket.io) const { getUserInfoByToken } require(../../utils/light/tools)let app express() const server http.createS…

【C++漂流记】结构体的定义和使用、结构体数组、结构体指针、结构体做函数参数以及结构体中const的使用

结构体&#xff08;struct&#xff09;是C语言中一种重要的数据类型&#xff0c;它由一组不同类型的成员组成。结构体可以用来表示一个复杂的数据结构&#xff0c;比如一个学生的信息、一个员工记录或者一个矩形的尺寸等。 结构体定义后&#xff0c;可以声明结构体变量&#xf…