• SpringCloud Gateway API接口加解密


    接口范围

    所有GET请求 白名单除外

    body 体 是 application_json 和 application_json_utf8 的 POST请求 白名单除外

    POST url传参也支持 白名单除外

    启用禁用/版本

    后端提供独立接口(或者现有接口)查询是否需要启用加密功能(如果后端启用了,前端请求被拦截修改为为启用,接口也无法访问回报解密错误),此接口明文传输

    请求头增加一个加密版本字段,标识当前的加密算法版本:crypto-version: 1.0.0

    加密算法

    考虑到全局加密,使用AES加密方式性能更高

    加密字符串:原始数据 > AES加密后的字节数组 > Base64编码处理

    解密字符串:Base64密文 > AES密文 -> 原始字符串

    AES加密细节

    aesKey:32/16 位由后端同一生成

    iv:aesKey

    mode:CBC

    padding:pkcs7

    js例子

    
    //加密
    static encryptAES(data, key) {
      const dataBytes = CryptoJS.enc.Utf8.parse(data);
      const keyBytes = CryptoJS.enc.Utf8.parse(key);
      const encrypted = CryptoJS.AES.encrypt(dataBytes, keyBytes, {
        iv: keyBytes,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      });
      return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
    }
    复制代码

    报文格式

    GET

    url:/app/xx/xx?xx=1

    加密处理

    秘钥:xxxxxxxxxxxxxxxx

    加密文本:{"xx":1}

    密文:xq4YR89LgUs4V5N5juKgW5hIsiOsCxBOwzX632S8NV4=

    加密后的请求

    /app/xx/xx?data=xq4YR89LgUs4V5N5juKgW5hIsiOsCxBOwzX632S8NV4=

    POST

    url:/app/xx/xx/xxx

    json body:

    {"xxx1":"111","xxx2":"huawei","xxx3":"789","xxx4":101,"xxx5":2}

    加密处理

    秘钥:xxxxxxxxxxxxxxxx

    加密文本

    {"xxx1":"111","xxx2":"huawei","xxx3":"789","xxx4":101,"xxx5":2}

    密文:1oUTYvWfyaeTJ5/wJTVBqUv0Dz0IAUQTZtxSKY9WLZZl8pILP2Sozk5yOYg9I1WTvzgbbGRDGcWV1ASpYykyS1Fq5cT8s3aLXQ6NMo0AaMOC9L0aVpR863qWso5O8aG3

    加密后的请求*

    json body:

    {

    "data": "1oUTYvWfyaeTJ5/wJTVBqUv0Dz0IAUQTZtxSKY9WLZZl8pILP2Sozk5yOYg9I1WTvzgbbGRDGcWV1ASpYykyS1Fq5cT8s3aLXQ6NMo0AaMOjt4G9dK0WwhMGZofYuBKmdF27R8Qkr3VtZvjadtvBazJurITyE7hFcr43nlHSL5E="

    }

    POST url传参 和GET格式一致

    网关实现细节代码

    基于GlobalFilter 接口包装请求request和响应response,先列出关键代码,完整代码见文末

    filter过滤器请求配置和请求方式分发

     @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            if (!cryptoProperties.isEnabled()) {
                return chain.filter(exchange);
            }
            ServerHttpRequest request = exchange.getRequest();
            //校验请求路径跳过加密
            String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange);
            String path = exchange.getRequest().getURI().getPath();
            if (isSkip(path) || isSkip(originalRequestUrl)) {
                return chain.filter(exchange);
            }
    
            HttpHeaders headers = request.getHeaders();
            MediaType contentType = headers.getContentType();
    
            //后期算法升级扩展,暂时只判断是否相等
            if (!cryptoProperties.getCryptoVersion().equals(headers.getFirst(cryptoProperties.getCryptoVersionHeader()))) {
                return Mono.error(new CryptoException("加密版本不支持"));
            }
    
            if (request.getMethod() == HttpMethod.GET) {
                return this.handleGetReq(exchange, chain);
            } else if (request.getMethod() == HttpMethod.POST &&
                    (contentType == null ||
                            MediaType.APPLICATION_JSON.equals(contentType) ||
                            MediaType.APPLICATION_JSON_UTF8.equals(contentType))) {
                return this.handlePostReq(exchange, chain);
            } else {
                return chain.filter(exchange);
            }
        }
    复制代码

    Get请求参数解密包装 ServerHttpRequestDecorator

    //构造查询参数Map
            MultiValueMap<String, String> map = buildMultiValueMap(dataJson);
            //新的解密后的uri
            ServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map);
            //新的解密后的uri request
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) {
                @Override
                public MultiValueMap<String, String> getQueryParams() {
                    return map;
                }
    
            };
    复制代码

    post请求参数解密包装 ServerHttpRequestDecorator

    //构造一个请求包装
            final MultiValueMap<String, String> finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap);
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {
    
                @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;
                }
    
                //处理post url传参解密
                @Override
                public MultiValueMap<String, String> getQueryParams() {
                    if (queryParamsDecrypt) {
                        return finalQueryParamMap;
                    }
                    return super.getQueryParams();
                }
    
                @Override
                public Flux<DataBuffer> getBody() {
                    //注意: 这里需要buffer一下,拿到完整报文后再map解密
                    return super.getBody().buffer().map(buffer -> {
                        DataBuffer joinDataBuffer = dataBufferFactory.join(buffer);
                        byte[] content = new byte[joinDataBuffer.readableByteCount()];
                        joinDataBuffer.read(content);
                        DataBufferUtils.release(joinDataBuffer);
                        String decryptData = new String(content, StandardCharsets.UTF_8);
                        log.info("post decryptData: {}", decryptData);
                        if (!queryParamsDecrypt && StringUtils.isEmpty(decryptData)) {
                            throw new CryptoException("参数格式错误");
                        } else {
                            JSONObject dataJsonObj = JSON.parseObject(decryptData);
                            if (!queryParamsDecrypt && !dataJsonObj.containsKey(cryptoProperties.getParamName())) {
                                throw new CryptoException("参数格式错误");
                            }
                            byte[] bytes = AesUtil.decryptFormBase64(dataJsonObj.getString(cryptoProperties.getParamName()), cryptoProperties.getAesKey());
                            return dataBufferFactory.wrap(Objects.requireNonNull(bytes));
                        }
                    });
    复制代码

    GET/POST返回值加密处理CryptoServerHttpResponseDecorator

    class CryptoServerHttpResponseDecorator extends ServerHttpResponseDecorator {
            final DataBufferFactory bufferFactory;
            boolean isPass = false;
            public CryptoServerHttpResponseDecorator(ServerHttpResponse delegate) {
                super(delegate);
                bufferFactory = delegate.bufferFactory();
            }
    
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = super.getHeaders();
                //同一个请求此处有可能调用多次,先重置为false
                isPass = false;
                if (headers.getContentType() != null &&
                        !MediaType.APPLICATION_JSON.equals(headers.getContentType()) &&
                        !MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) {
                    //相应体ContentType只处理json
                    isPass = true;
                } else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) {
                    //添加version响应头
                    headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion());
                }
                return headers;
            }
    
            //调用 writeWith 和 writeAndFlushWith 判断: NettyWriteResponseFilter
    
            // application/json;charset=UTF-8 走这里
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux && !isPass) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
                        DataBuffer joinDataBuffer = bufferFactory.join(dataBuffer);
                        byte[] content = new byte[joinDataBuffer.readableByteCount()];
                        joinDataBuffer.read(content);
                        DataBufferUtils.release(joinDataBuffer);
                        Map<String, String> data = new HashMap<>(1);
                        data.put(cryptoProperties.getParamName(), AesUtil.encryptToBase64(content, cryptoProperties.getAesKey()));
                        return bufferFactory.wrap(JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8));
                    }));
                }
                return super.writeWith(body);
            }
    
            // StreamingMediaType类型:application/stream 和 application/stream+json 走这里
            @Override
            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                return super.writeAndFlushWith(body);
            }
        }
    复制代码

    完整CryptoFilter实现

    package org.xx.xx.gateway.filter;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.google.common.collect.Lists;
    import io.netty.buffer.ByteBufAllocator;
    import lombok.RequiredArgsConstructor;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    import org.reactivestreams.Publisher;
    import org.xx.xx.gateway.props.CryptoProperties;
    import org.xx.xx.gateway.provider.RequestProvider;
    import org.xx.xx.gateway.provider.ResponseProvider;
    import org.xx.xx.gateway.util.AesUtil;
    import org.xx.xx.gateway.util.StringPool;
    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.NettyDataBufferFactory;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    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.util.AntPathMatcher;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.util.StringUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import java.net.URI;
    import java.net.URISyntaxException;
    import java.nio.charset.StandardCharsets;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * CryptoFilter
     *
     * @author lizheng 
     * @version 1.0
     * @date 2022/3/11 上午10:57
     */
    @Slf4j
    @RequiredArgsConstructor
    @Configuration
    @ConditionalOnProperty(value = "gateway.crypto.enabled", havingValue = "true", matchIfMissing = true)
    public class CryptoFilter implements GlobalFilter, Ordered {
    
        private final AntPathMatcher antPathMatcher = new AntPathMatcher();
        private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
    
        private final CryptoProperties cryptoProperties;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            if (!cryptoProperties.isEnabled()) {
                return chain.filter(exchange);
            }
            ServerHttpRequest request = exchange.getRequest();
            //校验请求路径跳过加密
            String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange);
            String path = exchange.getRequest().getURI().getPath();
            if (isSkip(path) || isSkip(originalRequestUrl)) {
                return chain.filter(exchange);
            }
    
            HttpHeaders headers = request.getHeaders();
            MediaType contentType = headers.getContentType();
    
            //后期算法升级扩展,暂时只判断是否相等
            if (!cryptoProperties.getCryptoVersion().equals(headers.getFirst(cryptoProperties.getCryptoVersionHeader()))) {
                return Mono.error(new CryptoException("加密版本不支持"));
            }
    
            if (request.getMethod() == HttpMethod.GET) {
                return this.handleGetReq(exchange, chain);
            } else if (request.getMethod() == HttpMethod.POST &&
                    (contentType == null ||
                            MediaType.APPLICATION_JSON.equals(contentType) ||
                            MediaType.APPLICATION_JSON_UTF8.equals(contentType))) {
                return this.handlePostReq(exchange, chain);
            } else {
                return chain.filter(exchange);
            }
        }
    
        class CryptoServerHttpResponseDecorator extends ServerHttpResponseDecorator {
            final DataBufferFactory bufferFactory;
            boolean isPass = false;
            public CryptoServerHttpResponseDecorator(ServerHttpResponse delegate) {
                super(delegate);
                bufferFactory = delegate.bufferFactory();
            }
    
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = super.getHeaders();
                //同一个请求此处有可能调用多次,先重置为false
                isPass = false;
                if (headers.getContentType() != null &&
                        !MediaType.APPLICATION_JSON.equals(headers.getContentType()) &&
                        !MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) {
                    //相应体ContentType只处理json
                    isPass = true;
                } else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) {
                    //添加version响应头
                    headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion());
                }
                return headers;
            }
    
            //调用 writeWith 和 writeAndFlushWith 判断: NettyWriteResponseFilter
    
            // application/json;charset=UTF-8 走这里
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux && !isPass) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
                        DataBuffer joinDataBuffer = bufferFactory.join(dataBuffer);
                        byte[] content = new byte[joinDataBuffer.readableByteCount()];
                        joinDataBuffer.read(content);
                        DataBufferUtils.release(joinDataBuffer);
                        Map<String, String> data = new HashMap<>(1);
                        data.put(cryptoProperties.getParamName(), AesUtil.encryptToBase64(content, cryptoProperties.getAesKey()));
                        return bufferFactory.wrap(JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8));
                    }));
                }
                return super.writeWith(body);
            }
    
            // StreamingMediaType类型:application/stream 和 application/stream+json 走这里
            @Override
            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                return super.writeAndFlushWith(body);
            }
        }
    
        @SneakyThrows
        private Mono<Void> handlePostReq(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            String paramData = request.getQueryParams().getFirst(cryptoProperties.getParamName());
            MultiValueMap<String, String> queryParamMap = new LinkedMultiValueMap<>();
            final boolean queryParamsDecrypt = !StringUtils.isEmpty(paramData);
            if (queryParamsDecrypt) {
                String dataJson;
                try {
                    //AES解密
                    dataJson = AesUtil.decryptFormBase64ToString(paramData, cryptoProperties.getAesKey());
                } catch (Exception e) {
                    log.error("请求参数解密异常: ", e);
                    return cryptoError(exchange.getResponse(), "请求参数解密异常");
                }
                //构造查询参数Map
                queryParamMap = buildMultiValueMap(dataJson);
                //新的解密后的uri request
                request = this.buildNewServerHttpRequest(request, queryParamMap);
            }
    
            //构造一个请求包装
            final MultiValueMap<String, String> finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap);
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {
    
                @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 MultiValueMap<String, String> getQueryParams() {
                    if (queryParamsDecrypt) {
                        return finalQueryParamMap;
                    }
                    return super.getQueryParams();
                }
    
                @Override
                public Flux<DataBuffer> getBody() {
                    //注意: 这里需要buffer,拿到完整报文后再map解密
                    return super.getBody().buffer().map(buffer -> {
                        DataBuffer joinDataBuffer = dataBufferFactory.join(buffer);
                        byte[] content = new byte[joinDataBuffer.readableByteCount()];
                        joinDataBuffer.read(content);
                        DataBufferUtils.release(joinDataBuffer);
                        String decryptData = new String(content, StandardCharsets.UTF_8);
                        log.info("post decryptData: {}", decryptData);
                        if (!queryParamsDecrypt && StringUtils.isEmpty(decryptData)) {
                            throw new CryptoException("参数格式错误");
                        } else {
                            JSONObject dataJsonObj = JSON.parseObject(decryptData);
                            if (!queryParamsDecrypt && !dataJsonObj.containsKey(cryptoProperties.getParamName())) {
                                throw new CryptoException("参数格式错误");
                            }
                            byte[] bytes = AesUtil.decryptFormBase64(dataJsonObj.getString(cryptoProperties.getParamName()), cryptoProperties.getAesKey());
                            return dataBufferFactory.wrap(Objects.requireNonNull(bytes));
                        }
                    });
                }
            };
            return chain.filter(exchange.mutate()
                    .request(decorator)
                    .response(new CryptoServerHttpResponseDecorator(exchange.getResponse()))
                    .build());
        }
    
        @SneakyThrows
        private Mono<Void> handleGetReq(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            if (request.getQueryParams().isEmpty()) {
                // get无参数 不走参数解密
                return chain.filter(exchange.mutate()
                        .request(request)
                        .response(new CryptoServerHttpResponseDecorator(exchange.getResponse()))
                        .build());
            }
            String paramData = request.getQueryParams().getFirst(cryptoProperties.getParamName());
    
            if (StringUtils.isEmpty(paramData)) {
                //有参数但是密文字段不存在
                throw new CryptoException("参数格式错误");
            }
    
            String dataJson;
            try {
                //AES解密
                dataJson = AesUtil.decryptFormBase64ToString(paramData, cryptoProperties.getAesKey());
            } catch (Exception e) {
                log.error("请求参数解密异常: ", e);
                return cryptoError(exchange.getResponse(), "请求参数解密异常");
            }
            //构造查询参数Map
            MultiValueMap<String, String> map = buildMultiValueMap(dataJson);
            //新的解密后的uri
            ServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map);
            //新的解密后的uri request
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) {
                @Override
                public MultiValueMap<String, String> getQueryParams() {
                    return map;
                }
    
            };
            return chain.filter(exchange.mutate()
                    .request(decorator)
                    .response(new CryptoServerHttpResponseDecorator(exchange.getResponse()))
                    .build());
        }
    
        private MultiValueMap<String, String> buildMultiValueMap(String dataJson) {
            JSONObject jsonObject = JSON.parseObject(dataJson);
            MultiValueMap<String, String> map = new LinkedMultiValueMap<>(jsonObject.size());
            for (String key : jsonObject.keySet()) {
                map.put(key, Lists.newArrayList(jsonObject.getString(key)));
            }
            return map;
        }
    
        private ServerHttpRequest buildNewServerHttpRequest(ServerHttpRequest request, MultiValueMap<String, String> params) throws URISyntaxException {
            StringBuilder queryBuilder = new StringBuilder();
            for (String key : params.keySet()) {
                queryBuilder.append(key);
                queryBuilder.append(StringPool.EQUALS);
                queryBuilder.append(params.getFirst(key));
                queryBuilder.append(StringPool.AMPERSAND);
            }
            queryBuilder.deleteCharAt(queryBuilder.length() - 1);
    
            //经过测试只覆盖 ServerHttpRequest的getQueryParams路由分发之后,无法携带过去新的参数,所以这里需要构造一个新的解密后的uri
            URI uri = request.getURI();
            URI newUri = new URI(uri.getScheme(),
                    uri.getUserInfo(),
                    uri.getHost(),
                    uri.getPort(),
                    uri.getPath(),
                    queryBuilder.toString(),
                    uri.getFragment());
    
            //构造一个新的ServerHttpRequest
            return request.mutate().uri(newUri).build();
        }
    
        private boolean isSkip(String path) {
            for (String pattern : cryptoProperties.getSkipPathPattern()) {
                if (antPathMatcher.match(pattern, path)) {
                    return true;
                }
            }
           return false;
        }
    
        private Mono<Void> cryptoError(ServerHttpResponse resp, String msg) {
            resp.setStatusCode(HttpStatus.UNAUTHORIZED);
            resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            String result = JSON.toJSONString(ResponseProvider.unAuth(msg));
            DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
            return resp.writeWith(Flux.just(buffer));
        }
    
        @Override
        public int getOrder() {
            return -200;
        }
    }
    复制代码
    来源;https://juejin.cn/post/7108878027458609188
  • 相关阅读:
    Android_bug之 task ':app:mergeDebugResources'. > Some file crunching failed, see logs f
    linux下vi命令大全[转]
    百度地图api 常用demo
    Mac之vim普通命令使用[转]
    java写文件
    java读取文件
    Android中对Log日志文件的分析[转]
    Android实用代码模块集锦
    java 位运算
    MyEclipse自带maven找不到或自己外置安装
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16756313.html
Copyright © 2020-2023  润新知