我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Spring Cloud Gateway网关XSS过滤方式

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Spring Cloud Gateway网关XSS过滤方式

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;

@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(dataBuffer -> {
                        // 取出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(request.getHeaders());
                        // 由于修改了传递参数,需要重新设置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);
        }
    }
    
    private boolean white(String url, String method) {
        return whiteUrls != null && whiteUrls.contains(XssWhiteUrl.builder().url(url).method(method).build());
    }
    
    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;
    }
}

2. 处理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;

public enum XssUtil {
    
    INSTANCE;
    private final static String RICH_TEXT = "</";
    
    private final static Whitelist CUSTOM_WHITELIST = Whitelist.relaxed()
            .addAttributes("video", "width", "height", "controls", "alt", "class="lazy" data-src")
            .addAttributes(":all", "style", "class");
    
    private final static Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
    
    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();
    }
    
    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);
    }
    
    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;
    }
    
    private boolean richText(String str) {
        return str.contains(RICH_TEXT);
    }
    
    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;
    }
    
    private String backSpecialStr(String str) {
        return str.replaceAll("&amp;", "&");
    }
}

3.其它使用到的工具


import com.alibaba.fastjson.JSONObject;
import org.springframework.util.StringUtils;

public enum JsonUtil {
    
    INSTANCE;
    
    private final static String JSON_OBJECT_START = "{";
    
    private final static String JSON_OBJECT_END = "}";
    
    private final static String JSON_ARRAY_START = "[";
    
    private final static String JSON_ARRAY_END = "]";
    
    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;
    }
    
    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;
    }
    
    public boolean isJsonObj(Object obj) {
        String str = JSONObject.toJSONString(obj);
        return this.isJsonObj(str);
    }
    
    public boolean isJson(String str) {
        if (StringUtils.isEmpty(str)) {
            return false;
        }
        return this.isJsonObj(str) || this.isJsonArr(str);
    }
}

大功告成。

----------------手动分隔----------------

修改

感谢@chang_p_x的指正,在第一步创建Filter时有问题,原因是使用了新旧代码的问题,现已经将元代码放在正文,新代码如下


@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);
        }
        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中的参数
                        String bodyString = "";
                        if (optional.isPresent()) {
                            byte[] oldBytes = new byte[optional.get().readableByteCount()];
                            optional.get().read(oldBytes);
                            bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                        }
                        HttpHeaders httpHeaders = request.getHeaders();
                        // 执行XSS清理
                        log.debug("{} - [{}:{}] XSS处理前参数:{}", method, uri.getPath(), bodyString);
                        bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
                        log.info("{} - [{}:{}] 参数:{}", method, uri.getPath(), 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);
        }
    }

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Spring Cloud Gateway网关XSS过滤方式

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Spring cloud网关gateway进行websocket路由转发规则配置过程

这篇文章主要介绍了Spring cloud网关gateway进行websocket路由转发规则配置过程,文中还通过实例代码介绍了Spring Cloud Gateway--配置路由的方法,需要的朋友可以参考下
2023-05-14

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录