使用AES 对表单数据进行加密传输

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方法执行之前对数据进行解密再进行方法参数的赋值,这里我引入了一个已经封装好的加解密插件,我们就不自己造轮子了(老板催的紧…)

  • 引入依赖

    <dependency>
        <groupId>com.cxytiandi</groupId>
        <artifactId>monkey-api-encrypt-core</artifactId>
        <version>1.2.RELEASE</version>
    </dependency>
    

因为这里我使用的是SpringBoot 2.3.4的版本,所以在文档中介绍了2种配置方式

a.手动注册过滤器使用
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<EncryptionFilter> 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<EncryptionFilter> registration = new FilterRegistrationBean<EncryptionFilter>();
        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<String, String> paramMap = new HashMap<>();
        Enumeration<String> 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 的加密模式要相同

参考:

AES加密算法的详细介绍与实现

monkey-api-encrypt 详细配置使用

你可能感兴趣的:(加密解密,java,spring)