教你如何使用AES对接口参数进行加密
前言
我们作为程序猿,在浏览网站的时候偶尔也会打开控制台看看请求的接口,我们会发现有些接口的传输是 “乱码” ,那么这个乱码究竟是什么呢?为什么要这么做?
其实这个所谓的 “乱码” 其实是一种加密后的密文,其原理是前后端提前约定好一种协议,在该协议下进行加解密的处理,例如:前端将数据加密后发送给后端,后端接收到参数后,第一时间先在约定好的协议下将密文解密成可识别的对象,再进行逻辑处理,最后将结果加密返回给前端,前端获取到密文后,同样依照约定好的协议对密文进行解密,最后将解密出来的数据拿来使用。
那么我们想实现同样的效果,应该如何做呢?别急,听哥们给你一一道来。
介绍
一般来说加密算法会分为两种:对称加密算法
和 非对称加密算法
。
对称加密算法
摘自百度百科: 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。
非对称加密算法
摘自百度百科: 不对称加密算法使用两把完全不同但又是完全匹配的一对钥匙—公钥和私钥。在使用不对称加密算法加密文件时,只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。
经过百度百科中的简单概要,我们已经知道了对称加密算法
和 非对称加密算法
都是什么,但它们中间又有什么不同呢?早就猜到你会这么问了,所以我已经把它们的区别一一列出来了。
区别
密钥
对称加密: 一共只有一种密钥,并将该密钥同时用来加解密。
非对称加密: 共有两种密钥:公钥
和 私钥
,使用公钥来加密,使用私钥来解密。
速度
对称加密: 算法简单且加解密容易,所以执行效率高、速度快。
非对称加密: 由于加密算法比较复杂,所以加解密的效率很低,速度远不如 对称加密
。
安全性
对称加密: 由于加解密均使用的为同一个密钥,那么若密钥泄露则有被破解密文的风险。
非对称加密: 由于使用了两种密钥,且公钥是可公开的密钥,使用私钥来进行解密,消除了用户交换密钥的条件,极大程度上保证了数据安全。
实现
在这里给大家介绍一下 AES + CBC + PKCS5Padding
的加密方式,具体实现如下:
引入依赖
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
编写密钥荷载
注意:这里的
AES_KEY
和AES_IV
可以自定义,但 必须是16位的。
/**
* @author Bummon
* @description 荷载
* @date 2023-08-12 10:27
*/
public class Common {
/**
* AES密钥
*/
public static final byte[] AES_KEY = "Ct9x5IUNHlhq0siZ".getBytes();
/**
* AES偏移
*/
public static final byte[] AES_IV = "MIIBIjANBgkqhkiG".getBytes();
}
编写AES工具类
import com.test.constant.Common;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @author Bummon
* @description AES工具类
* @date 2023-08-12 09:26
*/
public class AESUtils {
private static final String ALGORITHMSTR = "AES/CBC/PKCS5Padding";
/**
* @param content 加密内容
* @return {@link String}
* @date 2023-08-12 09:27
* @author Bummon
* @description 加密
*/
public static String encrypt(String content) {
String encode = null;
try {
Cipher cipher = initCipher(Cipher.ENCRYPT_MODE);
byte[] encodeBytes = cipher.doFinal(content.getBytes());
encode = Base64.encodeBase64String(encodeBytes);
} catch (Exception e) {
e.printStackTrace();
}
return encode;
}
public static String decrypt(String encryptStr) {
String decode = null;
try {
Cipher cipher = initCipher(Cipher.DECRYPT_MODE);
byte[] encodeBytes = Base64.decodeBase64(encryptStr);
byte[] decodeBytes = cipher.doFinal(encodeBytes);
decode = new String(decodeBytes);
} catch (Exception e) {
e.printStackTrace();
}
return decode;
}
/**
* @param cipherMode 操作类型 加密/解密
* @return {@link Cipher}
* @date 2023-08-12 09:42
* @author Bummon
* @description 初始化Cipher
*/
private static Cipher initCipher(int cipherMode) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
SecretKeySpec keySpec = new SecretKeySpec(Common.AES_KEY, "AES");
IvParameterSpec ivParam = new IvParameterSpec(Common.AES_IV);
cipher.init(cipherMode, keySpec, ivParam);
return cipher;
}
public static void main(String[] args) {
String encrypt = AESUtils.encrypt("Hello World");
String decrypt = AESUtils.decrypt(encrypt);
System.out.println(encrypt);
System.out.println(decrypt);
}
}
自定义注解
该注解作用于接口上,可以对接口的加密或者解密实现更加粒子化的控制,默认入参解密,出参加密。
import org.springframework.web.bind.annotation.Mapping;
import java.lang.annotation.*;
/**
* @author Bummon
* @description AES加解密注解
* @date 2023-08-12 09:44
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Mapping
@Documented
public @interface AES {
/**
* 入参是否解密,默认解密
*/
boolean inDecode() default true;
/**
* 出参是否加密,默认加密
*/
boolean outEncode() default true;
}
DecodeRequestBodyAdvice
import com.test.anno.AES;
import com.test.util.pwd.AESUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
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.RequestBodyAdvice;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
/**
* @author Bummon
* @date 2023-08-12 10:22
* @description 请求数据解密
*/
@Slf4j
@ControllerAdvice(basePackages = "com.test.controller")
public class DecodeRequestBodyAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
try {
boolean encode = false;
if (methodParameter.getMethod().isAnnotationPresent(AES.class)) {
//获取注解配置的包含和去除字段
AES aes = methodParameter.getMethodAnnotation(AES.class);
//入参是否需要解密
encode = aes.decode();
}
if (encode) {
log.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密");
return new MyHttpInputMessage(inputMessage);
} else {
return inputMessage;
}
} catch (Exception e) {
e.printStackTrace();
log.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
return inputMessage;
}
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}
class MyHttpInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private InputStream body;
public MyHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
this.headers = inputMessage.getHeaders();
String param = IOUtils.toString(inputMessage.getBody(), "UTF-8");
//去除请求数据中的转义字符
String encryptStr = easpString(param).replace("\"", "");
String decrypt = AESUtils.decrypt(encryptStr);
this.body = IOUtils.toInputStream(decrypt, "UTF-8");
}
@Override
public InputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
/**
* @param param
* @return
*/
public String easpString(String param) {
if (param != null && !param.equals("")) {
String s = "{\"param\":";
//去除param中的转义字符
String data = param.replaceAll("\\s*|\r|\n|\t", "");
if (!data.startsWith(s)) {
throw new RuntimeException("参数【param】缺失异常!");
} else {
int closeLen = data.length() - 1;
int openLen = "{\"param\":".length();
String substring = StringUtils.substring(data, openLen, closeLen);
return substring;
}
}
return "";
}
}
}
EncodeResponseBodyAdvice
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.anno.AES;
import com.test.util.pwd.AESUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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;
/**
* @author Bummon
* @date 2023-08-12 10:36
* @description 返回参数加密
*/
@Slf4j
@ControllerAdvice(basePackages = "com.test.controller")
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter methodParameter,
MediaType mediaType,
Class aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
boolean encode = false;
if (methodParameter.getMethod().isAnnotationPresent(AES.class)) {
//获取注解配置的包含和去除字段
AES aes = methodParameter.getMethodAnnotation(AES.class);
//出参是否需要加密
encode = aes.encode();
}
if (encode) {
log.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行加密");
ObjectMapper objectMapper = new ObjectMapper();
try {
String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
return AESUtils.encrypt(result);
} catch (Exception e) {
e.printStackTrace();
log.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
}
}
return body;
}
}
编写测试控制器
import java.util.HashMap;
import java.util.Map;
/**
* @author Bummon
* @description
* @date 2023-08-12 10:37
*/
@RestController
public class TestController {
@AES(decode = false)
@GetMapping("/getSecret")
public Object getSecret() {
Map<String, Object> map = new HashMap<>();
map.put("name", "Bummon");
map.put("homeUrl", "https://www.bummon.com/");
map.put("blogUrl", "https://blog.bummon.com/");
return map;
}
@AES(encode = false)
@PostMapping("/getBySecret")
public Object getBySecret(@RequestBody Map<String, Object> map) {
return map;
}
}
我们在这里编写了两个接口,其中 getSecret
接口不对入参进行解密,对出参进行加密,也就是前端传明文,后端返回为密文。getBySecret
接口是对入参进行解密,不对出参加密,也就是前端传密文,后端返回为明文。
我们的测试思路就是先测试getSecret
接口,同时也获取到了密文,在测试getBySecret
接口时将getSecret
接口返回的密文作为参数传进去。
测试
我们通过getSecret
接口拿到了密文,接下来将该密文作为参数调用getBySecret
接口。
可以看到我们成功将密文解析了出来,并且对接口入参没有影响。
感谢观看。
推荐
关注博客和公众号获取最新文章
Bummon’s Blog | Bummon’s Home | 公众号