Java后台解决request请求体不能重复读取+解决XSS漏洞问题

一、概述

本文使用了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;
    }


}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追逐梦想永不停

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值