Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格

 关于博客中使用的Guns版本问题请先阅读   Guns二次开发目录    

 

       前面的几篇博客中有说过,前端传入的参数,后端是需要做非空和合法性校验(比如商品id需要去数据库查询)的,因为一些不负责任的前端在调用后端接口之前,是不会做必传参数的空值校验的,如果后端也不做数据校验,就会导致非法数据被直接写入数据库,进而引发一系类的问题。对于Guns 这种前后端没有区分的项目,因为前端后端的工作都由Java开发人员包圆了,虽然能极大的保证前端检验会正常进行,但是后端校验应当作为一种硬性指标或者说是一种编程习惯,这也是我们后端人员对自己的工作的一种负责任的态度。

       其实前面的增删改查几篇博客中,项目源码不仅有做前端校验,后端校验其实也有介绍。比如后端使用 @Valid 注解来检验字段比较多的接口的参数,通过查询数据库来判断某些参数的合法性。而对于比较少的参数,前端控制器是直接通过@RequestParam 注解来接收的,但这是有问题的,如果此时参数的值是长度为0的空串时,@RequestParam 注解是无法当前判断传入的参数为无效参数的,此时后端代码需要调用StringUtils.isNotBlank()方法来判断。还有一些字符串参数,例如“  abc  ”,前端如果没有做去除首尾空格的操作,后端接收到后还是需要通过字符串的trim()处理后才能使用的。因此,后端校验对于字符串参数的处理,不仅需要去除参数的首尾空格,还要做非空判断。

        如果只是个别参数如此,也许还可以接受,毕竟工作量不大;但现实情况是几乎所有的参数都得做这样的校验啊!!!如果没有公共的方法来执行这个操作,那我岂不是需要在所有接口的内部业务逻辑执行之前,都写一大堆相同功能的代码来做这些操作?如果你这样做了,说明你对Java的理解真的还有待提高。我的解决方案是:通过Filter过滤器来过滤所有的请求参数,首先是对所有的参数key对应的value值做首尾空格的去除处理,然后再判断参数对应的value值是否为空串,如果是,则连同这个key我都不会传递下去。打个比方,假设请求参数是:{"key1":"    zhangsan  " , "key2" : "     "},那么过滤之后的参数就是:

{"key1" : "zhangsan"},此时就变成只有一个有效参数了,最终接口接收到的也只有这一个有效参数。

 

我们先来看下已经实现的前端校验:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第1张图片

 

此时可以发现,前端是做了空串判断的:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第2张图片

 

为了方便于后面的内容进行讲解,此时我要把前端校验放过去,所以需要制造一个bug:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第3张图片

 

然后添加时就报错了:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第4张图片

 

 

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第5张图片

 

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第6张图片

 

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第7张图片

 

这是在参数比较多的时候,可以定义一个实体类接收,然后使用@Valid 来做后端校验,如果参数只有一个的时候呢,专门定义一个实体类来接收就没有必要了,还是这个接口,我们做个修改:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第8张图片

 

 

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第9张图片

 

不仅是空格串(比如:【  "     "】),如果参数是空串(比如:【""】),那么请求也是能够成功的:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第10张图片

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第11张图片

 

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第12张图片

 

还是之前那个请求,发送之后,后台接收的就是一个空格串,此时后台还需要对这个参数调用 StringUtils.isBlank()方法来判断的,如果仅仅是这一个接口也就罢了,问题是后续开发的很多接口,可能都需要这样检验,这样就会产生很多无关我们业务的操作,无疑也是增加开发人的工作量。令人无奈的是,这个操作我们又不得不做:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第13张图片

 

鉴于此,我们就有必要写一个过滤器,对请求参数做一个字符串首尾空格去除的过滤了,只需要添加下面三个类:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第14张图片

 

这里值得关注的一个操作是,如果参数是空串或者空格,那么就不应该将这个参数名传给后端,通俗点说:如果前端传来的值是【name=""】,那么经过过滤之后,传给后端控制器的参数的map集合中,是没有key为"name"的这个键值对的:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第15张图片

 

当然了,一些接口,比如静态资源相关的接口,则没必要做参数过滤:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第16张图片

 

