• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Ruo-Yi 前后端分离防止XSS攻击和自定义可以重复读取InputStream流

武飞扬头像
会飞的皮卡丘EI
帮助1

Ruo-Yi 前后端分离防止XSS攻击和自定义可以重复读取InputStream流

防止XSS攻击分析

1、什么是xss攻击

**XSS 即(Cross Site Scripting)中文名称为:跨站脚本攻击。**XSS的重点不在于跨站点,而在于脚本的执行。那么XSS的原理是:

恶意攻击者在web页面中会插入一些恶意的script代码。当用户浏览该页面的时候,那么嵌入到web页面中script代码会执行,因此会达到恶意攻击用户的目的。

那么XSS攻击最主要有如下分类:反射型、存储型、及 DOM-based型。 反射性和DOM-baseed型可以归类为非持久性XSS攻击。存储型可以归类为持久性XSS攻击。

2、Ruo-Yi 实现的步骤以及代码分析

1、配置文件
xss: 
  # 过滤开关
  enabled: true
  # 排除链接(多个用逗号分隔)
  excludes: /system/notice
  # 匹配链接
  urlPatterns: /system/*,/monitor/*,/tool/*
# 是否开启xss
- enabled:true开启;false不开启
# excludes
	xss过滤器不经过的请求路径
# urlPatterns
	xss过滤器经过的请求路径
2、自定义xss攻击的过滤器
package com.ruoyi.common.filter;

import com.ruoyi.common.enums.HttpMethod;
import com.ruoyi.common.utils.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 防止XSS攻击的过滤器
 *      防止 xss 攻击
 *      如果请求是 GET、DELETE、或者是在 excludes集合里面,直接放行,不经过 XssHttpServletRequestWrapper
 * 
 * @author ruoyi
 */
public class XssFilter implements Filter
{
    /**
     * 排除链接
     *      this.init()方法会将需要排序的路径写入这个 List 集合
     */
    public List<String> excludes = new ArrayList<>();

    /**
     * 最先走这个 init() 方法,然后才会走 doFilter() 方法
     * @param filterConfig 这个是我们若依定义的类
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {
        String tempExcludes = filterConfig.getInitParameter("excludes");
        if (StringUtils.isNotEmpty(tempExcludes))
        {
            String[] url = tempExcludes.split(",");
            for (int i = 0; url != null && i < url.length; i  )
            {
                excludes.add(url[i]);
            }
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {
        HttpServletRequest req = (HttpServletRequest) request; // 获取请求对象
        HttpServletResponse resp = (HttpServletResponse) response; // 获取响应对象
        if (handleExcludeURL(req, resp))
        {
            // 如果请求是 GET、DELETE、或者是在 excludes集合里面,直接放行
            chain.doFilter(request, response);
            return;
        }
        // 自定义 XssHttpServletRequestWrapper 类 ,里面进行 防止 xss 攻击
        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(xssRequest, response);
    }

    // 如果请求是 GET、DELETE、或者是在 excludes集合里面,返回 true
    // 其他返回 false
    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response)
    {
        String url = request.getServletPath();
        String method = request.getMethod(); // 请求的方式:GET
        // GET DELETE 不过滤
        if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method))
        {
            return true;
        }
        return StringUtils.matches(url, excludes);
    }

    @Override
    public void destroy()
    {

    }
}
学新通
package com.ruoyi.common.filter;

import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.html.EscapeUtil;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;

/**
 * XSS过滤处理
 * 
 * @author ruoyi
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper
{
    /**
     * @param request
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request)
    {
        super(request);
    }

    @Override
    public String[] getParameterValues(String name)
    {
        // 获得有 name 指定的参数的所有值
        String[] values = super.getParameterValues(name);
        if (values != null)
        {
            // 获取到的 values 不为空
            int length = values.length;
            String[] escapseValues = new String[length];
            for (int i = 0; i < length; i  )
            {
                // 防xss攻击和过滤前后空格 具体解决 xss 攻击的其实是 EscapeUtil 工具类
                // 防xss攻击和过滤前后空格
                escapseValues[i] = EscapeUtil.clean(values[i]).trim(); // EscapeUtil.clean:清除所有HTML标签,但是不删除标签内的内容
            }
            return escapseValues;
        }
        // 调用系统里面的 getParameterValues
        return super.getParameterValues(name);
    }

    /**
     * 有可能提交的是图片形式的
     */
    @Override
    public ServletInputStream getInputStream() throws IOException
    {
        // 非json类型,直接返回
        if (!isJsonRequest())
        {
            return super.getInputStream();
        }

        // 为空,直接返回
        String json = IOUtils.toString(super.getInputStream(), "utf-8");
        if (StringUtils.isEmpty(json))
        {
            return super.getInputStream();
        }

        // xss过滤
        json = EscapeUtil.clean(json).trim();
        byte[] jsonBytes = json.getBytes("utf-8");
        final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
        return new ServletInputStream()
        {
            @Override
            public boolean isFinished()
            {
                return true;
            }

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

            @Override
            public int available() throws IOException
            {
                return jsonBytes.length;
            }

            @Override
            public void setReadListener(ReadListener readListener)
            {
            }

            @Override
            public int read() throws IOException
            {
                return bis.read();
            }
        };
    }

