SpringBoot使用AES对JSON数据加密和解密的实现方法
作者:彭世瑜
这篇文章主要介绍了SpringBoot使用AES对JSON数据加密和解密的实现方法,文章通过代码示例介绍的非常详细,对我们的学习或工作有一定的帮助,需要的朋友可以参考下
1、加密解密原理
客户端和服务端都可以加密和解密,使用base64进行网络传输
加密方
字符串 -> AES加密 -> base64
解密方
base64 -> AES解密 -> 字符串
2、项目示例
2.1、项目结构
$ tree -I target -I test . ├── 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"?> <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.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
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
WebMvcConfig.java
package com.example.demo.config; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; @Slf4j @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { // 设置静态资源映射 @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } }
CrossConfig.java
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; /** * 处理跨域问题 */ @Configuration public class CrossConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.setAllowCredentials(false); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); return new CorsFilter(configSource); } }
JsonRequest.java
package com.example.demo.request; import lombok.Data; /** * 统一的请求体数据 */ @Data public class JsonRequest { /** * 未加密数据 */ private Object data; /** * 加密数据 */ private String encryptData; }
JsonResult.java
package com.example.demo.response; import lombok.Data; /** * 统一的返回体数据 不加密 */ @Data public class JsonResult<T> { private String message; private T data; private Integer code; public static <T> JsonResult success(T data){ JsonResult<T> jsonResult = new JsonResult<>(); jsonResult.setCode(0); jsonResult.setData(data); jsonResult.setMessage("success"); return jsonResult; } }
JsonResultVO.java
package com.example.demo.response; import lombok.Data; /** * 统一的返回体数据 加密 */ @Data public class JsonResultVO { private String message; private String encryptData; private Integer code; }
IndexController.java
package com.example.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class IndexController { @GetMapping("/") public String index(){ return "index"; } }
UserController.java
package com.example.demo.controller; import com.example.demo.annotation.SecretData; import com.example.demo.response.JsonResult; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * 对该Controller中的所有方法进行加解密处理 */ @RestController @SecretData public class UserController { @GetMapping("/user/getUser") public JsonResult getUser() { Map<String, String> user = new HashMap<>(); user.put("name", "Tom"); user.put("age", "18"); return JsonResult.success(user); } @PostMapping("/user/addUser") public Object addUser(@RequestBody Map<String, String> data) { System.out.println(data); return data; } }
2.3、加密的实现
application.yml
secret: key: 1234567890123456 # 密钥位数为16位 enabled: true # 开启加解密功能
SecretData.java
package com.example.demo.annotation; import org.springframework.web.bind.annotation.Mapping; import java.lang.annotation.*; /** * 只有添加有该注解的Controller类或是具体接口方法才进行数据的加密解密 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Mapping @Documented public @interface SecretData { }
DecryptRequestBodyAdvice.java
package com.example.demo.config; import com.example.demo.annotation.SecretData; import com.example.demo.request.JsonRequest; import com.example.demo.service.SecretDataService; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter; import javax.annotation.Resource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; /** * 对请求内容进行解密 * 只有开启了加解密功能才会生效 * 仅对使用了@RqestBody注解的生效 * https://blog.csdn.net/xingbaozhen1210/article/details/98189562 */ @ControllerAdvice // @ConditionalOnProperty(name = "secret.enabled", havingValue = "true") public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter { @Resource private SecretDataService secretDataService; @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.getMethod().isAnnotationPresent(SecretData.class) || methodParameter.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { System.out.println("beforeBodyRead"); String body = inToString(inputMessage.getBody()); System.out.println(body); ObjectMapper objectMapper = new ObjectMapper(); 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); return new HttpInputMessage() { @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(data.getBytes()); } }; } /** * 读取输入流为字符串 * * @param is * @return */ private String inToString(InputStream is) { byte[] buf = new byte[10 * 1024]; int length = -1; StringBuilder sb = new StringBuilder(); try { while ((length = is.read(buf)) != -1) { sb.append(new String(buf, 0, length)); } return sb.toString(); } catch (IOException e) { throw new RuntimeException(e); } } }
EncryptResponseBodyAdvice.java
package com.example.demo.config; import com.example.demo.annotation.SecretData; import com.example.demo.response.JsonResult; import com.example.demo.response.JsonResultVO; import com.example.demo.service.SecretDataService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.beans.BeanUtils; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.annotation.Resource; /** * 对响应内容加密 */ @ControllerAdvice @ConditionalOnProperty(name = "secret.enabled", havingValue = "true") public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Resource private SecretDataService secretDataService; @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return returnType.getMethod().isAnnotationPresent(SecretData.class) || returnType.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class); } @SneakyThrows @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { System.out.println("beforeBodyWrite"); // 仅对JsonResult对象数据加密 if (body instanceof JsonResult) { JsonResult jsonResult = (JsonResult) body; JsonResultVO jsonResultVO = new JsonResultVO(); BeanUtils.copyProperties(jsonResult, jsonResultVO); String jsonStr = new ObjectMapper().writeValueAsString(jsonResult.getData()); jsonResultVO.setEncryptData(secretDataService.encrypt(jsonStr)); return jsonResultVO; } else { return body; } } }
SecretConfig.java
package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "secret") public class SecretConfig { private Boolean enabled; private String key; public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } }
SecretDataService.java
package com.example.demo.service; /** * 加密解密的接口 */ public interface SecretDataService { /** * 数据加密 * * @param data 待加密数据 * @return String 加密结果 */ String encrypt(String data); /** * 数据解密 * * @param data 待解密数据 * @return String 解密后的数据 */ String decrypt(String data); }
SecretDataServiceImpl.java
package com.example.demo.service.impl; import com.example.demo.config.SecretConfig; import com.example.demo.service.SecretDataService; import com.example.demo.utils.CipherUtil; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * 具体的加解密实现 */ @Service public class SecretDataServiceImpl implements SecretDataService { @Resource private SecretConfig secretConfig; @Override public String decrypt(String data) { return CipherUtil.decrypt(secretConfig.getKey(), data); } @Override public String encrypt(String data) { return CipherUtil.encrypt(secretConfig.getKey(), data); } }
CipherUtil.java
package com.example.demo.utils; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.util.Base64; /** * 数据加密解密工具类 * 加密后返回base64 */ public class CipherUtil { /** * 定义加密算法 */ private static final String ALGORITHM = "AES/ECB/PKCS5Padding"; /** * 解密 * * @param secretKey * @param cipherText base64 * @return */ public static String decrypt(String secretKey, String cipherText) { // 将Base64编码的密文解码 byte[] encrypted = Base64.getDecoder().decode(cipherText); try { Cipher cipher = Cipher.getInstance(ALGORITHM); SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), "AES"); cipher.init(Cipher.DECRYPT_MODE, key); return new String(cipher.doFinal(encrypted)); } catch (Exception e) { throw new RuntimeException(e); } } /** * 加密 * * @param secretKey * @param plainText base64 * @return */ public static String encrypt(String secretKey, String plainText) { try { Cipher cipher = Cipher.getInstance(ALGORITHM); SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), "AES"); cipher.init(Cipher.ENCRYPT_MODE, key); return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(Charset.forName("UTF-8")))); } catch (Exception e) { throw new RuntimeException(e); } } }
浏览器中实现加密解密
templates/index.html
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>接口数据加密解密</title> </head> <body> <!-- 引入依赖 --> <!-- <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> --> <script src="/static/crypto-js.min.js"></script> <!-- <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script> --> <script src="/static/axios.min.js"></script> <h1>查看控制台</h1> <!-- 加密解密模块 --> <script type="text/javascript"> const SECRET_KEY = "1234567890123456"; /** * 加密方法 * @param data 待加密数据 * @returns {string|*} */ function encrypt(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 待解密数据 */ function decrypt(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); return JSON.parse(result); } </script> <!-- http请求模块 --> <script type="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
来判断是否为加密数据 - 服务端和客户端都可以根据自己的环境来决定是否开启数据加密
完整代码:spring-boot-demo/SpringBoot-Secret at master · mouday/spring-boot-demo · GitHub
以上就是SpringBoot使用AES对JSON数据加密和解密的实现方法的详细内容,更多关于SpringBoot AES加解密的资料请关注脚本之家其它相关文章!