什么是sql注入
SQL注入是比较常见的网络攻击方式之一,在客户端在向服务器发送请求的时候,sql命令通过表单提交或者url字符串拼接传递到后台持久层,最终达到欺骗服务器执行恶意的SQL命令;它不是利用操作系统的BUG来实现攻击,而是针对程序员编程时的疏忽,通过SQL语句,实现无帐号登录,甚至篡改数据库。
sql注入可能产生的影响
恶意用户可以未经授权访问您的应用程序并窃取数据。
他们可以更改,删除数据库中的数据并关闭您的应用程序。
黑客还可以通过执行数据库特定的系统命令来控制运行数据库服务器的系统。
1.过滤器SqlInjectFilter
SqlInjectFilter,实现javax.servlet.Filter接口。即在doFilter方法中实现具体逻辑。
@Slf4j @WebFilter(urlPatterns = "/",filterName = "SqlInjectionFilter") @Configuration public class SqlInjectionFilter implements Filter { private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>( Arrays.asList(""))); private static final String SQL_REG_EXP = ".*(\\b(select|insert|into|update|delete|from|where|trancate" + "|drop|execute|grant|use|union)\\b).*"; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; CustomRequestWrapper requestWrapper = new CustomRequestWrapper(request); Map<String, Object> parameterMap = new HashMap<>(); String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", ""); boolean allowedPath = ALLOWED_PATHS.contains(path); if (!allowedPath) { parameterMap = getParameterMap(parameterMap, request, requestWrapper); // 正则校验是否有SQL关键字 for (Object obj : parameterMap.entrySet()) { Map.Entry entry = (Map.Entry) obj; Object value = entry.getValue(); if (value != null) { boolean isValid = isSqlInject(value.toString(), servletResponse); if (!isValid) { return; } } } } filterChain.doFilter(requestWrapper, servletResponse); } private Map<String, Object> getParameterMap(Map<String, Object> paramMap, HttpServletRequest request, CustomRequestWrapper requestWrapper) { // 1.POST请求获取参数 if ("POST".equals(request.getMethod().toUpperCase())) { String body = requestWrapper.getBody(); if(StringUtils.isNotEmpty(body)){ boolean jsonType = getJSONType(body); if(jsonType==true){ paramMap = JSONObject.parseObject(body, HashMap.class); }else { String[] split = body.split("&"); for (int i = 0; i < split.length; i++) { String[] split1; split1 = split[i].split("="); paramMap.put(split1[0],split1[1]); split1 = null; } } }else { Map<String, String[]> parameterMap = requestWrapper.getParameterMap(); if (parameterMap != null && parameterMap.size() > 0) { Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet(); for (Map.Entry<String, String[]> next : entries) { paramMap.put(next.getKey(), next.getValue()[0]); } } } } else { Map<String, String[]> parameterMap = requestWrapper.getParameterMap(); //普通的GET请求 if (parameterMap != null && parameterMap.size() > 0) { Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet(); for (Map.Entry<String, String[]> next : entries) { paramMap.put(next.getKey(), next.getValue()[0]); } } else { //GET请求,参数在URL路径型式,比如server/{var1}/{var2} String afterDecodeUrl = null; try { //编码过URL需解码解码还原字符 afterDecodeUrl = URLDecoder.decode(request.getRequestURI(), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } paramMap.put("pathVar", afterDecodeUrl); } } return paramMap; } private boolean isSqlInject(String value, ServletResponse servletResponse) throws IOException { if (null != value && value.toLowerCase().matches(SQL_REG_EXP)) { log.info("入参中有非法字符: " + value); HttpServletResponse response = (HttpServletResponse) servletResponse; Map<String, String> responseMap = new HashMap<>(); // 匹配到非法字符,立即返回 responseMap.put("code", "999"); responseMap.put("message","入参中有非法字符"); response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.OK.value()); response.getWriter().write(JSON.toJSONString(responseMap)); response.getWriter().flush(); response.getWriter().close(); return false; } return true; } private boolean getJSONType(String str){ boolean result = false; if (StringUtils.isNotBlank(str)) { str = str.trim(); if (str.startsWith("{") && str.endsWith("}")) { result = true; } else if (str.startsWith("[") && str.endsWith("]")) { result = true; } } return result; } @Override public void destroy() { } }
设置请求装饰类
在拦截请求时,会读取HttpServletRequest的InputStream,而这种数据流一旦读取后,后续无法获取,故所以需要将请求在回写进请求中一份。
public class CustomRequestWrapper extends HttpServletRequestWrapper { private final String body; public CustomRequestWrapper(HttpServletRequest request) throws IOException { super(request); StringBuilder sb = new StringBuilder(); BufferedReader bufferedReader = null; try { InputStream inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); char[] charBuffer = new char[512]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { sb.append(charBuffer, 0, bytesRead); } } else { sb.append(""); } } catch (IOException e) { e.printStackTrace(); throw e; } finally { if (bufferedReader != null) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); throw e; } } } body = sb.toString(); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes("UTF-8")); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() { return bais.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8)); } public String getBody() { return this.body; } @Override public String getParameter(String name) { return super.getParameter(name); } @Override public Map<String, String[]> getParameterMap() { return super.getParameterMap(); } @Override public Enumeration<String> getParameterNames() { return super.getParameterNames(); } @Override public String[] getParameterValues(String name) { return super.getParameterValues(name); } }
本次过滤器是过滤的全部路径,如想要放行某些路径需要在 ALLOWED_PATHS这个数组中进行定义
如果你只是想自定义过滤路径的话,那么@WebFilter(urlPatterns = "/",filterName = "SqlInjectionFilter")这个注解中的urlPatterns就不要写"/"了,写你自己自定义过滤的路径。但需要注意如果想自定义过滤路径就不要在过滤器上添加 @Component、@Repository否则会因大的路径会把小的覆盖掉,具体可参考https://blog.csdn.net/weixin_42822484/article/details/107270672这个大佬的博客。
另外如想要过滤器生效,存在两种方式。
一、以bean注解来诠释次配置文件相关代码如下
@Configuration public class FilterConfiguration { @Bean("sqlFilter") public SqlInjectionFilter sqlInjectFilter() { return new SqlInjectionFilter(); } @Bean public FilterRegistrationBean<SqlInjectionFilter> sqlFilterRegistrationBean() { FilterRegistrationBean<SqlInjectionFilter> filterReg = new FilterRegistrationBean<>(); filterReg.setFilter(sqlInjectFilter()); filterReg.addUrlPatterns("/*"); filterReg.setOrder(1); return filterReg; } @Bean public TomcatServletWebServerFactory webServerFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers((Connector connector) -> { connector.setProperty("relaxedPathChars", "\"<>[\\]^`{|}"); connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}"); }); return factory; } }
TomcatServletWebServerFactory为预防tomcat版本过高时,自动过滤特殊字符做出的配置,如tomcat配置在8.0以下,则不用考虑。
二、在springboot启动类上添加注解声明次过滤器
@ServletComponentScan(basePackages = "此处写你过滤器的全路径")