0


Java:SpringBoot使用AES对JSON数据加密和解密

目录

1、加密解密原理

客户端和服务端都可以加密和解密,使用base64进行网络传输

加密方

字符串 -> AES加密 -> base64

解密方

base64 -> AES解密 -> 字符串

2、项目示例

2.1、项目结构

$ tree -I target -Itest.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── demo
        │               ├── Application.java
        │               ├── annotation
        │               │   └── SecretData.java
        │               ├── config
        │               │   ├── CrossConfig.java
        │               │   ├── DecryptRequestBodyAdvice.java
        │               │   ├── EncryptResponseBodyAdvice.java
        │               │   ├── SecretConfig.java
        │               │   └── WebMvcConfig.java
        │               ├── controller
        │               │   ├── IndexController.java
        │               │   └── UserController.java
        │               ├── request
        │               │   └── JsonRequest.java
        │               ├── response
        │               │   ├── JsonResult.java
        │               │   └── JsonResultVO.java
        │               ├── service
        │               │   ├── SecretDataService.java
        │               │   └── impl
        │               │       └── SecretDataServiceImpl.java
        │               └── utils
        │                   └── CipherUtil.java
        └── resources
            ├── application.yml
            ├── static
            │   ├── axios.min.js
            │   └── crypto-js.min.js
            └── templates
                └── index.html

2.2、常规业务代码

pom.xml

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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.7.7</version><relativePath/><!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version><mybatis-plus.version>3.5.2</mybatis-plus.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.15</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><!-- test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

Application.java

packagecom.example.demo;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[] args){SpringApplication.run(Application.class, args);}}

WebMvcConfig.java

packagecom.example.demo.config;importlombok.extern.slf4j.Slf4j;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;@Slf4j@ConfigurationpublicclassWebMvcConfigextendsWebMvcConfigurationSupport{// 设置静态资源映射@OverrideprotectedvoidaddResourceHandlers(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}}

CrossConfig.java

packagecom.example.demo.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.cors.CorsConfiguration;importorg.springframework.web.cors.UrlBasedCorsConfigurationSource;importorg.springframework.web.filter.CorsFilter;/**
 * 处理跨域问题
 */@ConfigurationpublicclassCrossConfig{@BeanpublicCorsFiltercorsFilter(){CorsConfiguration config =newCorsConfiguration();
        config.addAllowedOrigin("*");
        config.setAllowCredentials(false);
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setMaxAge(3600L);UrlBasedCorsConfigurationSource configSource =newUrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);returnnewCorsFilter(configSource);}}

JsonRequest.java

packagecom.example.demo.request;importlombok.Data;/**
 * 统一的请求体数据
 */@DatapublicclassJsonRequest{/**
     * 未加密数据
     */privateObject data;/**
     * 加密数据
     */privateString encryptData;}

JsonResult.java

packagecom.example.demo.response;importlombok.Data;/**
 * 统一的返回体数据 不加密
 */@DatapublicclassJsonResult<T>{privateString message;privateT data;privateInteger code;publicstatic<T>JsonResultsuccess(T data){JsonResult<T> jsonResult =newJsonResult<>();
        jsonResult.setCode(0);
        jsonResult.setData(data);
        jsonResult.setMessage("success");return jsonResult;}}

JsonResultVO.java

packagecom.example.demo.response;importlombok.Data;/**
 * 统一的返回体数据 加密
 */@DatapublicclassJsonResultVO{privateString message;privateString encryptData;privateInteger code;}

IndexController.java

packagecom.example.demo.controller;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.GetMapping;@ControllerpublicclassIndexController{@GetMapping("/")publicStringindex(){return"index";}}

UserController.java

