前后端数据加密传输基于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
实现思路
- 前台首先请求非加密接口获取后台的公钥
- 前台在请求前生成自己的公钥和私钥,以及AES对称加密的key
- 使用前台生成的aeskey对数据进行加密
- 在请求前使用后台的公钥对前台的aeskey进行加密
- 将前台加密的data、aeskey和前台公钥一起传递给后台
- 后台使用私钥对前台的aeskey进行解密,再用这个aeskey去解密data
- 后台如果需要返回数据,这时使用后台生成的aeskey对数据进行加密
- 后端使用前台的公钥对aeskey进行加密
- 将aeskey和加密后的数据一起返还给前台,由前台使用私钥解密获得后端的aeskey
- 再使用后端的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)})})}
}
效果如下图:
仓库代码地址
代码地址
请点个star关注一下,后面还会持续分享干货的。
后记
使用RSA+AES进行加密只能保证数据在加密过程中不会被明文获取,但还是会有漏洞,避免不了中间人攻击这种方式:
中间人攻击(Man-in-the-Middle Attack)是一种网络攻击形式,攻击者在通信双方之间插入自己,以获取通信双方之间的信息。在这种攻击中,攻击者可以拦截、窃取、篡改通信双方之间的数据,从而破坏通信的安全性。中间人攻击可以通过对网络数据包进行篡改、窃取等方式来实现,通常需要攻击者拥有一定的技术能力和对网络协议的理解。为了防范中间人攻击,通信双方可以采用加密、数字签名等手段来保护数据的安全性。
参考资料
这里可以使用https来避免中间人攻击:
使用HTTPS可以有效地避免中间人攻击。HTTPS是一种基于SSL/TLS协议的安全通信方式,它可以确保通信双方之间的数据传输是加密的,从而防止攻击者窃取或篡改数据。在HTTPS中,通信双方通过密钥交换协商一个加密算法和密钥,然后使用该密钥对数据进行加密和解密。由于HTTPS使用了加密通信方式,因此可以有效地防止中间人攻击。另外,为了确保通信双方的身份真实可靠,HTTPS还可以使用数字签名来验证通信双方的身份。
参考资料