gateway欺人太甚!

戏子无情 提交于 2020-08-15 18:09:09

需求:

通过gateway来转义请求参数中的html标签的“<”、“>”等,来防止xss攻击

版本: springbootVersion:2.1.6RELASE

springCloudGatewayVersion:2.1.2RELASE

因为gateway版本不同所以获取请求参数的方式也不同,这个版本的gateway是通过webflux来处理参数的,什么是webflux请点击查看,这里就不过多赘述了

对于gateway的过滤器有很多种实现方式,根据具体的业务需求来选定最便捷、最简单的处理方式

这里是我自定义gatewayfilter过滤器代码实现:

此代码只处理了两种contentType是:application/json和application/json;charset=UTF-8

对于contentType是multipart/form-data没有处理这是缺陷,如有好的处理方式请留言,大家一起学习一起进步

import lombok.extern.slf4j.Slf4j;
import org.owasp.esapi.ESAPI;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * @version 0.1.0
 * @author: Lzz
 * @Date: 2020-07-08 11:11
 * @Description: $ 防御XssApi对请求参数进行过滤
 * @return
 * @since 0.1.0
 **/
@Component
@Slf4j
public class RequestParametersFilter implements GlobalFilter, Ordered {
    /**
     * 正则表达式预编译处理
     */
    private static final Pattern SCRIPT_PATTERN = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
    private static final Pattern SRC_PATTERN = Pattern.compile("src[\r\n]*=[\r\n]*'(.*?)'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern NR_PATTERN = Pattern.compile("src[\r\n]*=[\r\n]*\"(.*?)\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern SCRIPT_ALL_PATTERN = Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern EVL_PATTERN = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern E­XPR_PATTERN = Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern JAVASCRIPT_PATTERN = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
    private static final Pattern VBSCRIPT_PATTERN = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
    private static final Pattern ONLOAD_PATTERN = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern ONXX_PATTERN = Pattern.compile("on.*(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
    private static final String POST = "POST";
    private static final String GET = "GET";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String method = serverHttpRequest.getMethodValue();
        log.info("请求路径 {},请求方式{}", serverHttpRequest.getURI().getPath(), method);
        if (POST.equals(method)) {
            if (MediaType.APPLICATION_JSON.isCompatibleWith(serverHttpRequest.getHeaders().getContentType()) || MediaType.APPLICATION_JSON_UTF8.isCompatibleWith(serverHttpRequest.getHeaders().getContentType())) {
                log.info("contentType {}", serverHttpRequest.getHeaders().getContentType());
                return DataBufferUtils.join(exchange.getRequest().getBody())
                        .flatMap(dataBuffer -> {
                            byte[] bytes = new byte[dataBuffer.readableByteCount()];
                            dataBuffer.read(bytes);
                            //读取到具体的请求参数,但是读取到的json是string字符串,如果想做特殊处理可以从这里入手
                            String bodyString = new String(bytes, StandardCharsets.UTF_8);
                            log.info("post 请求参数 {}", bodyString);
                            //对string字符串进行正则过滤以及转义
                            String convertBody = cleanXss(bodyString);
                            //将处理完的数据重新放回ServerWebExchange
                            exchange.getAttributes().put("POST_BODY", convertBody);
                            DataBufferUtils.release(dataBuffer);
                            //需要注意的是:这里的参数都是IO流来读取,只能读一次,所有需要再次重新构建request
                            //提示:如果对于请参数没有处理需求,可以开启gateway的请求参数缓存方式,cachedRequestBodyObject
                            //适用于springbootVersion2.1+ 具体详见:https://github.com/spring-cloud/spring-cloud-gateway/issues/747
                            Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                                DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                                return Mono.just(buffer);
                            });
                            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                                @Override
                                public Flux<DataBuffer> getBody() {
                                    return cachedFlux;
                                }
                            };
                            return chain.filter(exchange.mutate().request(mutatedRequest).build());
                        });
            }
            return chain.filter(exchange);
        } else if (GET.equals(method)) {
            //获取到get请求的map参数集合
            MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
            //将处理完成的参数重新放入request中
            Consumer<HttpHeaders> httpHeaders = httpHeader -> queryParams.forEach((k, v) -> {
                //因为业务需求,在传参时参数名称会传入[ ] 等特殊符号,不轮需求如何这里一定要进行URLEncoder编码
                httpHeader.set(URLEncoder.encode(k, StandardCharsets.UTF_8), cleanXss(String.valueOf(v)));
                log.info("get 请求参数name={},value={}", URLEncoder.encode(k, StandardCharsets.UTF_8), v);
            });
            ServerHttpRequest build = exchange.getRequest().mutate().headers(httpHeaders).build();
            return chain.filter(exchange.mutate().request(build).build());
        }
        return chain.filter(exchange);
    }

    /**
     * 数字越小越先执行
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }


    private String cleanXss(String value) {
        if (value != null) {
            // 推荐使用ESAPI库来避免脚本攻击
            value = ESAPI.encoder().canonicalize(value);
            // 避免空字符串
            value = value.replaceAll("", "");
            // 避免script 标签
            value = SCRIPT_PATTERN.matcher(value).replaceAll("");
            // 避免src形式的表达式
            value = SRC_PATTERN.matcher(value).replaceAll("");
            value = NR_PATTERN.matcher(value).replaceAll("");
            // 删除单个的<script ...> 标签
            value = SCRIPT_ALL_PATTERN.matcher(value).replaceAll("");
            // 避免 eval(...) 形式表达式
            value = EVL_PATTERN.matcher(value).replaceAll("");
            // 避免 e­xpression(...) 表达式
            value = E­XPR_PATTERN.matcher(value).replaceAll("");
            // 避免 javascript: 表达式
            value = JAVASCRIPT_PATTERN.matcher(value).replaceAll("");
            // 避免 vbscript: 表达式
            value = VBSCRIPT_PATTERN.matcher(value).replaceAll("");
            // 避免 onload= 表达式
            value = ONLOAD_PATTERN.matcher(value).replaceAll("");
            // 避免 onXX= 表达式
            value = ONXX_PATTERN.matcher(value).replaceAll("");
            //将特殊字符进行转义
            return replaceTag(value);
        }
        return "";
    }

    /**
     * 替换尖括号等特殊字符
     *
     * @param input
     * @return
     */
    public String replaceTag(String input) {
        if (!hasSpecialChars(input)) {
            return input;
        }
        StringBuffer strBuffer = new StringBuffer(input.length());
        char c;
        for (int i = 0; i <= input.length() - 1; i++) {
            c = input.charAt(i);
            switch (c) {
                case '<':
                    strBuffer.append("&lt;");
                    break;
                case '>':
                    strBuffer.append("&gt;");
                    break;
                case '"':
                    strBuffer.append("&quot;");
                    break;
                case '&':
                    strBuffer.append("&amp;");
                    break;
                default:
                    strBuffer.append(c);
            }
        }
        return (strBuffer.toString());
    }

    /**
     * 基本功能:判断标记是否存在
     * <p>
     *
     * @param input
     * @return boolean
     */
    public boolean hasSpecialChars(String input) {
        boolean flag = false;
        if ((input != null) && (input.length() > 0)) {
            char c;
            for (int i = 0; i <= input.length() - 1; i++) {
                c = input.charAt(i);
                switch (c) {
                    case '>':
                        flag = true;
                        break;
                    case '<':
                        flag = true;
                        break;
                    case '"':
                        flag = true;
                        break;
                    case '&':
                        flag = true;
                        break;
                    default:
                }
            }
        }
        return flag;
    }
}

这个地方就是我在代码中说的要加URLEncoder进行编码的原因,这个问题是已经通过gateway请求到了具体的服务上报的错,

通过度娘知道了大致意思是,由于Tomcat高版本太高不支持特殊字符,然后我通过一下方式完美解决了这个问题,

详见:https://stackoverflow.com/questions/61735692/the-http-header-line-group-name-xxx-or-migrationxxx-or-novation-does-not-con

这个是对RFC 7230的讲解:https://datatracker.ietf.org/doc/rfc7230/

java.lang.IllegalArgumentException: The HTTP header line [test[1]: [RE]] does not conform to RFC 7230 and has been ignored.

原因:tomcatVersion:9.0.21中get请求header中的请求参数不支持特殊字符串需要通过

解决办法如下

URLEncoder.encode(parameter, StandardCharsets.UTF_8)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!