一、概述
本文使用了3个java类,原理是使用过滤器,封装一个自定义的HttpServletRequest对象,重写其中的getInputStream()
、getParameter()
等方法,替换掉非法字符<>\
等,使得从request中获得的key-value类型参数、header参数、请求体(包含json)参数是合法字符,以防止XSS漏洞;同时增加了request请求体可以重复读取的功能。
二、注意事项
1.springboot项目可直接用,spring项目可以自行修改后使用。
2.过滤器中,全局替换用的是switch,可以把<>\
替换为全角符号;如果还需要替换其它非法字符,可以自行增加。(本人的项目中,好多字符都不能替换,会影响其它逻辑,因此只替换了<>\)
3.注意,由于会替换请求体中的<>\
,因此,如果是上传文件接口,请单独配置,不要走这个过滤器,防止上传文件时、请求体被替换导致文件内容出错。
4.java中,int转char、char转int,直接强制转换就行。(不需要±’0’,用了反而不是想要的结果)
三、可用源码与使用方法
1.项目中可以创建一个filter文件夹,然后把下方3个java类放进去即可。
2.FilterConfig.java
这个类注册过滤器用,其中配置了过滤的路径为/*
。
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class FilterConfig {
/**
* 注册过滤器
*
* @return FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(wrappedHttpServletRequestFilter());
registration.addUrlPatterns("/*");
registration.setName("someFilterRegistration");
return registration;
}
/**
* 实例化StreamFilter
*
* @return Filter
*/
@Bean(name = "wrappedHttpServletRequestFilter")
public Filter wrappedHttpServletRequestFilter() {
return new WrappedHttpServletRequestFilter();
}
}
3.WrappedHttpServletRequestFilter.java
这个类是过滤器实体类,其中配置了,某些特殊路径不走特殊处理逻辑
。(例如文件上传url,不走特殊处理逻辑)
import javax.servlet.*;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 解决request流只读取一次的问题
*/
public class WrappedHttpServletRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if( inWhiteList( (HttpServletRequest)request ) ){
chain.doFilter(request, response);
}else{
ServletRequest wrappedHttpServletRequest = new WrappedHttpServletRequest((HttpServletRequest) request);
chain.doFilter(wrappedHttpServletRequest, response);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
public static boolean inWhiteList(HttpServletRequest request){
//当前请求的路径
String requestURI = request.getRequestURI();
//如果是上传文件的url,那就不走过滤器,直接放行
//注意,如果走了过滤器,就会把上传内容中的><&\#等字符替换为><&\#,可能会有问题,所以不能走
if(requestURI != null && requestURI.contains("/file/upload")){
return true;
}
else {
return false;
}
}
}
4.WrappedHttpServletRequest.java
这个是自定义的HttpServletRequest对象,让请求体可以重复读取
、替换非法字符解决XSS漏洞问题
的逻辑在这里。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 解决request流只读取一次的问题
*/
public class WrappedHttpServletRequest extends HttpServletRequestWrapper {
private Logger log = LoggerFactory.getLogger(WrappedHttpServletRequest.class);
/**
* 存储body数据的容器
*/
private final byte[] body;
public WrappedHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// 将body数据存储起来
body = getBodyString(request).getBytes(Charset.defaultCharset());
}
/**
* 获取请求Body
*
* @param request request
* @return String
*/
public String getBodyString(final ServletRequest request) {
try {
return inputStream2String(request.getInputStream());
} catch (IOException e) {
log.error("WrappedHttpServletRequest-getBodyString报错",e);
throw new RuntimeException(e);
}
}
/**
* 获取请求Body
*
* @return String
*/
public String getBodyString() {
final InputStream inputStream = new ByteArrayInputStream(body);
return inputStream2String(inputStream);
}
/**
* 将inputStream里的数据读取出来并转换成字符串
*
* @param inputStream inputStream
* @return String
*/
private String inputStream2String(InputStream inputStream) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("WrappedHttpServletRequest-inputStream2String报错1", e);
throw new RuntimeException(e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("WrappedHttpServletRequest-inputStream2String报错2", e);
}
}
}
return sb.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
//这里,防止xss漏洞
int i = inputStream.read();
//int转char,并过滤特殊字符
char c = xssEncode((char)i);
//char转int,返回
i = (int)c;
return i;
// return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
//以上是解决无法重复读取请求体用的,也解决了json中包含<img src=n οnerrοr=alert(1)>等xss漏洞的问题/
//以下是解决xss漏洞用的/
/**
* 覆盖getParameter方法,将参数名和参数值都做xss过滤。<br/>
* 如果需要获得原始的值,则通过super.getParameterValues(name)来获取<br/>
* getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
@Override
public String[] getParameterValues(String name) {
String values[] = super.getParameterValues(name);
if(values != null){
for(int i=0; i<values.length; i++){
values[i] = xssEncode(values[i]);
}
}
return values;
}
@Override
public Map<String, String[]> getParameterMap() {
//旧map
Map<String, String[]> parameterMap = super.getParameterMap();
//新map
Map<String, String[]> newParameterMap = new HashMap<String, String[]>();
for (String key : parameterMap.keySet()) {
newParameterMap.put(key, xssEncodes(parameterMap.get(key)));
}
return newParameterMap;
}
/**
* 覆盖getHeader方法,将参数名和参数值都做xss过滤。<br/>
* 如果需要获得原始的值,则通过super.getHeaders(name)来获取<br/>
* getHeaderNames 也可能需要覆盖
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
@Override
public Enumeration<String> getHeaderNames() {
return super.getHeaderNames();
}
/**
* 将容易引起xss漏洞的半角字符直接替换成全角字符
*
* @param s
* @return
*/
private static String xssEncode(String s) {
if (s == null || s.isEmpty()) {
return s;
}
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
//全局替换下,防止xss漏洞
c = xssEncode(c);
sb.append(c);
}
return sb.toString();
}
/**
* 将容易引起xss漏洞的半角字符直接替换成全角字符
*
* 结果,就是全局替换了一个 > < \ ,其它的本系统内不能换,换了会影响其它流程 .........
*
* 根据实际情况决定替换的内容吧
*/
private static char xssEncode(char c) {
char returnC;
//其实,这里转换后,后续可能报错:com.fasterxml.jackson.databind.JsonMappingException
//Illegal unquoted character ((CTRL-CHAR, code 30))
//意思是,json中包含><&\#的话,转json会失败,就进不到controller了。
//这里我没有继续处理了,报错就报错吧,本来json中包含这些字段就不应该继续处理了,报错了正好。
switch (c) {
case '>':
returnC = '>';//全角大于号
break;
case '<':
returnC = '<';//全角小于号
break;
//引号不能换,传json要用
//case '\'':
// sb.append('‘');//全角单引号
// break;
//case '\"':
// sb.append('“');//全角双引号
// break;
//and符号也不能换,传url会用...
//case '&':
// returnC = '&';//全角
// break;
case '\\':
returnC = '\';//全角斜线
break;
//#号也不能换,传url会用...
//case '#':
// returnC = '#';//全角井号
// break;
default:
returnC = c;
break;
}
return returnC;
}
private static String[] xssEncodes(String[] s) {
if(s != null && s.length != 0){
for(int i=0; i<s.length; i++){
s[i] = xssEncode(s[i]);
}
}
return s;
}
}