springboot gateway 接口加密

网关加密/解密拦截器

使用aes加密,加密工具使用Hutool

  • 解密接口的url参数
  • 解密接口的body参数
  • 加密接口返回数据

1.yml配置

  #接口加密
config:
  crypto:
    #是否启动加密
    enabled: true
    #加密信息字段
    cryptoParam: cryptoParam
    #AES加密key
    appkey: 123456
logging:
  level:
    #日志级别 debug:输出加密过程 info:不输出
    com.ty.filter.CryptoFilter: info

2.配置类 工具类

@Data
@Configuration
@ConfigurationProperties(prefix = "config.crypto")
public class CryptoConfigProperties {
    /**
     * 加密字段,用哪个字段显示加密的数据
     */
    private String cryptoParam;

    /**
     * AES加密key
     */
    private String appkey;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class CryptoVo {
    private String body;
    /**
     * 前端是否加密,加密则响应体也加密 1:url加密 2:body加密
     */
    private Integer encryptFlag;

    private MediaType contentType;
}
@Slf4j
public class CryptoUtil {

 private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();

    private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();

    public static final int REQ_URL = 0;
    public static final int REQ_BODY = 1;
    public static final int RES_DATA = 2;

    /**
     * 解密
     * 6ase64 + aes
     *
     * @param data
     * @param key
     * @return
     */
    public static String decrypt(String data, String key) throws Exception {
        if (StringUtils.isEmpty(data) || StringUtils.isEmpty(key)) {
            return data;
        }
        String data64 = new String(BASE64_DECODER.decode(data), StandardCharsets.UTF_8);
        String result = AESForJsUtil.decryptAES(data64, key);
        if (StringUtils.isEmpty(result)) {
            throw new RuntimeException("解密失败," + data);
        }
        return result;
    }

    /**
     * 加密
     * aes + 6ase64
     *
     * @param data
     * @param key
     * @return
     */
    public static String encrypt(String data, String key) throws Exception {
        if (StringUtils.isEmpty(data) || StringUtils.isEmpty(key)) {
            return data;
        }
        String result = AESForJsUtil.encryptAES(data, key);
        result = new String(BASE64_ENCODER.encode(result.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
        if (StringUtils.isEmpty(result)) {
            throw new RuntimeException("加密失败," + data);
        }
        return result;
    }

    public static void main(String[] args) throws Exception {

    }

3.拦截器

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.charset.StandardCharsets;

/**
 * 加密接口-解密请求拦截器
 * 注意:get请求,body不能传值
 * 1.解密接口的url参数
 * 2.解密接口的body参数
 * 3.加密接口返回数据
 *
 * @date 2023-12-28
 */
@Slf4j
@Configuration
@ConditionalOnProperty(value = "config.crypto.enabled", havingValue = "true", matchIfMissing = false)
public class CryptoFilter implements GlobalFilter, Ordered {

    /**
     * 加密配置属性
     */
    @Resource
    private CryptoConfigProperties cryptoConfigProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders headers = request.getHeaders();
        CryptoVo cryptoVo = new CryptoVo();
        cryptoVo.setEncryptFlag(0);
        //对url的参数进行解密
        updateUrlParam(exchange, cryptoVo);
        //不存在body则直接返回
        if (HttpMethod.GET.equals(method) || !MediaType.APPLICATION_JSON.isCompatibleWith(headers.getContentType())) {
            if (cryptoVo.getEncryptFlag() > 0) {
                //响应体加密
                exchange = exchange.mutate().response(serverHttpResponseDecorator(response, cryptoVo)).build();
            }
            return chain.filter(exchange);
        }
        //application/json对body的参数解密
        Flux<DataBuffer> body = request.getBody();
        Mono<DataBuffer> dataBufferMono = DataBufferUtils.join(body);
        ServerWebExchange finalExchange = exchange;
        return dataBufferMono.flatMap(dataBuffer -> {
            try {
                // 获取请求参数
                String originalRequestBody = getOriginalRequestBody(dataBuffer);
                cryptoVo.setBody(originalRequestBody);
                // 解密请求参数
                String decryptRequestBody = decryptRequest(request, cryptoVo);
                // 重写请求和响应方式
                return getEnResponseBody(finalExchange, chain, request, decryptRequestBody, response, cryptoVo);
            } catch (Exception e) {
                log.error("密文过滤器加解密错误", e);
                return Mono.error(e);
            } finally {
                DataBufferUtils.release(dataBuffer);
            }
        });
    }

    /**
     * 返回加密响应体
     *
     * @param exchange
     * @param chain
     * @param request
     * @param decryptRequestBody
     * @param response
     * @param cryptoVo
     * @return
     */
    private Mono<Void> getEnResponseBody(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request, String decryptRequestBody, ServerHttpResponse response, CryptoVo cryptoVo) {
        // 新的请求体
        ServerHttpRequestDecorator requestDecorator = serverHttpRequestDecorator(request, decryptRequestBody);
        //加密响应体
        ServerHttpResponseDecorator serverHttpResponseDecorator = serverHttpResponseDecorator(response, cryptoVo);
        ServerWebExchange serverWebExchange = exchange.mutate().request(requestDecorator).response(serverHttpResponseDecorator).build();
        // 使用新的请求和响应转发
        return chain.filter(serverWebExchange);
    }

    /**
     * body解密
     *
     * @param request
     * @param cryptoVo
     * @return
     */
    private String decryptRequest(ServerHttpRequest request, CryptoVo cryptoVo) {
        String body = cryptoVo.getBody();
        if (!JSON.isValid(body)) {
            return body;
        }
        JSONObject jsonObject = JSON.parseObject(body);
        String cryptoParam = jsonObject.getString(cryptoConfigProperties.getCryptoParam());
        if (!StringUtils.isEmpty(cryptoParam)) {
            cryptoVo.setEncryptFlag(2);
            try {
                body = CryptoUtil.decrypt(cryptoParam, cryptoConfigProperties.getAppkey());
                cryptorDetail(request.getPath(), cryptoParam, body, CryptoUtil.REQ_BODY);
            } catch (Exception e) {
                log.error("【body解密失败】路径:{},参数:{}", request.getPath(), body, e);
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        return body;
    }

    /**
     * 新建请求体
     *
     * @param originalRequest
     * @param decryptRequestBody
     * @return
     */
    private ServerHttpRequestDecorator serverHttpRequestDecorator(ServerHttpRequest originalRequest, String decryptRequestBody) {
        return new ServerHttpRequestDecorator(originalRequest) {
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                return httpHeaders;
            }

            @Override
            public Flux<DataBuffer> getBody() {
   if (decryptRequestBody == null) {
                    return super.getBody();
                }
                byte[] bytes = decryptRequestBody.getBytes(StandardCharsets.UTF_8);
                return Flux.just(new DefaultDataBufferFactory().wrap(bytes));
            }
        };
    }


    /**
     * 新建响应体
     *
     * @param originalResponse
     * @param cryptoVo
     * @return
     */
    private ServerHttpResponseDecorator serverHttpResponseDecorator(ServerHttpResponse originalResponse, CryptoVo cryptoVo) {
        DataBufferFactory dataBufferFactory = originalResponse.bufferFactory();
        return new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                //encryptFlag 前端加密,响应体也加密
                if (body instanceof Flux && cryptoVo.getEncryptFlag() > 0) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.buffer().handle((dataBuffers, sink) -> {
                        DataBuffer join = dataBufferFactory.join(dataBuffers);
                        byte[] byteArray = new byte[join.readableByteCount()];
                        join.read(byteArray);
                        DataBufferUtils.release(join);
                        String originalResponseBody = new String(byteArray, StandardCharsets.UTF_8);
                        byte[] encryptedByteArray;
                        try {
                            String encryptResult = CryptoUtil.encrypt(originalResponseBody, cryptoConfigProperties.getAppkey());
                            //将响应体加密后 包装成json格式
                            JSONObject jsonObject = new JSONObject();
                            jsonObject.put(cryptoConfigProperties.getCryptoParam(), encryptResult);
                            encryptedByteArray = jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8);
                            cryptorDetail(null, originalResponseBody, encryptResult, CryptoUtil.RES_DATA);
                        } catch (Exception e) {
                            log.error("加密失败,{}", originalResponseBody, e);
                            sink.error(new RuntimeException(e.getMessage(), e));
                            return;
                        }
                        originalResponse.getHeaders().setContentLength(encryptedByteArray.length);
                        originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                        sink.next(dataBufferFactory.wrap(encryptedByteArray));
                    }));
                }
                return super.writeWith(body);
            }