packagecom.example.demo.controller;importcom.example.demo.annotation.SecretData;importcom.example.demo.response.JsonResult;importorg.springframework.web.bind.annotation.*;importjava.util.HashMap;importjava.util.Map;/**
 * 对该Controller中的所有方法进行加解密处理
 */@RestController@SecretDatapublicclassUserController{@GetMapping("/user/getUser")publicJsonResultgetUser(){Map<String,String> user =newHashMap<>();
        user.put("name","Tom");
        user.put("age","18");returnJsonResult.success(user);}@PostMapping("/user/addUser")publicObjectaddUser(@RequestBodyMap<String,String> data){System.out.println(data);return data;}}

2.3、加密的实现

application.yml

secret:key:1234567890123456# 密钥位数为16位enabled:true# 开启加解密功能

SecretData.java

packagecom.example.demo.annotation;importorg.springframework.web.bind.annotation.Mapping;importjava.lang.annotation.*;/**
 * 只有添加有该注解的Controller类或是具体接口方法才进行数据的加密解密
 */@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Mapping@Documentedpublic@interfaceSecretData{}

DecryptRequestBodyAdvice.java

packagecom.example.demo.config;importcom.example.demo.annotation.SecretData;importcom.example.demo.request.JsonRequest;importcom.example.demo.service.SecretDataService;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.springframework.boot.autoconfigure.condition.ConditionalOnProperty;importorg.springframework.core.MethodParameter;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpInputMessage;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;importjavax.annotation.Resource;importjava.io.ByteArrayInputStream;importjava.io.IOException;importjava.io.InputStream;importjava.lang.reflect.Type;/**
 * 对请求内容进行解密
 * 只有开启了加解密功能才会生效
 * 仅对使用了@RqestBody注解的生效
 * https://blog.csdn.net/xingbaozhen1210/article/details/98189562
 */@ControllerAdvice// @ConditionalOnProperty(name = "secret.enabled", havingValue = "true")publicclassDecryptRequestBodyAdviceextendsRequestBodyAdviceAdapter{@ResourceprivateSecretDataService secretDataService;@Overridepublicbooleansupports(MethodParameter methodParameter,Type targetType,Class<?extendsHttpMessageConverter<?>> converterType){return methodParameter.getMethod().isAnnotationPresent(SecretData.class)|| methodParameter.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class);}@OverridepublicHttpInputMessagebeforeBodyRead(HttpInputMessage inputMessage,MethodParameter parameter,Type targetType,Class<?extendsHttpMessageConverter<?>> converterType)throwsIOException{System.out.println("beforeBodyRead");String body =inToString(inputMessage.getBody());System.out.println(body);ObjectMapper objectMapper =newObjectMapper();JsonRequest jsonRequest = objectMapper.readValue(body,JsonRequest.class);// 默认取data数据,如果提交加密数据则解密String decryptData =null;if(jsonRequest.getEncryptData()!=null){
            decryptData = secretDataService.decrypt(jsonRequest.getEncryptData());}else{
            decryptData = objectMapper.writeValueAsString(jsonRequest.getData());}String data = decryptData;// 解密后的数据System.out.println(data);returnnewHttpInputMessage(){@OverridepublicHttpHeadersgetHeaders(){return inputMessage.getHeaders();}@OverridepublicInputStreamgetBody()throwsIOException{returnnewByteArrayInputStream(data.getBytes());}};}/**
     * 读取输入流为字符串
     *
     * @param is
     * @return
     */privateStringinToString(InputStream is){byte[] buf =newbyte[10*1024];int length =-1;StringBuilder sb =newStringBuilder();try{while((length = is.read(buf))!=-1){
                sb.append(newString(buf,0, length));}return sb.toString();}catch(IOException e){thrownewRuntimeException(e);}}}

EncryptResponseBodyAdvice.java

