基于ruoyi-cloud版本实现api加密
参考文章:若依框架添加出入参加密解密-阿里云开发者社区 (aliyun.com)
由于项目需求要求实现API接口的加密功能,而若依框架虽然提供了密码加密的支持,但并未直接提供API接口加密的解决方案。因此,我决定记录并分享一下实现这一功能的过程。
前端部分
- 安装 crypto-js
npm install crypto-js --save
2. 编写加密js,代码如下
import CryptoJS from 'crypto-js';
/**
* 随机生成16位的AES密钥
* @returns {string}
*/
export const getAESKey = () => {
var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
var n = 16;
var res = "";
for (var i = 0; i < n; i++) {
var id = Math.ceil(Math.random() * 35);
res += chars[id];
}
return res;
}
/**
* 加密
* @param data
* @param aesKey
* @returns {string}
* @constructor
*/
export const WithAes = (data, aesKey) => {
let key = CryptoJS.enc.Utf8.parse(aesKey);
let srcs = CryptoJS.enc.Utf8.parse(data);
let encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString();
};
/**
* 解密Aes
* @param data
* @param aesKey
* @returns {string}
*/
export const decryptAes = (data, aesKey) => {
let keyStr;
keyStr = aesKey ? aesKey : "1234567890abcdef";
var key = CryptoJS.enc.Utf8.parse(keyStr);
var decrypt = CryptoJS.AES.decrypt(CryptoJS.format.Hex.parse(data), key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
};
3. 然后在 request.js request拦截器添加加密和响应拦截器添加解密
// request拦截器 加密部分代码
const aesKey = getAESKey();
config.headers['encrypt-key'] = aesKey;
config.data =typeof config.data === "object" ? WithAes(JSON.stringify(config.data), aesKey) : WithAes(config.data, aesKey);
// 响应拦截器
const keyStr = res.headers['encrypt-key'];
if (keyStr != null && keyStr != '') {
const Str = decryptAes(res.data, keyStr);
res.data = JSON.parse(Str);
}
到这里我们的前端工作就可以到一段落了!
二、后端部分
其实做法和参考文章差不多,主要是把加密开关提取到了yml里。
- api接口加密配置属性
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "api-decrypt")
public class DecryptProperties {
/**
* 验证码开关
*/
private boolean enabled;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
2. 我通过参考文章的方法获取body是null,所以我是另外写了一个filter,通过上下文获取请求体。
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 检查是否是PUT或POST请求
if (HttpMethod.PUT.matches(request.getMethodValue()) || HttpMethod.POST.matches(request.getMethodValue())) {
// 将请求体合并为一个DataBuffer
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
// 创建一个字节数组,用于存储请求体的字节数据
byte[] bytes = new byte[dataBuffer.readableByteCount()];
// 将DataBuffer中的数据读取到字节数组中
dataBuffer.read(bytes);
// 释放DataBuffer
DataBufferUtils.release(dataBuffer);
// 将字节数组转换为字符串
String body = new String(bytes, StandardCharsets.UTF_8);
// 创建一个GatewayContext对象,用于存储请求体的字符串
GatewayContext gatewayContext = new GatewayContext();
gatewayContext.setCacheBody(body);
// 将GatewayContext对象存储到ServerWebExchange的属性中
exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext);
// 重新包装请求,以便后续过滤器可以再次读取body
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
// 返回一个包含DataBuffer的Flux
return Flux.just(dataBuffer);
}
};
// 调用GatewayFilterChain的filter方法,继续处理请求
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
// 如果不是PUT或POST请求,直接调用GatewayFilterChain的filter方法,继续处理请求
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
3. 我也是在AuthFilter中做的入参加密
// 解密
if(decryptProperties.isEnabled()){
request = getServerHttpRequest(request, exchange);
}
return chain.filter(exchange.mutate().request(request).build());
/**
* 获取ServerHttpRequest对象
* @param request
* @param exchange
* @return
*/
private ServerHttpRequest getServerHttpRequest(ServerHttpRequest request, ServerWebExchange exchange) {
//获取GatewayContext对象
GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT);
if (gatewayContext != null) {
//获取缓存中的body字符串
String bodyStr = gatewayContext.getCacheBody();
//获取请求头中的encrypt-key
String encryptKey = request.getHeaders().getFirst("encrypt-key");
String decode = "";
try {
//如果bodyStr为空,则直接返回request
if (StringUtils.isEmpty(bodyStr)) {
return request;
}
//使用AES解密方法解密bodyStr
decode = AESUtil.aesDecodeByKey(bodyStr, encryptKey);//解密方法可以自由替换,这里使用AES为例。
} catch (Exception e) {
e.printStackTrace();
}
//获取请求的URI
URI uri = request.getURI();
//将解密后的字符串转换为DataBuffer对象
DataBuffer bodyDataBuffer = stringBuffer(decode);
//将DataBuffer对象转换为Flux对象
Flux<DataBuffer> bodyFlux1 = Flux.just(bodyDataBuffer);
//创建一个新的ServerHttpRequestDecorator对象,并重写getBody方法,返回Flux对象
request = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux1;
}
};
return request;
}
return null;
}
//将字符串转换为DataBuffer对象
private DataBuffer stringBuffer(String value) {
this.value = value;
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
//创建NettyDataBufferFactory对象
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
//分配缓冲区
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
//将字符串写入缓冲区
buffer.write(bytes);
return buffer;
}
4. 出参加密也是大同小异,我是为省事直接hutool工具生成16位字符串,当成key传给前端解密。废话不多说,直接上代码。
@Component
public class ResponseFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(ResponseFilter.class);
private static Joiner joiner = Joiner.on("");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponse response = exchange.getResponse();
String value = exchange.getRequest().getPath().value();
// 排除滑动验证
if (value.equals("/auth/captcha/get") || value.equals("/auth/captcha/check") || value.equals("/auth/captcha/verify")) { return chain.filter(exchange); }
DataBufferFactory dataBufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) {
// 获取ContentType,判断是否返回JSON格式数据
String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (StrUtil.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
//(返回数据内如果字符串过大,默认会切割)解决返回体分段传输
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
List<String> list = Lists.newArrayList();
dataBuffers.forEach(dataBuffer -> {
try {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
list.add(new String(content, "utf-8"));
} catch (Exception e) {
log.info("加载Response字节流异常,失败原因:{}", Throwables.getStackTraceAsString(e));
}
});
String responseData = joiner.join(list);
// hutool 工具随机16位字符串作为aes加密key
String aseKey = RandomUtil.randomString(16);
String s = AESUtil.aesEncodeByKey(responseData, aseKey);
s = s.replaceAll("\r\n", "").replaceAll("\n","");
byte[] uppedContent = new String(s.getBytes(), Charset.forName("UTF-8")).getBytes();
originalResponse.getHeaders().setContentLength(uppedContent.length);
originalResponse.getHeaders().add("encrypt-key", aseKey);
return bufferFactory.wrap(uppedContent);
}));
}
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -2;
}
}
三、结果
大功告成!!!