将这三个类引入进来后,重启服务器,还是用之前的请求测试,此时就会抛出异常了,这个异常是说,缺少了请求参数名为“name”的参数,这其实也是 @RequestParameter("name")注解起作用了。

 

为了让异常能够更方便阅读,我们需要重写一遍这个异常信息,此时需要需改全局的异常处理类:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第17张图片

 

在这个类中,添加下面这段代码就可以了:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第18张图片

 

重启服务器,测试效果:

Guns二次开发(十四):自定义拦截器去除请求参数的首尾空格_第19张图片

 

 

值得注意的是,这个过滤器只能对表单提交和地址栏拼接的参数做过滤,对于路径变量这些参数则没有办法,还是只能使用老的方法挨个校验,所以建议后期写接口的时候,尽量避免使用路径变量传参。

 

下面是分享这四个类:

(1)FilterConfig.java

package cn.stylefeng.guns.elephish.filter.trim;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.DispatcherType;

/**
 * Created by hqq on 2020/3/21.
 */
@Configuration
public class FilterConfig {

    /**
     * 去除参数头尾空格过滤器
     * @return
     */
    @Bean
    public FilterRegistrationBean paramsFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setFilter(new ParamsFilter());
        registration.addUrlPatterns("/*");
        registration.setName("paramsFilter");
        registration.setOrder(-10);
        return registration;
    }
}

 

(2)ParameterRequestWrapper.java

package cn.stylefeng.guns.elephish.filter.trim;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.util.*;

/**
 * Created by hqq 
 */
public class ParameterRequestWrapper extends HttpServletRequestWrapper {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private Map params = new HashMap<>();

    public ParameterRequestWrapper(HttpServletRequest request) {
        // 将request交给父类,以便于调用对应方法的时候,将其输出,其实父亲类的实现方式和第一种new的方式类似
        super(request);
        //将参数表,赋予给当前的Map以便于持有request中的参数
        Map requestMap=request.getParameterMap();
//        logger.info("请求地址栏的参数转化前:"+ JSON.toJSONString(requestMap));

        this.params.putAll(requestMap);
        this.modifyParameterValues();

//        logger.info("请求地址栏的参数转化后:"+JSON.toJSONString(params));
    }

    /**
     * 重写getInputStream方法  post类型的请求参数必须通过流才能获取到值
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {

       logger.info("content-type是:"+super.getHeader(HttpHeaders.CONTENT_TYPE)+",要求的:"+MediaType.APPLICATION_JSON_UTF8_VALUE);

        //非json类型,直接返回
       if(!MediaType.APPLICATION_JSON_UTF8_VALUE.toLowerCase().contains(super.getHeader(HttpHeaders.CONTENT_TYPE).toLowerCase())){
           return super.getInputStream();
       }

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

//        logger.info("json请求体内的参数转化前:"+ json);
        Map map=jsonStringToMap(json);
//        logger.info("json请求体内的参数转化后:"+ JSON.toJSONString(map));

        ByteArrayInputStream bis = new ByteArrayInputStream(JSON.toJSONString(map).getBytes("utf-8"));
        return new MyServletInputStream(bis);
    }

    /**
     * 将parameter的值去除空格后重写回去
     */
    private void modifyParameterValues(){

        //定义一个map保存最终的参数
        Map map = new HashMap<>();

        Set set =params.keySet();
        Iterator it=set.iterator();
        String key =null;
        String[] values = null;
        String value = null;
        while(it.hasNext()){
            key= it.next();
            values = params.get(key);
            value = values[0].trim();

            //如果没有值,那么这个参数就不应该出现,所以直接去除这个键值对
            if(StringUtils.isBlank(value)){
                continue;
            }

            values[0] = value;
            map.put(key,values);
        }

        this.params = map;
    }