packagecom.example.demo.config;importcom.example.demo.annotation.SecretData;importcom.example.demo.response.JsonResult;importcom.example.demo.response.JsonResultVO;importcom.example.demo.service.SecretDataService;importcom.fasterxml.jackson.databind.ObjectMapper;importlombok.SneakyThrows;importorg.springframework.beans.BeanUtils;importorg.springframework.boot.autoconfigure.condition.ConditionalOnProperty;importorg.springframework.core.MethodParameter;importorg.springframework.http.MediaType;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.http.server.ServerHttpRequest;importorg.springframework.http.server.ServerHttpResponse;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;importjavax.annotation.Resource;/**
 * 对响应内容加密
 */@ControllerAdvice@ConditionalOnProperty(name ="secret.enabled", havingValue ="true")publicclassEncryptResponseBodyAdviceimplementsResponseBodyAdvice<Object>{@ResourceprivateSecretDataService secretDataService;@Overridepublicbooleansupports(MethodParameter returnType,Class<?extendsHttpMessageConverter<?>> converterType){return returnType.getMethod().isAnnotationPresent(SecretData.class)|| returnType.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class);}@SneakyThrows@OverridepublicObjectbeforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class<?extendsHttpMessageConverter<?>> selectedConverterType,ServerHttpRequest request,ServerHttpResponse response){System.out.println("beforeBodyWrite");// 仅对JsonResult对象数据加密if(body instanceofJsonResult){JsonResult jsonResult =(JsonResult) body;JsonResultVO jsonResultVO =newJsonResultVO();BeanUtils.copyProperties(jsonResult, jsonResultVO);String jsonStr =newObjectMapper().writeValueAsString(jsonResult.getData());

            jsonResultVO.setEncryptData(secretDataService.encrypt(jsonStr));return jsonResultVO;}else{return body;}}}

SecretConfig.java

packagecom.example.demo.config;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.context.annotation.Configuration;@Configuration@ConfigurationProperties(prefix ="secret")publicclassSecretConfig{privateBoolean enabled;privateString key;publicBooleangetEnabled(){return enabled;}publicvoidsetEnabled(Boolean enabled){this.enabled = enabled;}publicStringgetKey(){return key;}publicvoidsetKey(String key){this.key = key;}}

SecretDataService.java

packagecom.example.demo.service;/**
 * 加密解密的接口
 */publicinterfaceSecretDataService{/**
     * 数据加密
     *
     * @param data 待加密数据
     * @return String 加密结果
     */Stringencrypt(String data);/**
     * 数据解密
     *
     * @param data 待解密数据
     * @return String 解密后的数据
     */Stringdecrypt(String data);}

SecretDataServiceImpl.java

packagecom.example.demo.service.impl;importcom.example.demo.config.SecretConfig;importcom.example.demo.service.SecretDataService;importcom.example.demo.utils.CipherUtil;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;/**
 * 具体的加解密实现
 */@ServicepublicclassSecretDataServiceImplimplementsSecretDataService{@ResourceprivateSecretConfig secretConfig;@OverridepublicStringdecrypt(String data){returnCipherUtil.decrypt(secretConfig.getKey(), data);}@OverridepublicStringencrypt(String data){returnCipherUtil.encrypt(secretConfig.getKey(), data);}}

CipherUtil.java

