1. AES算法简介
高级加密标准(AES,Advanced Encryption Standard) 为最常见的对称加密算法。对称加密算法也就是加密和解密使用相同的秘钥,具体的加密流程如下图:
下面简单介绍下各个部分的作用与意义:
- 明文P
没有经过加密的数据。
- 密钥K
用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。
- AES加密函数
设AES加密函数为E,则 C = E(K, P),其中P为明文,K为密钥,C为密文。也就是说,把明文P和密钥K作为加密函数的参数输入,则加密函数E会输出密文C。
- 密文C
经加密函数处理后的数据
- AES解密函数
设AES 解密函数为D,则 P = D(K, C),其中C为密文,K为密钥,P为明文。也就是说,把密文C和密钥K作为解密函数的参数输入,则解密函数会输出明文P。
在这里简单介绍下对称加密算法与非对称加密算法的区别。
- 对称加密算法
加密和解密用到的密钥是相同的,这种加密方式加密速度非常快,适合经常发送数据的场合。缺点是密钥的传输比较麻烦。
- 非对称加密算法
加密和解密用的密钥是不同的,这种加密方式是用数学上的难解问题构造的,通常加密解密的速度比较慢,适合偶尔发送数据的场合。优点是密钥传输方便。常见的非对称加密算法为RSA、ECC和EIGamal。
实际中,一般是通过RSA加密AES的密钥,传输到接收方,接收方解密得到AES密钥,然后发送方和接收方用AES密钥来通信
2.后端引入加解密插件
如果有时间可以自己试着封装一个,我首先想到的便是Filter,在controller方法执行之前对数据进行解密再进行方法参数的赋值,这里我引入了一个已经封装好的加解密插件,我们就不自己造轮子了(老板催的紧...)
-
引入依赖
com.cxytiandi monkey-api-encrypt-core 1.2.RELEASE
因为这里我使用的是SpringBoot 2.3.4的版本,所以在文档中介绍了2种配置方式
a.手动注册过滤器使用
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean filterRegistration() {
//默认的配置类
EncryptionConfig config = new EncryptionConfig();
//前后端加解密使用的秘钥
config.setKey("abcdef0123456789");
//需要解密的请求地址,保存在一个list中
config.setRequestDecyptUriList(Arrays.asList("/save***", "/update***"));
//同上,需要加密的地址(返回数据加密)
config.setResponseEncryptUriList(Arrays.asList("/encryptStr", "/encryptEntity"));
//SpringBoot(1.4开始就有)的注册请求过滤器()
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new EncryptionFilter(config));
registration.addUrlPatterns("/*");
registration.setName("EncryptionFilter");
registration.setOrder(1);
return registration;
}
}
b.Spring Boot Starter方式使用
启动类加@EnableEncrypt注解,开启加解密自动配置,省略了手动注册Filter的步骤
@EnableEncrypt
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
配置文件中配置加密的信息,也就是EncryptionConfig,也就是上面我们手动注册过滤器代码中的EncryptionConfig,SpringBoot 的使用方式会从程序启动时读取配置文件并赋值到该配置类中
- application.properties
spring.encrypt.key=abcdef0123456789
spring.encrypt.requestDecyptUriList[0]=/save
spring.encrypt.requestDecyptUriList[1]=/decryptEntityXml
spring.encrypt.responseEncryptUriList[0]=/encryptStr
spring.encrypt.responseEncryptUriList[1]=/encryptEntity
spring.encrypt.responseEncryptUriList[2]=/save
spring.encrypt.responseEncryptUriList[3]=/encryptEntityXml
spring.encrypt.responseEncryptUriList[4]=/decryptEntityXml
-
application.yml
spring: encrypt: key: 'abcdef0123456789' ....等等
如果感觉配置比较繁琐,你的加解密接口很多,需要大量的配置,还可以采用另一种方式来标识加解密,就是注解的方式。
响应的数据需要加密,就在接口的方法上加@Encrypt注解
@Encrypt
@GetMapping("/encryptEntity")
public UserDto encryptEntity() {
UserDto dto = new UserDto();
dto.setId(1);
dto.setName("加密实体对象");
return dto;
}
接收的数据需要解密,就在接口的方法上加@Decrypt注解
@Decrypt
@PostMapping("/save")
public UserDto save(@RequestBody UserDto dto) {
System.err.println(dto.getId() + "\t" + dto.getName());
return dto;
}
同时需要加解密那么两个注解都加上即可
@Encrypt
@Decrypt
@PostMapping("/save")
public UserDto save(@RequestBody UserDto dto) {
System.err.println(dto.getId() + "\t" + dto.getName());
return dto;
}
3.前端引入加密插件
第一步 小程序端先引入CryptoJS
npm install crypto-js
第二步 在新建的文件中写入
一、前端JS加密与解密
import CryptoJS from 'crypto-js'
//秘钥,必须由16位字符组成,这里可以是程序启动时传递的秘钥,为了方便这里先写死
let secretKey = "aaaabbbbccccdddd"
export const AESUtil = {
/**
* AES加密方法
* @param content 要加密的字符串
* @returns {string} 加密结果
*/
aesEncrypt: (content) => {
let key = CryptoJS.enc.Utf8.parse(secretKey);
let srcs = CryptoJS.enc.Utf8.parse(content);
let encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode:CryptoJS.mode.ECB, // AES有多种加密模式 这里使用ECB 和后端保持一致
padding: CryptoJS.pad.Pkcs7 //这里使用Pkcs7加密,后端使用Pkcs5解密
});
return encrypted.toString();
},
/**
* 解密方法
* @param encryptStr 密文
* @returns {string} 明文
*/
aesDecrypt: (encryptStr) => {
let key = CryptoJS.enc.Utf8.parse(secretKey);
let decrypt = CryptoJS.AES.decrypt(encryptStr, key, {
mode:CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
}
加解密工具类 编写完毕后,就可以在发送请求的时候将参数进行加密,对返回的数据进行解密等操作.
4.插件原理简单分析
手动注册过滤器的方式
我们在手动注册过滤器的时候通过Springboot 的注册请求过滤器 注册了一个EncryptionFilter ,进入该类
public class EncryptionFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(EncryptionFilter.class);
//加解密配置类
private EncryptionConfig encryptionConfig;
//加解密的公共接口 其实现是AES的加密方式
private EncryptAlgorithm encryptAlgorithm = new AesEncryptAlgorithm();
//实现Filter接口 并重写了doFilter 方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
}
...
}
所以当请求过来的时候,会走到该类的doFilter方法中 ,作者定义了2个实现了HttpServletRequest,HttpServletResponse 的包装类
EncryptionResponseWrapper responseWrapper = null;
EncryptionReqestWrapper reqestWrapper = null;
在EncryptionReqestWrapper 类中,会将request 的数据转换成一个byte[] 数组 ,在通过调用getRequestData() 转换为String 字符串,我们看看doFilter 中对应的解密方法
EncryptionResponseWrapper responseWrapper = null;
EncryptionReqestWrapper reqestWrapper = null;
// 配置了需要解密才处理
if (decryptionStatus) {
reqestWrapper = new EncryptionReqestWrapper(req);
//进行请求解密处理
processDecryption(reqestWrapper, req);
}
/**
* 请求解密处理
* @param reqestWrapper
* @param req
*/
private void processDecryption(EncryptionReqestWrapper reqestWrapper, HttpServletRequest req) {
//将EncryptionReqestWrapper中的byte[] requestBody转换为字符串
String requestData = reqestWrapper.getRequestData();
String uri = req.getRequestURI();
logger.debug("RequestData: {}", requestData);
try {
//encryptAlgorithm 为前边提到的加密算法实现,这里是AES的实现,并开始进行解密处理
String decyptRequestData = encryptAlgorithm.decrypt(requestData, encryptionConfig.getKey());
logger.debug("DecyptRequestData: {}", decyptRequestData);
//将解密后的数据重新赋值到包装类中
reqestWrapper.setRequestData(decyptRequestData);
// url参数解密,对跟在请求地址后的参数进行解密
Map paramMap = new HashMap<>();
Enumeration parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String prefixUri = req.getMethod().toLowerCase() + ":" + uri;
if (encryptionConfig.getRequestDecyptParams(prefixUri).contains(paramName)) {
String paramValue = req.getParameter(paramName);
String decryptParamValue = encryptAlgorithm.decrypt(paramValue, encryptionConfig.getKey());
paramMap.put(paramName, decryptParamValue);
}
}
reqestWrapper.setParamMap(paramMap);
} catch (Exception e) {
logger.error("请求数据解密失败", e);
throw new RuntimeException(e);
}
}
Spring boot Starter 启动的方式
该启动方式是怎么工作的呢?文档介绍到需要对启动类中增加@EnableEncrypt 注解 我们进入该注解看看
/**
* 启用加密Starter
* 在Spring Boot启动类上加上此注解
* @author yinjihuan http://cxytiandi.com/about
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//这里是该注解的核心配置,我们知道@Import注解是Spring 3.0提供的用于在程序运行时动态导入组件到IOC容器中,
//该类导入了加解密的核心配置类,这里也算是Springboot 约定优于配置的一种体现了,像Springboot集成第3方插件 基本都是
//导入第3方的XXXAutoCongfiguration,如RedisAutoConfiguration,MybatisAutoConfiguration等等...
@Import({EncryptAutoConfiguration.class})
public @interface EnableEncrypt {
}
然后看看EncryptAutoConfiguration 这个配置类,基本和手动注册过滤器的方式如出一辙,也是通过FilterRegistrationBean 将自身的过滤器注册到过滤器链中
/**
* 加解密自动配置
* @author yinjihuan
*/
@Configuration
@EnableAutoConfiguration
//通过EncryptionConfig导入配置文件中的相关信息
@EnableConfigurationProperties(EncryptionConfig.class)
public class EncryptAutoConfiguration {
//默认配置类
@Autowired
private EncryptionConfig encryptionConfig;
//加密算法实现
@Autowired(required=false)
private EncryptAlgorithm encryptAlgorithm;
/**
* 不要用泛型注册Filter,泛型在Spring Boot 2.x版本中才有
* @return 过滤器
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean filterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
//这里看到我们可以自定义加密方式 将encryptAlgorithm注入进来,否则使用插件默认的加密方式
if (encryptAlgorithm != null) {
registration.setFilter(new EncryptionFilter(encryptionConfig, encryptAlgorithm));
} else {
registration.setFilter(new EncryptionFilter(encryptionConfig));
}
registration.addUrlPatterns(encryptionConfig.getUrlPatterns());
registration.setName("EncryptionFilter");
registration.setOrder(encryptionConfig.getOrder());
return registration;
}
@Bean
public ApiEncryptDataInit apiEncryptDataInit() {
//该类会对使用了加解密注解的方式进行处理,并将加了加解密注解的uri 存入配置类中的list中等等
return new ApiEncryptDataInit();
}
}
5.总结
- 使用加解密的时候,传递的key 一定要相同
- 使用的AES 的加密模式要相同
参考:
https://blog.csdn.net/qq_28205153/article/details/55798628https://github.com/yinjihuan/monkey-api-encrypt/wiki/%E8%AF%A6%E7%BB%86%E9%85%8D%E7%BD%AE%E4%BD%BF%E7%94%A8