关于博客中使用的Guns版本问题请先阅读 Guns二次开发目录
前面的几篇博客中有说过,前端传入的参数,后端是需要做非空和合法性校验(比如商品id需要去数据库查询)的,因为一些不负责任的前端在调用后端接口之前,是不会做必传参数的空值校验的,如果后端也不做数据校验,就会导致非法数据被直接写入数据库,进而引发一系类的问题。对于Guns 这种前后端没有区分的项目,因为前端后端的工作都由Java开发人员包圆了,虽然能极大的保证前端检验会正常进行,但是后端校验应当作为一种硬性指标或者说是一种编程习惯,这也是我们后端人员对自己的工作的一种负责任的态度。
其实前面的增删改查几篇博客中,项目源码不仅有做前端校验,后端校验其实也有介绍。比如后端使用 @Valid 注解来检验字段比较多的接口的参数,通过查询数据库来判断某些参数的合法性。而对于比较少的参数,前端控制器是直接通过@RequestParam 注解来接收的,但这是有问题的,如果此时参数的值是长度为0的空串时,@RequestParam 注解是无法当前判断传入的参数为无效参数的,此时后端代码需要调用StringUtils.isNotBlank()方法来判断。还有一些字符串参数,例如“ abc ”,前端如果没有做去除首尾空格的操作,后端接收到后还是需要通过字符串的trim()处理后才能使用的。因此,后端校验对于字符串参数的处理,不仅需要去除参数的首尾空格,还要做非空判断。
如果只是个别参数如此,也许还可以接受,毕竟工作量不大;但现实情况是几乎所有的参数都得做这样的校验啊!!!如果没有公共的方法来执行这个操作,那我岂不是需要在所有接口的内部业务逻辑执行之前,都写一大堆相同功能的代码来做这些操作?如果你这样做了,说明你对Java的理解真的还有待提高。我的解决方案是:通过Filter过滤器来过滤所有的请求参数,首先是对所有的参数key对应的value值做首尾空格的去除处理,然后再判断参数对应的value值是否为空串,如果是,则连同这个key我都不会传递下去。打个比方,假设请求参数是:{"key1":" zhangsan " , "key2" : " "},那么过滤之后的参数就是:
{"key1" : "zhangsan"},此时就变成只有一个有效参数了,最终接口接收到的也只有这一个有效参数。
我们先来看下已经实现的前端校验:
此时可以发现,前端是做了空串判断的:
为了方便于后面的内容进行讲解,此时我要把前端校验放过去,所以需要制造一个bug:
然后添加时就报错了:
这是在参数比较多的时候,可以定义一个实体类接收,然后使用@Valid 来做后端校验,如果参数只有一个的时候呢,专门定义一个实体类来接收就没有必要了,还是这个接口,我们做个修改:
不仅是空格串(比如:【 " "】),如果参数是空串(比如:【""】),那么请求也是能够成功的:
还是之前那个请求,发送之后,后台接收的就是一个空格串,此时后台还需要对这个参数调用 StringUtils.isBlank()方法来判断的,如果仅仅是这一个接口也就罢了,问题是后续开发的很多接口,可能都需要这样检验,这样就会产生很多无关我们业务的操作,无疑也是增加开发人的工作量。令人无奈的是,这个操作我们又不得不做:
鉴于此,我们就有必要写一个过滤器,对请求参数做一个字符串首尾空格去除的过滤了,只需要添加下面三个类:
这里值得关注的一个操作是,如果参数是空串或者空格,那么就不应该将这个参数名传给后端,通俗点说:如果前端传来的值是【name=""】,那么经过过滤之后,传给后端控制器的参数的map集合中,是没有key为"name"的这个键值对的:
当然了,一些接口,比如静态资源相关的接口,则没必要做参数过滤:
将这三个类引入进来后,重启服务器,还是用之前的请求测试,此时就会抛出异常了,这个异常是说,缺少了请求参数名为“name”的参数,这其实也是 @RequestParameter("name")注解起作用了。
为了让异常能够更方便阅读,我们需要重写一遍这个异常信息,此时需要需改全局的异常处理类:
在这个类中,添加下面这段代码就可以了:
重启服务器,测试效果:
值得注意的是,这个过滤器只能对表单提交和地址栏拼接的参数做过滤,对于路径变量这些参数则没有办法,还是只能使用老的方法挨个校验,所以建议后期写接口的时候,尽量避免使用路径变量传参。
下面是分享这四个类:
(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
(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二次开发目录