packagecom.example.demo.utils;importjavax.crypto.Cipher;importjavax.crypto.spec.SecretKeySpec;importjava.nio.charset.Charset;importjava.util.Base64;/**
 * 数据加密解密工具类
 * 加密后返回base64
 */publicclassCipherUtil{/**
     * 定义加密算法
     */privatestaticfinalStringALGORITHM="AES/ECB/PKCS5Padding";/**
     * 解密
     *
     * @param secretKey
     * @param cipherText base64
     * @return
     */publicstaticStringdecrypt(String secretKey,String cipherText){// 将Base64编码的密文解码byte[] encrypted =Base64.getDecoder().decode(cipherText);try{Cipher cipher =Cipher.getInstance(ALGORITHM);SecretKeySpec key =newSecretKeySpec(secretKey.getBytes(),"AES");
            cipher.init(Cipher.DECRYPT_MODE, key);returnnewString(cipher.doFinal(encrypted));}catch(Exception e){thrownewRuntimeException(e);}}/**
     * 加密
     *
     * @param secretKey
     * @param plainText base64
     * @return
     */publicstaticStringencrypt(String secretKey,String plainText){try{Cipher cipher =Cipher.getInstance(ALGORITHM);SecretKeySpec key =newSecretKeySpec(secretKey.getBytes(),"AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);returnBase64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(Charset.forName("UTF-8"))));}catch(Exception e){thrownewRuntimeException(e);}}}

浏览器中实现加密解密

templates/index.html

<!DOCTYPEhtml><htmllang="zh"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"/><title>接口数据加密解密</title></head><body><!-- 引入依赖 --><!-- <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> --><scriptsrc="/static/crypto-js.min.js"></script><!-- <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script> --><scriptsrc="/static/axios.min.js"></script><h1>查看控制台</h1><!-- 加密解密模块 --><scripttype="text/javascript">constSECRET_KEY="1234567890123456";/**
       * 加密方法
       * @param data 待加密数据
       * @returns {string|*}
       */functionencrypt(data){let key = CryptoJS.enc.Utf8.parse(SECRET_KEY);if(typeof data ==="object"){
          data =JSON.stringify(data);}let plainText = CryptoJS.enc.Utf8.parse(data);let secretText = CryptoJS.AES.encrypt(plainText, key,{mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7
        }).toString();return secretText;}/**
       * 解密数据
       * @param data 待解密数据
       */functiondecrypt(data){let key = CryptoJS.enc.Utf8.parse(SECRET_KEY);let result = CryptoJS.AES.decrypt(data, key,{mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7
        }).toString(CryptoJS.enc.Utf8);returnJSON.parse(result);}</script><!-- http请求模块 --><scripttype="text/javascript">// 获取加密数据并解密
      axios.get("http://127.0.0.1:8080/user/getUser").then((res)=>{
        console.log("接收到api返回加密数据:");
        console.log(decrypt(res.data.encryptData));});// 提交加密参数const data ={name:"Tom",age:"18",};

      axios
        .post("http://127.0.0.1:8080/user/addUser",{encryptData:encrypt(data),}).then((res)=>{
          console.log("接收到api返回未加密数据:");
          console.log(res.data);});</script></body></html>

2.4、接口测试

  • 开发环境不加密更易于开发调试
  • 生产环境需要数据加密

前后端都可以通过参数

encryptData

判断对方提交/返回的数据是否为加密数据,如果是加密数据则进行解密操作

接口返回加密数据

GET http://127.0.0.1:8080/user/getUser

未加密的数据

{"message":"success","data":{"name":"Tom","age":"18"},"code":0}

返回数据

{"message":"success","encryptData":"kun2Wvk2LNKICaXIeIExA7jKRyqOV0qCv5KQXFOzfpQ=","code":0}

客户端提交参数

POST http://127.0.0.1:8080/user/addUser

提交数据

{"data":{"name":"Tom","age":"18"}}

提交加密数据

{"encryptData":"kun2Wvk2LNKICaXIeIExA7jKRyqOV0qCv5KQXFOzfpQ="}

2.5、总结

服务端

  • 全局开关:通过控制secret.enabled=true全局开启返回数据加密
  • 全局局部:可以通过SecretData或者自定义PassSecretData来控制单个控制器或者单个接口的需要或不需要加密

客户端

  • 可以根据开发环境、测试环境、生产环境来控制是否开启加密
  • 需要注意,FormData传输文件的数据格式可以考虑不加密

相同点

  • 服务端和客户端都通过对方传输的encryptData来判断是否为加密数据
  • 服务端和客户端都可以根据自己的环境来决定是否开启数据加密

完整代码:https://github.com/mouday/spring-boot-demo/tree/master/SpringBoot-Secret

参考文章

  • 详解API接口如何安全的传输数据
标签: java spring boot json

本文转载自: https://blog.csdn.net/mouday/article/details/132560838
版权归原作者 彭世瑜 所有, 如有侵权,请联系我们删除。

“Java:SpringBoot使用AES对JSON数据加密和解密”的评论:

还没有评论