springboot结合AES和国密SM4进行接口加密
api接口加密
1.为什么需要api接口加密呢?
1.防止爬虫
2.防止数据被串改
3.确保数据安全
2.如何实现接口加密呢?

3.我们可以使用哪些加密算法来加密呢?
AES
密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。
SM4
SM4.0(原名SMS4.0)是中华人民共和国政府采用的一种分组密码标准,由国家密码管理局于2012年3月21日发布。相关标准为“GM/T 0002-2012《SM4分组密码算法》(原SMS4分组密码算法)”。在商用密码体系中,SM4主要用于数据加密,其算法公开,分组长度与密钥长度均为128bit,加密算法与密钥扩展算法都采用32轮非线性迭代结构,S盒为固定的8比特输入8比特输出。SM4.0中的指令长度被提升到大于64K(即64×1024)的水平,这是SM 3.0规格(渲染指令长度允许大于512)的128倍。

4.具体实现
4.1后端
在这之前我们先了解一个类RequestBodyAdviceAdapter,主要通过该类实现~
RequestBodyAdviceAdapter
官网解释:RequestBodyAdviceAdapter (Spring Framework 6.2.1 API)
RequestBodyAdviceAdapter是Spring MVC中用于增强请求体处理的一个工具类,它实现了RequestBodyAdvice接口。RequestBodyAdviceAdapter提供了三个主要方法:beforeBodyRead、afterBodyRead和handleEmptyBody。
-
beforeBodyRead:这个方法在请求体被读取之前调用,主要用于预处理请求体。默认实现是直接返回传入的
HttpInputMessage对象。 -
afterBodyRead:在请求体被读取并转换为对象之后调用,用于对读取到的对象进行进一步处理。默认实现是直接返回转换后的对象。
-
handleEmptyBody:当请求体为空时调用,用于处理这种情况。默认实现是直接返回null。
代码实现
这里我们使用类似Aop的思想实现,定义对应的注解!
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiDecrypt { //解密
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiEncrypt {//加密
}
@Slf4j
@ControllerAdvice
public class DecryptRequestAdvice extends RequestBodyAdviceAdapter {
private static final String ENCODING = "UTF-8";
@Resource
private ApiEncryptService apiEncryptService;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(ApiDecrypt.class) || methodParameter.hasParameterAnnotation(ApiDecrypt.class)
|| methodParameter.getContainingClass().isAnnotationPresent(ApiDecrypt.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
try {
String bodyStr = IOUtils.toString(inputMessage.getBody(), ENCODING);
ApiEncryptForm apiEncryptForm = JSONObject.parseObject(bodyStr, ApiEncryptForm.class);
if (StrUtil.isEmpty(apiEncryptForm.getEncryptData())) {
return inputMessage;
}
String decrypt = apiEncryptService.decrypt(apiEncryptForm.getEncryptData());
return new DecryptHttpInputMessage(inputMessage.getHeaders(), IOUtils.toInputStream(decrypt, ENCODING));
} catch (Exception e) {
return inputMessage;
}
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
static class DecryptHttpInputMessage implements HttpInputMessage {
private final HttpHeaders headers;
private final InputStream body;
public DecryptHttpInputMessage(HttpHeaders headers, InputStream body) {
this.headers = headers;
this.body = body;
}
@Override
public InputStream getBody() {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
}
@Slf4j
@ControllerAdvice
public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
@Resource
private ApiEncryptService apiEncryptService;
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(ApiEncrypt.class) || returnType.getContainingClass().isAnnotationPresent(ApiEncrypt.class);
}
@Override
public ResponseDTO beforeBodyWrite(ResponseDTO body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body.getData() == null) {
return body;
}
String encrypt = null;
try {
encrypt = apiEncryptService.encrypt(objectMapper.writeValueAsString(body.getData()));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
body.setData(encrypt);
body.setDataType(DataTypeEnum.ENCRYPT.getValue());
return body;
}
}
实现类
/**
* AES 加密和解密
* 1、AES加密算法支持三种密钥长度:128位、192位和256位,这里选择128位
* 2、AES 要求秘钥为 128bit,转化字节为 16个字节;
* 3、js前端使用 UCS-2 或者 UTF-16 编码,字母、数字、特殊符号等 占用1个字节;
* 4、所以:秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
*/
@Slf4j
public class ApiEncryptServiceAesImpl implements ApiEncryptService {
private static final String CHARSET = "UTF-8";
private static final String AES_KEY = "1024lab__1024lab";
static {
Security.addProvider(new BouncyCastleProvider());
}
@Override
public String encrypt(String data) {
try {
// AES 加密 并转为 base64
AES aes = new AES(hexToBytes(stringToHex(AES_KEY)));
return aes.encryptBase64(data);
} catch (Exception e) {
log.error(e.getMessage(), e);
return StringConst.EMPTY;
}
}
@Override
public String decrypt(String data) {
try {
// 第一步: Base64 解码
byte[] base64Decode = Base64.getDecoder().decode(data);
// 第二步: AES 解密
AES aes = new AES(hexToBytes(stringToHex(AES_KEY)));
byte[] decryptedBytes = aes.decrypt(base64Decode);
return new String(decryptedBytes, CHARSET);
} catch (Exception e) {
log.error(e.getMessage(), e);
return StringConst.EMPTY;
}
}
/**
* 16 进制串转字节数组
*
* @param hex 16进制字符串
* @return byte数组
*/
public static byte[] hexToBytes(String hex) {
int length = hex.length();
byte[] result;
if (length % 2 == 1) {
length++;
result = new byte[(length / 2)];
hex = "0" + hex;
} else {
result = new byte[(length / 2)];
}
int j = 0;
for (int i = 0; i < length; i += 2) {
result[j] = hexToByte(hex.substring(i, i + 2));
j++;
}
return result;
}
public static String stringToHex(String input) {
char[] chars = input.toCharArray();
StringBuilder hex = new StringBuilder();
for (char c : chars) {
hex.append(Integer.toHexString((int) c));
}
return hex.toString();
}
/**
* 16 进制字符转字节
*
* @param hex 16进制字符 0x00到0xFF
* @return byte
*/
private static byte hexToByte(String hex) {
return (byte) Integer.parseInt(hex, 16);
}
public static void main(String[] args) {
// System.out.println(new ApiEncryptServiceAesImpl().encrypt("{\"name\":\"chen\",\"age\":18}"));
System.out.println(new ApiEncryptServiceAesImpl().decrypt("MWFjZTI3MDA0ZDcxY2RlM2U4YTY5NDU3MjE3MmE0YzlhOTI0OTVhOTBiM2VmYzYwZjgxZTlmMjRmNDVkMWNlMg=="));
}
}
@Slf4j
@Service
public class ApiEncryptServiceSmImpl implements ApiEncryptService {
private static final String CHARSET = "UTF-8";
private static final String SM4_KEY = "1024lab__1024lab";
static {
Security.addProvider(new BouncyCastleProvider());
}
@Override
public String encrypt(String data) {
try {
// 第一步: SM4 加密
SM4 sm4 = new SM4(hexToBytes(stringToHex(SM4_KEY)));
String encryptHex = sm4.encryptHex(data);
// 第二步: Base64 编码
return new String(Base64.getEncoder().encode(encryptHex.getBytes(CHARSET)), CHARSET);
} catch (Exception e) {
log.error(e.getMessage(), e);
return StringConst.EMPTY;
}
}
@Override
public String decrypt(String data) {
try {
// 第一步: Base64 解码
byte[] base64Decode = Base64.getDecoder().decode(data);
// 第二步: SM4 解密
SM4 sm4 = new SM4(hexToBytes(stringToHex(SM4_KEY)));
return sm4.decryptStr(new String(base64Decode));
} catch (Exception e) {
log.error(e.getMessage(), e);
return StringConst.EMPTY;
}
}
public static String stringToHex(String input) {
char[] chars = input.toCharArray();
StringBuilder hex = new StringBuilder();
for (char c : chars) {
hex.append(Integer.toHexString((int) c));
}
return hex.toString();
}
/**
* 16 进制串转字节数组
*
* @param hex 16进制字符串
* @return byte数组
*/
public static byte[] hexToBytes(String hex) {
int length = hex.length();
byte[] result;
if (length % 2 == 1) {
length++;
result = new byte[(length / 2)];
hex = "0" + hex;
} else {
result = new byte[(length / 2)];
}
int j = 0;
for (int i = 0; i < length; i += 2) {
result[j] = hexToByte(hex.substring(i, i + 2));
j++;
}
return result;
}
/**
* 16 进制字符转字节
*
* @param hex 16进制字符 0x00到0xFF
* @return byte
*/
private static byte hexToByte(String hex) {
return (byte) Integer.parseInt(hex, 16);
}
}
4.2前端
axios.js
import axios from 'axios'
import { decryptData, encryptData } from './encrypt.js';
const requests = axios.create({
//共同的接口前缀
baseURL: 'http://localhost:8881',
//超时时间
timeout: 2000
});
//请求拦截器:再发请求之前,请求拦截器可以检查到,可以在请求发出去之前做一些事情
requests.interceptors.request.use(config => {
return config
}), err => {
return Promise.reject(err)
}
//响应拦截器
requests.interceptors.response.use(response => {
// 如果是加密数据
if (response.data.dataType === 10) {
response.data.encryptData = response.data.data;
let decryptStr = decryptData(response.data.data);
if (decryptStr) {
response.data.data = JSON.parse(decryptStr);
}
}
return response.data
}, err => {
return Promise.reject(err)
})
// ================================= 加密 =================================
/**
* 加密请求参数的post请求
*/
export const postEncryptRequest = (url, data) => {
return requests({
data: { encryptData: encryptData(data) },
url,
method: 'post',
});
};
/**
* post请求
*/
export const postRequest = (url, data) => {
return requests({
data,
url,
method: 'post',
});
};
/**
* get请求 /api?encryptData=xxx
*/
export const getEncryptRequest = (url, data) => {
return requests({
params: { encryptData: encryptData(data) },
url,
method: 'get',
});
};
前端加密
import CryptoJS from 'crypto-js';
import CryptoSM from 'sm-crypto';
function object2string(data) {
if (typeof data === 'object') {
return JSON.stringify(data);
}
let str = JSON.stringify(data);
if (str.startsWith("'") || str.startsWith('"')) {
str = str.substring(1);
}
if (str.endsWith("'") || str.endsWith('"')) {
str = str.substring(0, str.length - 1);
}
return str;
}
/**
* 字符串转为数字
*/
function stringToHex(str) {
let hex = '';
for (let i = 0; i < str.length; i++) {
hex += str.charCodeAt(i).toString(16).padStart(2, '0');
}
return hex;
}
/*
* -------------------- ※ AES 加密、解密 begin ※ --------------------
*
* 1、AES加密算法支持三种密钥长度:128位、192位和256位,这里选择128位
* 2、AES 要求秘钥为 128bit,转化字节为 16个字节;
* 3、js前端使用 UCS-2 或者 UTF-16 编码,字母、数字、特殊符号等 占用1个字节;
* 4、所以:秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
*
* -------------------- ※ AES 加密、解密 end ※ --------------------
*/
const AES_KEY = '1024lab__1024lab';
const AES = {
encryptData: function (data) {
// AES 加密 并转为 base64
let utf8Data = CryptoJS.enc.Utf8.parse(object2string(data));
const key = CryptoJS.enc.Utf8.parse(AES_KEY);
const encrypted = CryptoJS.AES.encrypt(utf8Data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
},
decryptData: function (data) {
// 第一步:Base64 解码
let words = CryptoJS.enc.Base64.parse(data);
// 第二步:AES 解密
const key = CryptoJS.enc.Utf8.parse(AES_KEY);
return CryptoJS.AES.decrypt({ ciphertext: words }, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}).toString(CryptoJS.enc.Utf8);
},
};
/*
* -------------------- ※ 国密SM4算法 加密、解密 begin ※ --------------------
*
* 1、国密SM4 要求秘钥为 128bit,转化字节为 16个字节;
* 2、js前端使用 UCS-2 或者 UTF-16 编码,字母、数字、特殊符号等 占用1个字节;
* 3、java中 每个 字母数字 也是占用1个字节;
* 4、所以:前端和后端的 秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
*
* -------------------- ※ 国密SM4算法 加密、解密 end ※ --------------------
*/
// 秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
const SM4_KEY = '1024lab__1024lab';
const SM4 = {
encryptData: function (data) {
// 第一步:SM4 加密
let encryptData = CryptoSM.sm4.encrypt(object2string(data), stringToHex(SM4_KEY));
// 第二步: Base64 编码
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptData));
},
decryptData: function (data) {
// 第一步:Base64 解码
let words = CryptoJS.enc.Base64.parse(data);
let decode64Str = CryptoJS.enc.Utf8.stringify(words);
// 第二步:SM4 解密
return CryptoSM.sm4.decrypt(decode64Str, stringToHex(SM4_KEY));
},
};
// ----------------------- 对外暴露: 加密、解密 -----------------------
// 默认使用SM4算法
const EncryptObject = SM4;
// const EncryptObject = AES;
/**
* 加密
*/
export const encryptData = function (data) {
return !data ? null : EncryptObject.encryptData(data);
};
/**
* 解密
*/
export const decryptData = function (data) {
return !data ? null : EncryptObject.decryptData(data);
};
api.js
import { postRequest, postEncryptRequest, getEncryptRequest } from '@/utils/axios.js';
// 测试获取列表 双向加密
export const getList = (data) => {
return postEncryptRequest(
'/api/test/getList',
data
)
}
export const getListOfTetsGet = (data) => {
return getEncryptRequest(
'/api/testget/getList',
data
)
}
// 请求加密paramsAes
export const getListParamsAes = (data) => {
return postEncryptRequest(
'/api/paramsAes/getList',
data
)
}
// 响应加密 /resAes/getList
export const getListResAes = (data) => {
return postRequest(
'/api/resAes/getList',
data
)
}
vue界面
<template>
<div>
<el-button type="primary" @click="aes">发送加密请求</el-button>
<el-button type="primary" @click="paramsAsc">请求体加密</el-button>
<el-button type="primary" @click="reqAsc">返回加密</el-button>
<div>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="title" label="书标题">
</el-table-column>
<el-table-column prop="author" label="作者">
</el-table-column>
<el-table-column prop="price" label="价格">
</el-table-column>
<el-table-column fixed="right" label="操作" width="100">
<template slot-scope="scope">
<el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
<el-button type="text" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import { getList, getListParamsAes, getListResAes, getListOfTetsGet } from "@/api/test.js"
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
tableData: []
}
},
mounted() {
this.aes()
},
methods: {
aes() {
getList({ pageNum: 1, pageSize: 3 }).then(res => {
if (res.code == 0) {
this.tableData = res.data
}
})
},
paramsAsc() {
getListParamsAes({ pageNum: 1, pageSize: 3 }).then(res => {
console.log(res);
})
},
reqAsc() {
getListResAes({ pageNum: 1, pageSize: 3 }).then(res => {
console.log(res);
})
}
},
}
</script>
5.测试结果
双向加密

请求体加密

响应体加密

6.声明
该案例参考于开源的smartadmin项目,该项目类似于若依,整体代码比较规范,推荐使用~
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)