            @Override
            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                return writeWith(Flux.from(body).flatMapSequential(p -> p));
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.putAll(originalResponse.getHeaders());
                return headers;
            }
        };
    }

    /**
     * 获取原始的请求参数
     *
     * @param dataBuffer 数据缓冲
     * @return 原始的请求参数
     */
    private String getOriginalRequestBody(DataBuffer dataBuffer) {
        byte[] bytes = new byte[dataBuffer.readableByteCount()];
        dataBuffer.read(bytes);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    /**
     * 对url的参数进行解密
     */
    private void updateUrlParam(ServerWebExchange exchange, CryptoVo encryptFlag) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String query = uri.getQuery();
        String param = "";
        if (!StringUtils.isEmpty(query) && query.contains(cryptoConfigProperties.getCryptoParam())) {
            encryptFlag.setEncryptFlag(1);
            try {
                String[] split = query.split("=");
                String dataSec = split[1];
                param = CryptoUtil.decrypt(dataSec, cryptoConfigProperties.getAppkey());
                Field targetQuery = uri.getClass().getDeclaredField("query");
                targetQuery.setAccessible(true);
                targetQuery.set(uri, param);
                cryptorDetail(request.getPath(), dataSec, param, CryptoUtil.REQ_URL);
            } catch (Exception e) {
                log.error("【url解密失败】路径:{},参数:{}", request.getPath(), param);
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }

    /**
     * 输出详细日志
     *
     * @param path
     * @param before
     * @param after
     * @param type   0:请求url 1请求body 2 响应
     */
    private void cryptorDetail(RequestPath path, String before, String after, Integer type) {
        if (!log.isDebugEnabled()) {
            return;
        }
        switch (type) {
            case CryptoUtil.REQ_URL:
                log.info("【请求url解密】路径:{},解密前:{},解密后:{}", path, before, after);
                break;
            case CryptoUtil.REQ_BODY:
                log.info("【请求body解密】路径:{},解密前:{},解密后:{}", path, before, after);
                break;
            case CryptoUtil.RES_DATA:
                log.info("【响应加密】加密前:{},加密后:{}", before, after);
                break;
            default:
                break;
        }
    }


    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342