SpringCloud Zuul网关处解决XSS跨站问题

1,922 阅读2分钟

文章来源 www.houxiurong.com

1.添加过滤器

/**
 * 拦截防止xss注入
 *
 * @author houxiurong
 * @date 2022-01-21
 */
@Slf4j
@Component
public class XssFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 3;
    }

    @Autowired
    private NoCheckTokenUriCfg noCheckTokenUriCfg;

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        // 注册和登录接口不拦截,其他接口都要拦截校验 token
        log.info("XssFilter.shouldFilter() 请求URL:" + request.getRequestURI());
        String requestUrl = request.getRequestURI();
        // 放行不校验token的请求
        for (String urlStr : noCheckTokenUriCfg.getUriArray()) {
            if (requestUrl.contains(urlStr)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        // 放行不校验文件上传请求
        if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {
            return null;
        }
        try (InputStream requestInputStream = request.getInputStream()) {
            //String toString = IOUtils.toString(requestInputStream, StandardCharsets.UTF_8);
            String requestBody = StreamUtils.copyToString(requestInputStream, StandardCharsets.UTF_8);
            // 空值不处理
            if (StringUtils.isEmpty(requestBody)) {
                return null;
            }
            Map<String, Object> beforeRequestBody = JSON.parseObject(requestBody);
            log.info("Xss filter beforeRequestBody={}", beforeRequestBody);
            for (Map.Entry<String, Object> entry : beforeRequestBody.entrySet()) {
                if (this.containsXssValue(entry.getValue().toString())) {
                    log.error("Xss filter containsXssValue包含有特殊xss字符,{}", entry.getValue().toString());
                    this.setXssResponse(requestContext, HttpStatus.BAD_REQUEST.value(), "XSS跨站安全检查不通过");
                }
            }
        } catch (Exception e) {
            log.error("zuul 过滤器读取参数XSS跨站危险检查异常", e);
        }
        return null;
    }

    /**
     * 特殊字符判断
     *
     * @param requestValue 参数
     * @return handled requestValue
     */
    private Boolean containsXssValue(String requestValue) {
        if (Pattern.matches("^.*(script).*$", requestValue)
                || Pattern.matches("^.*(eval).*$", requestValue)
                || Pattern.matches("^.*javascript.*$", requestValue)) {
            return true;
        }
        return false;
    }

    /**
     * 拒绝提交
     */
    private void setXssResponse(RequestContext requestContext, int code, String msg) {
        // 过滤该请求对其进行路由
        requestContext.setSendZuulResponse(false);
        //组装统一格式返回报文
        ObjectResult result = new ObjectResult();
        result.setStatusCode(code + "");
        result.setMsg(msg);

        // 返回错误代码
        requestContext.setResponseBody(JSONUtils.toJSONString(result));
        requestContext.getResponse().setContentType("application/json;charset=UTF-8");
    }
}

2.解决request.getInputStream()使用一次后请求为空问题

request.getInputStream()在使用过一次后,再次使用会为null,这个可以重新RequestWrapper来解决该问题。 下面添加 MyServletRequestWrapper

/**
 * 包装HttpServletRequest
 *
 * @author houxiurong
 * @date 2022-01-21
 */
public class MyServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    public MyServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = IOUtils.toByteArray(super.getInputStream());
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new RequestBodyCachingInputStream(body);
    }

    /**
     * 缓存RequestBodyCachingInputStream
     */
    private class RequestBodyCachingInputStream extends ServletInputStream {
        private byte[] body;
        private int lastIndexRetrieved = -1;
        private ReadListener listener;

        public RequestBodyCachingInputStream(byte[] body) {
            this.body = body;
        }

        @Override
        public int read() throws IOException {
            if (isFinished()) {
                return -1;
            }
            int i = body[lastIndexRetrieved + 1];
            lastIndexRetrieved++;
            if (isFinished() && listener != null) {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                    throw e;
                }
            }
            return i;
        }

        @Override
        public boolean isFinished() {
            return lastIndexRetrieved == body.length - 1;
        }

        @Override
        public boolean isReady() {
            return isFinished();
        }

        @Override
        public void setReadListener(ReadListener listener) {
            if (listener == null) {
                throw new IllegalArgumentException("listener cann not be null");
            }
            if (this.listener != null) {
                throw new IllegalArgumentException("listener has been set");
            }
            this.listener = listener;
            if (!isFinished()) {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                }
            } else {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                }
            }
        }

        @Override
        public int available() throws IOException {
            return body.length - lastIndexRetrieved - 1;
        }

        @Override
        public void close() throws IOException {
            lastIndexRetrieved = body.length - 1;
            body = null;
        }
    }
}

网关服务注入Filter过滤器,此处需要过滤掉文件上传接口。

/**
 * 替换Request对象被getInputStream读取一次的问题
 *
 * @author houxiurong
 * @date 2022-01-21
 */
@Component
public class RequestReplaceFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestUrl = request.getRequestURI();
        // 放行不校验文件上传请求
        if (!StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {
            if (!(request instanceof MyServletRequestWrapper)) {
                request = new MyServletRequestWrapper(request);
            }
        }
        filterChain.doFilter(request, response);
    }
}

最后启动网关服务,大功告成! ^_^为了解决 requestInputSteam这个读取一次失效问题,此处研究了半天。感谢踩过坑的同事提醒