Skip to content
🔴🟠🟡🟢🔵🟣🟤⚫⚪

(七)全局处理字符串两边空格

基础项目地址:

https://gitee.com/springzb/admin-boot

一、相关代码

原理都是利用过滤器,和预防 xss 攻击的原理几乎一致

1、前端请求进入到SpaceFilter

2、包装request

3、过滤转义原来的body

4、进入其他过滤器,后到达controller

5、返回数据给前端

SpaceProperties 配置属性

java
package cn.mesmile.admin.common.filter.space;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashSet;
import java.util.Set;

/**
 * 去除请求中字符串两边空格
 * @author zb
 */
@ConfigurationProperties("security.space")
@Data
public class SpaceProperties {

    /**
     *  是否开启去除空格
     */
    private Boolean enabled = true;

    /**
     *  放行拦截的路径
     */
    private Set<String> skipUrl = new HashSet<>();


}

SpaceHttpServletRequestWrapper 请求包装器

java
package cn.mesmile.admin.common.filter.space;

import cn.hutool.core.util.StrUtil;
import cn.mesmile.admin.common.filter.xss.StringJsonUtils;
import cn.mesmile.admin.common.utils.WebUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
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;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * xss过滤包装器
 * @author zb
 */
@Slf4j
public class SpaceHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final HttpServletRequest orgRequest;
    private byte[] body;

    public SpaceHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        this.orgRequest = request;
    }

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

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (super.getHeader("Content-Type") == null) {
            return super.getInputStream();
        } else if (super.getHeader("Content-Type").startsWith("multipart/form-data")) {
            return super.getInputStream();
        } else {
            if (this.body == null) {
                String requestBody = WebUtil.getRequestBody(super.getInputStream());
                // 去除两边空格
                if (StrUtil.isNotEmpty(requestBody)){
                    // 去除json字符串中所有类型为string两边的空格
                    Map<String, Object> stringObjectMap = StringJsonUtils.
                            jsonStringToMapAndTrim(requestBody, false, true);
                    requestBody = JSONObject.toJSONString(stringObjectMap);
                }else {
                    // 为空则直接返回
                    return super.getInputStream();
                }
                this.body = requestBody.getBytes();
            }
            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.body);
            return new ServletInputStream() {
                @Override
                public int read() {
                    return byteArrayInputStream.read();
                }
                @Override
                public boolean isFinished() {
                    return false;
                }
                @Override
                public boolean isReady() {
                    return false;
                }
                @Override
                public void setReadListener(ReadListener readListener) {
                }
            };
        }
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(this.spaceTrim(name));
        if (StrUtil.isNotBlank(value)) {
            value = this.spaceTrim(value);
        }
        return value;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] parameters = super.getParameterValues(name);
        if (parameters != null && parameters.length != 0) {
            for(int i = 0; i < parameters.length; ++i) {
                parameters[i] = this.spaceTrim(parameters[i]);
            }
            return parameters;
        } else {
            return null;
        }
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = new LinkedHashMap();
        Map<String, String[]> parameters = super.getParameterMap();
        Iterator iterator = parameters.keySet().iterator();
        while(iterator.hasNext()) {
            String key = (String)iterator.next();
            String[] values = (String[])parameters.get(key);
            for(int i = 0; i < values.length; ++i) {
                values[i] = this.spaceTrim(values[i]);
            }
            map.put(key, values);
        }
        return map;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(this.spaceTrim(name));
        if (StrUtil.isNotBlank(value)) {
            value = this.spaceTrim(value);
        }
        return value;
    }


    private String spaceTrim(String input) {
        // 去除字符串两边空格
        return StrUtil.trim(input);
    }

    public HttpServletRequest getOrgRequest() {
        return this.orgRequest;
    }

    public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
        return request instanceof SpaceHttpServletRequestWrapper ? ((SpaceHttpServletRequestWrapper)request).getOrgRequest() : request;
    }
}

SpaceRequestFilter 过滤器

java
package cn.mesmile.admin.common.filter.space;

import org.springframework.util.AntPathMatcher;

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

/***
 * 自定义space过滤器
 * @author zb
 */
public class SpaceRequestFilter implements Filter {

    private final SpaceProperties spaceProperties;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void init(FilterConfig config) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String path = ((HttpServletRequest) request).getServletPath();
        if (this.spaceProperties.getEnabled() && !this.isSpaceSkip(path)) {
            SpaceHttpServletRequestWrapper spaceRequest = new SpaceHttpServletRequestWrapper((HttpServletRequest) request);
            chain.doFilter(spaceRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    private boolean isSpaceSkip(String path) {
        return this.spaceProperties.getSkipUrl().stream().anyMatch((pattern) -> {
            return this.antPathMatcher.match(pattern, path);
        });
    }

    @Override
    public void destroy() {
    }

    public SpaceRequestFilter(final SpaceProperties spaceProperties) {
        this.spaceProperties = spaceProperties;
    }
}

注册过滤器

java
package cn.mesmile.admin.common.filter;

import cn.mesmile.admin.common.filter.space.SpaceProperties;
import cn.mesmile.admin.common.filter.space.SpaceRequestFilter;
import cn.mesmile.admin.common.filter.xss.XssProperties;
import cn.mesmile.admin.common.filter.xss.XssRequestFilter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.DispatcherType;

/**
 * @author zb
 * @Description 配置注册过滤器
 */
@Configuration(
        proxyBeanMethods = false
)
@EnableConfigurationProperties({XssProperties.class, SpaceProperties.class})
public class RequestConfiguration {

    private final SpaceProperties spaceProperties;
    private final XssProperties xssProperties;

    @Bean
    public FilterRegistrationBean<XssRequestFilter> xssFilterRegistration() {
        FilterRegistrationBean<XssRequestFilter> registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[0]);
        registration.setFilter(new XssRequestFilter(this.xssProperties));
        registration.addUrlPatterns(new String[]{"/*"});
        registration.setName("xssRequestFilter");
        registration.setOrder(Integer.MAX_VALUE);
        return registration;
    }

    @Bean
    public FilterRegistrationBean<SpaceRequestFilter> spaceFilterRegistration() {
        FilterRegistrationBean<SpaceRequestFilter> registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[0]);
        registration.setFilter(new SpaceRequestFilter(this.spaceProperties));
        registration.addUrlPatterns(new String[]{"/*"});
        registration.setName("spaceRequestFilter");
        registration.setOrder(Integer.MAX_VALUE - 1);
        return registration;
    }

    public RequestConfiguration(final XssProperties xssProperties,final SpaceProperties spaceProperties) {
        this.xssProperties = xssProperties;
        this.spaceProperties = spaceProperties;
    }

}

二、测试

java
package cn.mesmile.admin.modules.system.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zb
 * @Description
 */
@Slf4j
@RequestMapping("/api/v1/hello")
@RestController
public class HelloController {

    @GetMapping("/get")
    public String hello (@RequestParam("word") String word) {
        return word;
    }

}

修改application-dev.yml里面的配置,先跳过测试路径

yaml
# 去除空格相关配置
security:
  space:
    enabled: true
    # 先跳过检查路径
    skip-url:
      - /api/v1/hello/**

修改application-dev.yml里面的配置,不跳过测试路径

yaml
# 去除空格相关配置
security:
  space:
    enabled: true
    # 先跳过检查路径
    skip-url:
      - /api