    /**
     * 是否是Json请求
     * 
     * @param request
     */
    public boolean isJsonRequest()
    {
        // CONTENT_TYPE = "Content-Type";
        String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
        // APPLICATION_JSON_VALUE = "application/json";
        // 不分大小写检查字符序列是否以指定的前缀开始【application/json】
        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
    }
}
学新通
3、将xss过滤器配置到系统中
package com.ruoyi.framework.config;

import com.ruoyi.common.filter.RepeatableFilter;
import com.ruoyi.common.filter.XssFilter;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.DispatcherType;
import java.util.HashMap;
import java.util.Map;

/**
 * Filter配置
 *      为了给 项目添加一些 自定义过滤器,也可以设置 filter 的排序值
 *
 *      Spring 提供了FilterRegistrationBean类,此类提供setOrder方法,可以为filter设置排序值,让spring在注册web filter之前排序后再依次注册。
 * @author ruoyi
 */
@Configuration
public class FilterConfig
{
    //   # 排除链接(多个用逗号分隔) excludes: /system/notice
    @Value("${xss.excludes}")
    private String excludes;

    //   # 匹配链接  urlPatterns: /system/*,/monitor/*,/tool/*
    @Value("${xss.urlPatterns}")
    private String urlPatterns;

    // 添加 XssFilter 过滤器
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    /**
     * 如果 application.yml 文件中的 xss.enabled 值和 havingValue 值相同的话 就返回 true,即下面配置类生效
     * 不相等的话 下面的类就不生效,即 spring 池里面没有 FilterRegistrationBean 对象
     */
    @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
    public FilterRegistrationBean xssFilterRegistration()
    {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setFilter(new XssFilter());
        // 添加需要 XssFilter 过滤的路径
        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
        // 设置过滤器名称
        registration.setName("xssFilter");
        // 设置 添加过滤器的 级别,越低越先执行
        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
        Map<String, String> initParameters = new HashMap<String, String>();
        // 添加不需要 XssFilter 拦截的路径,但是这些路径其实还是会先走  XssFilter 过滤器,只是在这个 过滤器里面会放行掉
        initParameters.put("excludes", excludes);
        registration.setInitParameters(initParameters);
        return registration;
    }

    // 添加 RepeatableFilter 过滤器
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean someFilterRegistration()
    {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new RepeatableFilter());
        // 添加 所有路径都需要走 这个过滤器
        registration.addUrlPatterns("/*");
        registration.setName("repeatableFilter");
        // 最低级别 最后执行的 过滤器
        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
        return registration;
    }

}
学新通

可以给系统添加过滤器,也可以设置过滤器的执行顺序

4、总结

1、在若依基础上开发的话,如果我们不想使用xss攻击过滤器的话,只需要将 application.yml文件里面,改为false

# 防止XSS攻击
xss: 
  # 过滤开关
  enabled: false

2、如果只想某些接口不进行xss攻击过滤器的话,只需要在excludes里面加上想要路径即可:

# 防止XSS攻击
xss: 
  # 过滤开关
  enabled: true
  # 排除链接(多个用逗号分隔)
  excludes: /system/notice
5、拓展:使用mica-xss实现防止xss攻击

参考:https://blog.csdn.net/HDesigner/article/details/121246322?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-4-121246322-blog-126586638.t5_landing_title_tags_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-4-121246322-blog-126586638.t5_landing_title_tags_v3&utm_relevant_index=5

自定义可以重复读取InputStream流

1、问题

系统为了提高项目安全性,拦截非法访问,要给项目增加了一个过滤器,拦截所有的请求,校验是否有不安全因素。 这个过程就遇到了一个问题:ServletRequest 的 getReader() 和 getInputStream() 两个方法只能被调用一次,而且不能两个都调用。那么如果 Filter 中调用了一次,在 Controller 里面就不能再调用了

