前言:
在公司协作开发的过程中,自己的项目是公共调用平台,也可以说是中转平台,供公司其他团队的项目进行接口调用。因为是不同团队项目之间的相互调用,所以不能通过openFeign远程调用。只能通过http远程调用,但是在http调用的过程中数据不能通过明文传输,这样是不安全的。所以下面演示如何使用AES秘钥,对接口数据进行加解密。
目录
一,引入maven依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.37</version>
</dependency>
二,生成AES对称秘钥
运行这一块代码输出秘钥:
@Test
public void test() {
byte[] keyBytes = SecureUtil.generateKey("AES").getEncoded();
String keyStr = Base64.encode(keyBytes); // 转 Base64 字符串,方便存配置
System.out.println("AES 密钥:" + keyStr);
}
三,将控制台打印的秘钥保存,粘贴到yml文件中去
四,核心加解密工具类
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class AesKit {
@Value("${aes.key}")
private String key;
private SymmetricCrypto aes;
@PostConstruct
public void init() {
this.aes = new SymmetricCrypto(SymmetricAlgorithm.AES, Base64.decode(key));
}
public String encrypt(String plain) {
return aes.encryptBase64(plain);
}
public String decrypt(String cipher) {
return aes.decryptStr(cipher);
}
}
五,封装统一返回对象工具类
import com.zqd.common.constant.HttpStatus;
import com.zqd.common.utils.uuid.IdUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 加密data业务数据响应实体类
*/
@ApiModel(
value = "返回对象",
description = "返回结果"
)
@Data
public class EncryptionR<T> implements Serializable {
private static final long serialVersionUID = 1L;
/** 成功 */
public static final int SUCCESS = HttpStatus.SUCCESS;
/** 失败 */
public static final int FAIL = HttpStatus.ERROR;
@ApiModelProperty("返回code值")
private int code;
@ApiModelProperty("时间戳")
private Long timestamp=System.currentTimeMillis();
@ApiModelProperty("随机数")//如果生成uuid工具类用不了那就切换成你自己生成uuid的工具类
private String nonce= IdUtils.fastSimpleUUID();
@ApiModelProperty("返回提示信息")
private String msg;
@ApiModelProperty("返回数据")
private T data;
public static <T> EncryptionR<T> ok()
{
return restResult(null, SUCCESS, "操作成功");
}
public static <T> EncryptionR<T> ok(T data)
{
return restResult(data, SUCCESS, "操作成功");
}
public static <T> EncryptionR<T> ok(T data, String msg)
{
return restResult(data, SUCCESS, msg);
}
public static <T> EncryptionR<T> fail()
{
return restResult(null, FAIL, "操作失败");
}
public static <T> EncryptionR<T> fail(String msg)
{
return restResult(null, FAIL, msg);
}
public static <T> EncryptionR<T> fail(T data)
{
return restResult(data, FAIL, "操作失败");
}
public static <T> EncryptionR<T> fail(T data, String msg)
{
return restResult(data, FAIL, msg);
}
public static <T> EncryptionR<T> fail(int code, String msg)
{
return restResult(null, code, msg);
}
private static <T> EncryptionR<T> restResult(T data, int code, String msg)
{
EncryptionR<T> apiResult = new EncryptionR<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
}
5.1,校验重复请求工具类(可选,非必要)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class HttpRequestCheck {
@Autowired
public RedisTemplate redisTemplate;
/**
* 校验重复请求
* @param timestamp 时间戳
* @param nonce 随机数
* @return
*/
public void isRepeatSubmit(Long timestamp, String nonce) {
long now = System.currentTimeMillis();
long diff = Math.abs(now - timestamp);
// 允许 60 秒误差(发送请求到接收到请求的时间差)
if (diff > 60000) {
throw new RuntimeException("请求已过期");
}
// 防止重复 nonce:Redis 查重
String key = "nonce:" + nonce;
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
throw new RuntimeException("重复请求");
}
redisTemplate.opsForValue().set(key, "1", Duration.ofMinutes(5));
}
}
六,接收加密后的数据请求接口(解密示例接口)
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSON;
import com.zqd.common.annotation.Anonymous;
import com.zqd.common.constant.HttpStatus;
import com.zqd.common.exception.ServiceException;
import com.zqd.system.serverutils.dome2.AesKit;
import com.zqd.system.serverutils.dome2.EncryptionR;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
@RequestMapping("/domeD")
@Api(tags = "测试D")
public class DomeTestControllerB {
private static final Logger log = LoggerFactory.getLogger(DomeTestController.class);
@Autowired
AesKit aesKit;
// @Autowired
// HttpRequestCheck httpRequestCheck;
@PostMapping("/testD")
@ApiOperation(value = "内测试D")
@Anonymous
public EncryptionR queryIncompleteExamination(@RequestBody @Validated String cipher) {
EncryptionR req = JSONUtil.toBean(cipher, EncryptionR.class);
if(!Objects.equals(HttpStatus.SUCCESS, req.getCode())){
throw new ServiceException("请求失败:"+req.getMsg());
}
//校验重复请求(可选)
// httpRequestCheck.isRepeatSubmit(req.getTimestamp(), req.getNonce());
log.info("接收到的密文:{}", req.getData());
String plain = aesKit.decrypt(req.getData().toString());
log.info("解密后的明文:{}", JSON.toJSONString(plain));
String resp = JSONUtil.toJsonStr("哈哈哈111111111111返回");
// 直接返回其他密文字符串
return EncryptionR.ok(aesKit.encrypt(resp));
}
}
七,发送请求加密data业务数据(加密示例接口)
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSON;
import com.zqd.common.annotation.Anonymous;
import com.zqd.common.constant.HttpStatus;
import com.zqd.common.exception.ServiceException;
import com.zqd.system.serverutils.dome.dome.AesKit;
import com.zqd.system.serverutils.dome.dome.EncryptionR;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
@RequestMapping("/domeE")
@Api(tags = "测试E")
public class DomeTestControllerA {
@Autowired
AesKit aesKit;
@PostMapping("/testE")
@ApiOperation(value = "内测试E")
@Anonymous
public EncryptionR<String> queryIncompleteExamination() {
// 1. 构造业务 JSON
String json = JSONUtil.toJsonStr("哈哈哈哈");
// 2. 加密
String cipher = aesKit.encrypt(json);
// 3. 发 HTTP,直接发字符串
String respCipher = HttpRequest.post("http://localhost:****/game/domeD/testD")//请求地址换成你自己的目标地址url
.body(JSON.toJSONString(EncryptionR.ok(cipher)))
.execute()
.body();
EncryptionR bean = JSONUtil.toBean(respCipher, EncryptionR.class);
if(!Objects.equals(HttpStatus.SUCCESS, bean.getCode())){
throw new ServiceException("请求失败:"+bean.getMsg());
}
// 4. 解密 B 的返回
String plainResp = aesKit.decrypt(bean.getData().toString());
return EncryptionR.ok(plainResp);
}
}
八,发送请求测试
在DomeTestControllerA中加密业务数据发送请求到DomeTestControllerB解密数据。
DomeTestControllerA:
DomeTestControllerB:
后面DomeTestControllerB响应的业务数据也需要加密响应给DomeTestControllerA,DomeTestControllerA以相同的方式解密即可。
到此就可以实现接口数据的加解密过程了。
九,升级拓展
如果你想实现校验重复请求,打开这行代码即可。
如果觉得粒度不够,没有细粒度到用户id。那么你也可以在我的代码基础上修改。
修改步骤:
1,在HttpRequestCheck中添加属性userId
2,在HttpRequestCheck重载一个isRepeatSubmit方法,入参添加一个用户id。redsi缓存的key在随机数前面加上用户id即可
3,根据你的业务需求调用校验方法
到此结束!!!