    /**
     * 将json字符串转换成map
     * @param jsonString
     * @return
     */
    private Map jsonStringToMap(String jsonString) {
        Map map = new HashMap<>();
        JSONObject jsonObject = JSONObject.parseObject(jsonString);
        for (Object k : jsonObject.keySet()) {
            Object o = jsonObject.get(k);

            if(o==null || StringUtils.isBlank(o.toString())){
                continue;
            }

            if (o instanceof JSONArray) {
                List> list = new ArrayList<>();
                Iterator it = ((JSONArray) o).iterator();
                while (it.hasNext()) {
                    Object obj = it.next();
                    list.add(jsonStringToMap(obj.toString()));
                }
                map.put(k.toString(), list);
            } else if (o instanceof JSONObject) {
                // 如果内层是json对象的话,继续解析
                map.put(k.toString(), jsonStringToMap(o.toString()));
            } else {
                // 如果内层是普通对象的话,直接放入map中
                // map.put(k.toString(), o.toString().trim());
                if (o instanceof String) {
                    map.put(k.toString(), o.toString().trim());
                } else {
                    map.put(k.toString(), o);
                }
            }
        }
        return map;
    }

    /**
     * 重写getParameter 参数从当前类中的map获取
     */
    @Override
    public String getParameter(String name) {
        String[] values = params.get(name);
        if(values == null || values.length == 0) {
            return null;
        }
        return values[0];
    }
    /**
     * 重写getParameterValues
     */
    @Override
    public String[] getParameterValues(String name) {//同上
        return params.get(name);
    }

    class MyServletInputStream extends  ServletInputStream{
        private ByteArrayInputStream bis;
        public MyServletInputStream(ByteArrayInputStream bis){
            this.bis=bis;
        }
        @Override
        public boolean isFinished() {
            return true;
        }

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

        @Override
        public void setReadListener(ReadListener listener) {

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

}
 
  

 

(3)ParamsFilter.java

package cn.stylefeng.guns.elephish.filter.trim;

import cn.stylefeng.guns.elephish.utils.StringUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

/**
 * Created by hqq on 2020/3/21.
 */
@Component
@WebFilter(urlPatterns = "/**", filterName = "ParamsFilter", dispatcherTypes = DispatcherType.REQUEST)
public class ParamsFilter implements Filter {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) arg0;
        String uri = request.getRequestURI();

        //所有的静态资源和某些接口都不拦截
        if(StringUtils.startsWithIgnoreCase(uri,"/static")
                ||StringUtils.contains("/mgr/upload,/ueditor,",uri+",")
                || StringUtils.endsWithIgnoreCase(uri,".png")
                || StringUtils.endsWithIgnoreCase(uri,".jpg")){

            arg2.doFilter(arg0, arg1);
            return ;
        }

        logger.info("进入网关的去除非路径变量的字符串首尾空格的拦截器了,URI是:"+uri);
        ParameterRequestWrapper requestWrapper = new ParameterRequestWrapper(request);

        arg2.doFilter(requestWrapper, arg1);

    }

    @Override
    public void destroy() {

    }

    @Override
    public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {

    }



}

 

(4)GlobalExceptionHandler.java

