保姆式若依cloud实现api接口加密

722 阅读4分钟

基于ruoyi-cloud版本实现api加密

参考文章:若依框架添加出入参加密解密-阿里云开发者社区 (aliyun.com)

由于项目需求要求实现API接口的加密功能,而若依框架虽然提供了密码加密的支持,但并未直接提供API接口加密的解决方案。因此,我决定记录并分享一下实现这一功能的过程。

前端部分

  1. 安装 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里。

  1. 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;
    }
}

三、结果

image.png

image.png

大功告成!!!