目录
- 1、加密解密原理
- 2、项目示例
- 2.1、项目结构
- 2.2、常规业务代码
- 2.3、加密的实现
- 2.4、接口测试
- 2.5、总结
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 {// 设置静态资源映射@Overrideprotected 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 {@Beanpublic 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 {@Resourceprivate SecretDataService secretDataService;@Overridepublic boolean supports(MethodParameter methodParameter, Type targetType,Class<? extends HttpMessageConverter<?>> converterType) {return methodParameter.getMethod().isAnnotationPresent(SecretData.class)|| methodParameter.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class);}@Overridepublic 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() {@Overridepublic HttpHeaders getHeaders() {return inputMessage.getHeaders();}@Overridepublic 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> {@Resourceprivate SecretDataService secretDataService;@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return returnType.getMethod().isAnnotationPresent(SecretData.class)|| returnType.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class);}@SneakyThrows@Overridepublic 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 {@Resourceprivate SecretConfig secretConfig;@Overridepublic String decrypt(String data) {return CipherUtil.decrypt(secretConfig.getKey(), data);}@Overridepublic 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
来判断是否为加密数据 - 服务端和客户端都可以根据自己的环境来决定是否开启数据加密
完整代码:https://github.com/mouday/spring-boot-demo/tree/master/SpringBoot-Secret
参考文章
- 详解API接口如何安全的传输数据