**XSS 即(Cross Site Scripting)中文名称为:跨站脚本攻击。**XSS的重点不在于跨站点,而在于脚本的执行。那么XSS的原理是:
恶意攻击者在web页面中会插入一些恶意的script代码。当用户浏览该页面的时候,那么嵌入到web页面中script代码会执行,因此会达到恶意攻击用户的目的。
那么XSS攻击最主要有如下分类:反射型、存储型、及 DOM-based型。 反射性和DOM-baseed型可以归类为非持久性XSS攻击。存储型可以归类为持久性XSS攻击。
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 是否开启xss
- enabled:true开启;false不开启
# excludes
xss过滤器不经过的请求路径
# urlPatterns
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);
}
}
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;
}
}
可以给系统添加过滤器,也可以设置过滤器的执行顺序
1、在若依基础上开发的话,如果我们不想使用xss攻击过滤器的话,只需要将 application.yml
文件里面,改为false
:
# 防止XSS攻击
xss:
# 过滤开关
enabled: false
2、如果只想某些接口不进行xss攻击过滤器的话,只需要在excludes
里面加上想要路径即可:
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
参考:https://blog.csdn.net/HDesigner/article/details/121246322?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-121246322-blog-126586638.t5_landing_title_tags_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-121246322-blog-126586638.t5_landing_title_tags_v3&utm_relevant_index=5
系统为了提高项目安全性,拦截非法访问,要给项目增加了一个过滤器,拦截所有的请求,校验是否有不安全因素。 这个过程就遇到了一个问题:ServletRequest 的 getReader() 和 getInputStream() 两个方法只能被调用一次,而且不能两个都调用。那么如果 Filter 中调用了一次,在 Controller 里面就不能再调用了
解决问题思路:先将 RequestBody 保存为一个byte数组,然后通过Servlet自带的 HttpServletRequestWrapper 类覆盖getReader()和getInputStream()方法,使流从保存的byte数组读取。然后在Filter中将 ServletRequest 替换为 RepeatedlyRequestWrapper。
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;
}
}
先进入过滤器后进入拦截器,先出拦截器后出过滤器