/**
 * Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng)
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.stylefeng.guns.core.aop; import cn.stylefeng.guns.core.common.exception.BizExceptionEnum; import cn.stylefeng.guns.core.common.exception.InvalidKaptchaException; import cn.stylefeng.guns.core.log.LogManager; import cn.stylefeng.guns.core.log.factory.LogTaskFactory; import cn.stylefeng.guns.core.shiro.ShiroKit; import cn.stylefeng.guns.elephish.utils.StringUtil; import cn.stylefeng.roses.core.reqres.response.ErrorResponseData; import cn.stylefeng.roses.kernel.model.exception.ServiceException; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.CredentialsException; import org.apache.shiro.authc.DisabledAccountException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.dao.DuplicateKeyException; import org.springframework.http.HttpStatus; import org.springframework.ui.Model; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import java.lang.reflect.UndeclaredThrowableException; import java.sql.SQLIntegrityConstraintViolationException; import java.util.List; import static cn.stylefeng.roses.core.util.HttpContext.getIp; import static cn.stylefeng.roses.core.util.HttpContext.getRequest; /** * 全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截) * * @author fengshuonan * @date 2016年11月12日 下午3:19:56 */ @ControllerAdvice @Order(-1) public class GlobalExceptionHandler { private Logger log = LoggerFactory.getLogger(this.getClass()); /** * 拦截业务异常 */ @ExceptionHandler(ServiceException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData bussiness(ServiceException e) { LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e)); getRequest().setAttribute("tip", e.getMessage()); log.error("业务异常:", e); return new ErrorResponseData(e.getCode(), e.getMessage()); } /** * 拦截数据绑定异常 * @param e * @return */ @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData bindException(BindException e) { //添加错误日志 LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e)); BindingResult result = e.getBindingResult(); String errorMsg = "数据绑定异常"; if (result.hasErrors()) { errorMsg = result.getFieldErrors().get(0).getDefaultMessage(); } getRequest().setAttribute("tip", errorMsg); log.error("数据绑定异常:", errorMsg); return new ErrorResponseData(BizExceptionEnum.SERVER_ERROR.getCode(),errorMsg); } /** * 捕获数据库唯一索引异常 * @param e * @return */ @ExceptionHandler(DuplicateKeyException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData duplicateKeyException(DuplicateKeyException e) { LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e)); String errorMsg = e.getMessage().substring(0,200); if(StringUtils.contains(errorMsg,"Duplicate entry")){ String s=StringUtils.substringBetween(errorMsg,"Duplicate entry '","' for key"); errorMsg=StringUtils.isNotBlank(s)?"数据库中存在同名记录【"+s+"】":errorMsg; } getRequest().setAttribute("tip", errorMsg); log.error("数据库操作异常:",errorMsg); return new ErrorResponseData(500,errorMsg); } /** * 缺少请求参数异常 * @param e * @return */ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData missingServletRequestParameterException(MissingServletRequestParameterException e) { LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e)); String msg = "缺少【"+e.getParameterType()+"】类型的参数【"+e.getParameterName()+"】"; getRequest().setAttribute("tip", "缺少请求参数"); log.error("缺少请求参数异常:", msg); return new ErrorResponseData(500,msg); } /** * 请求参数的类型转换异常 * @param e * @return */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e)); StringBuilder msg = new StringBuilder(); msg.append("参数【").append(e.getName()).append("】的值"); if(StringUtils.isBlank(e.getValue().toString())){ msg.append("不能为空"); }else{ msg.append("应该是【"+e.getRequiredType()+"】类型的"); } getRequest().setAttribute("tip", msg.toString()); log.error("传入参数的类型不合法异常:", msg.toString()); return new ErrorResponseData(500,msg.toString()); } /** * 用户未登录异常 */ @ExceptionHandler(AuthenticationException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public String unAuth(AuthenticationException e) { log.error("用户未登陆:", e); return "/login.html"; } /** * 账号被冻结异常 */ @ExceptionHandler(DisabledAccountException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public String accountLocked(DisabledAccountException e, Model model) { String username = getRequest().getParameter("username"); LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号被冻结", getIp())); model.addAttribute("tips", "账号被冻结"); return "/login.html"; } /** * 账号密码错误异常 */ @ExceptionHandler(CredentialsException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public String credentials(CredentialsException e, Model model) { String username = getRequest().getParameter("username"); LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号密码错误", getIp())); model.addAttribute("tips", "账号密码错误"); return "/login.html"; } /** * 验证码错误异常 */ @ExceptionHandler(InvalidKaptchaException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public String credentials(InvalidKaptchaException e, Model model) { String username = getRequest().getParameter("username"); LogManager.me().executeLog(LogTaskFactory.loginLog(username, "验证码错误", getIp())); model.addAttribute("tips", "验证码错误"); return "/login.html"; } /** * 无权访问该资源异常 */ @ExceptionHandler(UndeclaredThrowableException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) @ResponseBody public ErrorResponseData credentials(UndeclaredThrowableException e) { getRequest().setAttribute("tip", "权限异常"); log.error("权限异常!", e); return new ErrorResponseData(BizExceptionEnum.NO_PERMITION.getCode(), BizExceptionEnum.NO_PERMITION.getMessage()); } /** * 拦截未知的运行时异常 */ @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData notFount(RuntimeException e) { LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e)); getRequest().setAttribute("tip", "服务器未知运行时异常"); log.error("运行时异常:", e); return new ErrorResponseData(BizExceptionEnum.SERVER_ERROR.getCode(), e.getMessage()); } }

 

至此,分享结束!

 

该系列更多文章请前往 Guns二次开发目录

 

你可能感兴趣的:(Guns)