美文网首页框架建设收集Spring Cloud
Spring Cloud Gateway网关XSS过滤

Spring Cloud Gateway网关XSS过滤

作者: 千年的心 | 来源:发表于2020-07-01 14:58 被阅读0次

    XSS是一种经常出现在web应用中的计算机安全漏洞,具体信息请自行Google。本文只分享在Spring Cloud Gateway中执行通用的XSS防范。首次作文,全是代码,若有遗漏不明之处,请各位看官原谅指点。

    使用版本

    Spring Cloud版本为 Greenwich.SR4
    Spring Boot版本为 2.1.11.RELEASE

    1.创建一个Filter。特别注意的是在处理完成之后需要重新构造请求,否则后续业务无法获得参数。

    import io.netty.buffer.ByteBufAllocator;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    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.core.io.buffer.NettyDataBufferFactory;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
    import org.springframework.stereotype.Component;
    import org.springframework.util.Assert;
    import org.springframework.util.DigestUtils;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import javax.validation.constraints.NotEmpty;
    import java.net.URI;
    import java.nio.charset.StandardCharsets;
    import java.util.List;
    import java.util.Optional;
    
    /**
     * XSS过滤
     *
     * @author lieber
     */
    @Component
    @Slf4j
    @ConfigurationProperties("config.xss")
    @Data
    public class XssFilter implements GlobalFilter, Ordered {
    
        private List<XssWhiteUrl> whiteUrls;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            URI uri = request.getURI();
            String method = request.getMethodValue();
            // 判断是否在白名单中
            if (this.white(uri.getPath(), method)) {
                return chain.filter(exchange);
            }
            // 只拦截POST和PUT请求
            if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {
                return DataBufferUtils.join(request.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(Optional.empty())
                        .flatMap(optional -> {
                            // 取出body中的参数
                            byte[] oldBytes = new byte[dataBuffer.readableByteCount()];
                            dataBuffer.read(oldBytes);
                            String bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                            log.debug("原请求参数为:{}", bodyString);
                            // 执行XSS清理
                            bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
                            log.debug("修改后参数为:{}", bodyString);
    
                            ServerHttpRequest newRequest = request.mutate().uri(uri).build();
    
                            // 重新构造body
                            byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
                            DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
                            Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
    
                            // 重新构造header
                            HttpHeaders headers = new HttpHeaders();
                            headers.putAll(httpHeaders);
                            // 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
                            int length = newBytes.length;
                            headers.remove(HttpHeaders.CONTENT_LENGTH);
                            headers.setContentLength(length);
                            headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
                            // 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法
                            newRequest = new ServerHttpRequestDecorator(newRequest) {
                                @Override
                                public Flux<DataBuffer> getBody() {
                                    return bodyFlux;
                                }
    
                                @Override
                                public HttpHeaders getHeaders() {
                                    return headers;
                                }
                            };
    
                            return chain.filter(exchange.mutate().request(newRequest).build());
                        });
            } else {
                return chain.filter(exchange);
            }
        }
    
        /**
         * 是否是白名单
         *
         * @param url    路由
         * @param method 请求方式
         * @return true/false
         */
        private boolean white(String url, String method) {
            return whiteUrls != null && whiteUrls.contains(XssWhiteUrl.builder().url(url).method(method).build());
        }
    
        /**
         * 字节数组转DataBuffer
         *
         * @param bytes 字节数组
         * @return DataBuffer
         */
        private DataBuffer toDataBuffer(byte[] bytes) {
            NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
            DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
            buffer.write(bytes);
            return buffer;
        }
    
        public static final int ORDER = 10;
    
        @Override
        public int getOrder() {
            return ORDER;
        }
    
        @Data
        @Validated
        @AllArgsConstructor
        @NoArgsConstructor
        private static class XssWhiteUrl {
    
            @NotEmpty
            private String url;
    
            @NotEmpty
            private String method;
        }
    }
    
    1. 处理XSS字符串。这里大范围采用Jsoup处理,然后根据自己的业务做了一部分定制。较为特殊的是,我们将字符串中含有'</'标示为这段文本是富文本。在清除xss攻击字符串方法时优化空间较大。
    import com.alibaba.fastjson.JSONObject;
    import org.jsoup.Jsoup;
    import org.jsoup.nodes.Document;
    import org.jsoup.safety.Whitelist;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * xss拦截工具类
     *
     * @author lieber
     */
    public enum XssUtil {
    
        /**
         * 实例
         */
        INSTANCE;
    
        private final static String RICH_TEXT = "</";
    
        /**
         * 自定义白名单
         */
        private final static Whitelist CUSTOM_WHITELIST = Whitelist.relaxed()
                .addAttributes("video", "width", "height", "controls", "alt", "src")
                .addAttributes(":all", "style", "class");
    
        /**
         * jsoup不格式化代码
         */
        private final static Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
    
        /**
         * 清除json对象中的xss攻击字符
         *
         * @param val json对象字符串
         * @return 清除后的json对象字符串
         */
        private String cleanObj(String val) {
            JSONObject jsonObject = JSONObject.parseObject(val);
            for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                if (entry.getValue() != null && entry.getValue() instanceof String) {
                    String str = (String) entry.getValue();
                    str = this.cleanXss(str);
                    entry.setValue(str);
                }
            }
            return jsonObject.toJSONString();
        }
    
        /**
         * 清除json数组中的xss攻击字符
         *
         * @param val json数组字符串
         * @return 清除后的json数组字符串
         */
        private String cleanArr(String val) {
            List<String> list = JSONObject.parseArray(val, String.class);
            List<String> result = new ArrayList<>(list.size());
            for (String str : list) {
                str = this.cleanXss(str);
                result.add(str);
            }
            return JSONObject.toJSONString(result);
        }
    
        /**
         * 清除xss攻击字符串,此处优化空间较大
         *
         * @param str 字符串
         * @return 清除后无害的字符串
         */
        public String cleanXss(String str) {
            if (JsonUtil.INSTANCE.isJsonObj(str)) {
                str = this.cleanObj(str);
            } else if (JsonUtil.INSTANCE.isJsonArr(str)) {
                str = this.cleanArr(str);
            } else {
                boolean richText = this.richText(str);
                if (!richText) {
                    str = str.trim();
                    str = str.replaceAll(" +", " ");
                }
                String afterClean = Jsoup.clean(str, "", CUSTOM_WHITELIST, OUTPUT_SETTINGS);
                if (paramError(richText, afterClean, str)) {
                    throw new BizRunTimeException(ApiCode.PARAM_ERROR, "参数包含特殊字符");
                }
                str = richText ? afterClean : this.backSpecialStr(afterClean);
            }
            return str;
        }
    
    
        /**
         * 判断是否是富文本
         *
         * @param str 待判断字符串
         * @return true/false
         */
        private boolean richText(String str) {
            return str.contains(RICH_TEXT);
        }
    
        /**
         * 判断是否参数错误
         *
         * @param richText   是否富文本
         * @param afterClean 清理后字符
         * @param str        原字符串
         * @return true/false
         */
        private boolean paramError(boolean richText, String afterClean, String str) {
            // 如果包含富文本字符,那么不是参数错误
            if (richText) {
                return false;
            }
            // 如果清理后的字符和清理前的字符匹配,那么不是参数错误
            if (Objects.equals(str, afterClean)) {
                return false;
            }
            // 如果仅仅包含可以通过的特殊字符,那么不是参数错误
            if (Objects.equals(str, this.backSpecialStr(afterClean))) {
                return false;
            }
            // 如果还有......
            return true;
        }
    
        /**
         * 转义回特殊字符
         *
         * @param str 已经通过转义字符
         * @return 转义后特殊字符
         */
        private String backSpecialStr(String str) {
            return str.replaceAll("&amp;", "&");
        }
    }
    

    3.其它使用到的工具

    
    import com.alibaba.fastjson.JSONObject;
    import org.springframework.util.StringUtils;
    
    /**
     * JSON处理工具类
     *
     * @author lieber
     */
    public enum JsonUtil {
    
        /**
         * 实例
         */
        INSTANCE;
    
        /**
         * json对象字符串开始标记
         */
        private final static String JSON_OBJECT_START = "{";
    
        /**
         * json对象字符串结束标记
         */
        private final static String JSON_OBJECT_END = "}";
    
        /**
         * json数组字符串开始标记
         */
        private final static String JSON_ARRAY_START = "[";
    
        /**
         * json数组字符串结束标记
         */
        private final static String JSON_ARRAY_END = "]";
    
    
        /**
         * 判断字符串是否json对象字符串
         *
         * @param val 字符串
         * @return true/false
         */
        public boolean isJsonObj(String val) {
            if (StringUtils.isEmpty(val)) {
                return false;
            }
            val = val.trim();
            if (val.startsWith(JSON_OBJECT_START) && val.endsWith(JSON_OBJECT_END)) {
                try {
                    JSONObject.parseObject(val);
                    return true;
                } catch (Exception e) {
                    return false;
                }
            }
            return false;
        }
    
        /**
         * 判断字符串是否json数组字符串
         *
         * @param val 字符串
         * @return true/false
         */
        public boolean isJsonArr(String val) {
            if (StringUtils.isEmpty(val)) {
                return false;
            }
            val = val.trim();
            if (StringUtils.isEmpty(val)) {
                return false;
            }
            val = val.trim();
            if (val.startsWith(JSON_ARRAY_START) && val.endsWith(JSON_ARRAY_END)) {
                try {
                    JSONObject.parseArray(val);
                    return true;
                } catch (Exception e) {
                    return false;
                }
            }
            return false;
        }
    
        /**
         * 判断对象是否是json对象
         *
         * @param obj 待判断对象
         * @return true/false
         */
        public boolean isJsonObj(Object obj) {
            String str = JSONObject.toJSONString(obj);
            return this.isJsonObj(str);
        }
    
        /**
         * 判断字符串是否json字符串
         *
         * @param str 字符串
         * @return true/false
         */
        public boolean isJson(String str) {
            if (StringUtils.isEmpty(str)) {
                return false;
            }
            return this.isJsonObj(str) || this.isJsonArr(str);
        }
    }
    

    大功告成。

    相关文章

      网友评论

        本文标题:Spring Cloud Gateway网关XSS过滤

        本文链接:https://www.haomeiwen.com/subject/egvzqhtx.html