2、解决思路

解决问题思路:先将 RequestBody 保存为一个byte数组,然后通过Servlet自带的 HttpServletRequestWrapper 类覆盖getReader()和getInputStream()方法,使流从保存的byte数组读取。然后在Filter中将 ServletRequest 替换为 RepeatedlyRequestWrapper。

3、实现方法

1、创建过滤器

package com.ruoyi.common.filter;

import com.ruoyi.common.utils.StringUtils;
import org.springframework.http.MediaType;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * Repeatable 过滤器
 * 
 * @author ruoyi
 */
public class RepeatableFilter implements Filter
{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {
        ServletRequest requestWrapper = null;
        // 判断请求是否属于 HttpServletRequest,并且 Content-type 是否是 "application/json"
        if (request instanceof HttpServletRequest
                && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
        {
            // 判断结果:HttpServletRequest 请求,Content-type 是 "application/json"
            // 创建包装类 RepeatedlyRequestWrapper 封装原生 HttpServletRequest 请求

            // Filter 中将 ServletRequest 替换为 RepeatedlyRequestWrapper
            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
        }
        if (null == requestWrapper)
        {
            // 如果为空 --> 使用原生 HttpServletRequest 请求
            chain.doFilter(request, response);
        }
        else
        {
            // 如果不为空 -->  使用包装类 RepeatedlyRequestWrapper
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy()
    {

    }
}

学新通
package com.ruoyi.common.filter;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.http.HttpHelper;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 构建可重复读取inputStream的request
 *
 *  问题:系统为了提高项目安全性,拦截非法访问,要给项目增加了一个过滤器,拦截所有的请求,校验是否有不安全因素。
 *      这个过程就遇到了一个问题:ServletRequest 的 getReader() 和 getInputStream() 两个方法只能被调用一次,而且不能两个都调用。
 *      那么如果 Filter 中调用了一次,在 Controller 里面就不能再调用了
 *
 *  解决问题思路:先将 RequestBody 保存为一个byte数组,然后通过Servlet自带的 HttpServletRequestWrapper 类覆盖getReader()和getInputStream()方法,
 *      使流从保存的byte数组读取。然后在Filter中将 ServletRequest 替换为 RepeatedlyRequestWrapper。【】
 *
 * @author ruoyi
 */
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{
    /**
     * ServletRequest 类里面的 getInputStream() 和 getReader() 两个方法都注明方法只能被调用一次,由于RequestBody是流的形式读取,
     * 那么流读了一次就没有了,所以只能被调用一次。既然是因为流只能读一次的原因,那么只要将流的内容保存下来,就可以实现反复读取了。
     * byte数组允许被多次读取,而不会丢失内容。下面使用byte数组将流的内容保存下来。
     */
    // 用于保存流的 byte[]
    private final byte[] body;

    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
    {
        super(request);
        request.setCharacterEncoding(Constants.UTF8);
        response.setCharacterEncoding(Constants.UTF8);

        // 读取流中的数据 并保存到数组 body
        body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8);
    }

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

    @Override
    public ServletInputStream getInputStream() throws IOException
    {
        // 从 body 中创建新的流
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream()
        {
            @Override
            public int read() throws IOException
            {
                return bais.read();
            }

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

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

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

            @Override
            public void setReadListener(ReadListener readListener)
            {

            }
        };
    }
}

学新通

2、将过滤器配置到系统中

package com.ruoyi.framework.config;

import com.ruoyi.common.filter.RepeatableFilter;
import com.ruoyi.common.filter.XssFilter;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.DispatcherType;
import java.util.HashMap;
import java.util.Map;

/**
 * Filter配置
 *      为了给 项目添加一些 自定义过滤器,也可以设置 filter 的排序值
 *
 *      Spring 提供了FilterRegistrationBean类,此类提供setOrder方法,可以为filter设置排序值,让spring在注册web filter之前排序后再依次注册。
 * @author ruoyi
 */
@Configuration
public class FilterConfig
{
    // 添加 RepeatableFilter 过滤器
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean someFilterRegistration()
    {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new RepeatableFilter());
        // 添加 所有路径都需要走 这个过滤器
        registration.addUrlPatterns("/*");
        registration.setName("repeatableFilter");
        // 最低级别 最后执行的 过滤器
        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
        return registration;
    }

}

学新通

拦截(Interceptor)和过滤器(Filter)的执行顺序

先进入过滤器后进入拦截器,先出拦截器后出过滤器

学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhibfgah
系列文章
更多 icon
同类精品
更多 icon
继续加载