1、加密解密原理
客戶端和服務(wù)端都可以加密和解密,使用base64進(jìn)行網(wǎng)絡(luò)傳輸
加密方
字符串 -> AES加密 -> base64
解密方
base64 -> AES解密 -> 字符串
2、項(xiàng)目示例
2.1、項(xiàng)目結(jié)構(gòu)
$ 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、常規(guī)業(yè)務(wù)代碼
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 {
// 設(shè)置靜態(tài)資源映射
@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;
/**
* 統(tǒng)一的請(qǐng)求體數(shù)據(jù)
*/
@Data
public class JsonRequest {
/**
* 未加密數(shù)據(jù)
*/
private Object data;
/**
* 加密數(shù)據(jù)
*/
private String encryptData;
}
JsonResult.java
package com.example.demo.response;
import lombok.Data;
/**
* 統(tǒng)一的返回體數(shù)據(jù) 不加密
*/
@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;
/**
* 統(tǒng)一的返回體數(shù)據(jù) 加密
*/
@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;
/**
* 對(duì)該Controller中的所有方法進(jìn)行加解密處理
*/
@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、加密的實(shí)現(xiàn)
application.yml
secret:
key: 1234567890123456 # 密鑰位數(shù)為16位
enabled: true # 開啟加解密功能
SecretData.java
package com.example.demo.annotation;
import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;
/**
* 只有添加有該注解的Controller類或是具體接口方法才進(jìn)行數(shù)據(jù)的加密解密
*/
@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;
/**
* 對(duì)請(qǐng)求內(nèi)容進(jìn)行解密
* 只有開啟了加解密功能才會(huì)生效
* 僅對(duì)使用了@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);
// 默認(rèn)取data數(shù)據(jù),如果提交加密數(shù)據(jù)則解密
String decryptData = null;
if (jsonRequest.getEncryptData() != null) {
decryptData = secretDataService.decrypt(jsonRequest.getEncryptData());
} else{
decryptData = objectMapper.writeValueAsString(jsonRequest.getData());
}
String data = decryptData;
// 解密后的數(shù)據(jù)
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;
/**
* 對(duì)響應(yīng)內(nèi)容加密
*/
@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");
// 僅對(duì)JsonResult對(duì)象數(shù)據(jù)加密
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 {
/**
* 數(shù)據(jù)加密
*
* @param data 待加密數(shù)據(jù)
* @return String 加密結(jié)果
*/
String encrypt(String data);
/**
* 數(shù)據(jù)解密
*
* @param data 待解密數(shù)據(jù)
* @return String 解密后的數(shù)據(jù)
*/
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;
/**
* 具體的加解密實(shí)現(xiàn)
*/
@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;
/**
* 數(shù)據(jù)加密解密工具類
* 加密后返回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);
}
}
}
瀏覽器中實(shí)現(xiàn)加密解密
templates/index.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>接口數(shù)據(jù)加密解密</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>查看控制臺(tái)</h1>
<!-- 加密解密模塊 -->
<script type="text/javascript">
const SECRET_KEY = "1234567890123456";
/**
* 加密方法
* @param data 待加密數(shù)據(jù)
* @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;
}
/**
* 解密數(shù)據(jù)
* @param data 待解密數(shù)據(jù)
*/
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請(qǐng)求模塊 -->
<script type="text/javascript">
// 獲取加密數(shù)據(jù)并解密
axios.get("http://127.0.0.1:8080/user/getUser").then((res) => {
console.log("接收到api返回加密數(shù)據(jù):");
console.log(decrypt(res.data.encryptData));
});
// 提交加密參數(shù)
const data = {
name: "Tom",
age: "18",
};
axios
.post("http://127.0.0.1:8080/user/addUser", {
encryptData: encrypt(data),
})
.then((res) => {
console.log("接收到api返回未加密數(shù)據(jù):");
console.log(res.data);
});
</script>
</body>
</html>
2.4、接口測(cè)試
- 開發(fā)環(huán)境不加密更易于開發(fā)調(diào)試
- 生產(chǎn)環(huán)境需要數(shù)據(jù)加密
前后端都可以通過參數(shù) encryptData
判斷對(duì)方提交/返回的數(shù)據(jù)是否為加密數(shù)據(jù),如果是加密數(shù)據(jù)則進(jìn)行解密操作
接口返回加密數(shù)據(jù)
GET http://127.0.0.1:8080/user/getUser
未加密的數(shù)據(jù)
{
"message": "success",
"data": {
"name": "Tom",
"age": "18"
},
"code": 0
}
返回?cái)?shù)據(jù)
{
"message": "success",
"encryptData": "kun2Wvk2LNKICaXIeIExA7jKRyqOV0qCv5KQXFOzfpQ=",
"code": 0
}
客戶端提交參數(shù)
POST http://127.0.0.1:8080/user/addUser
提交數(shù)據(jù)
{
"data": {
"name": "Tom",
"age": "18"
}
}
提交加密數(shù)據(jù)
{
"encryptData": "kun2Wvk2LNKICaXIeIExA7jKRyqOV0qCv5KQXFOzfpQ="
}
2.5、總結(jié)
服務(wù)端
- 全局開關(guān):通過控制
secret.enabled=true
全局開啟返回?cái)?shù)據(jù)加密 - 全局局部:可以通過
SecretData
或者自定義PassSecretData
來控制單個(gè)控制器或者單個(gè)接口的需要或不需要加密
客戶端
- 可以根據(jù)開發(fā)環(huán)境、測(cè)試環(huán)境、生產(chǎn)環(huán)境來控制是否開啟加密
- 需要注意,F(xiàn)ormData傳輸文件的數(shù)據(jù)格式可以考慮不加密
相同點(diǎn)
- 服務(wù)端和客戶端都通過對(duì)方傳輸?shù)?code>encryptData來判斷是否為加密數(shù)據(jù)
- 服務(wù)端和客戶端都可以根據(jù)自己的環(huán)境來決定是否開啟數(shù)據(jù)加密
完整代碼:https://github.com/mouday/spring-boot-demo/tree/master/SpringBoot-Secret文章來源:http://www.zghlxwxcb.cn/news/detail-682365.html
參考文章文章來源地址http://www.zghlxwxcb.cn/news/detail-682365.html
- 詳解API接口如何安全的傳輸數(shù)據(jù)
到了這里,關(guān)于Java:SpringBoot使用AES對(duì)JSON數(shù)據(jù)加密和解密的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!