开源项目——实现XSS过滤Cookie过滤拦截器(二)

开源项目——实现XSS过滤Cookie过滤拦截器(二)

背景

日常我们开发人员在开发一些常用的平台时都会用到各种各样的接口,而对于这些接口的有效管理都会成为我们的一些麻烦事,一些常见的接口管理平台我们使用起来又不是很顺手,因此我想进行编写一个自己的API接口平台,用于我们日常的一些接口快速开发和管理共享使用。
里面会涉及到各类开发的知识,每项知识我们都会进行同步发布相应的学习记录文章,以便于想要学习某类知识的小伙伴能一起来成长。
该项目将每周进行更新2-4篇,该类别下同类延伸出来的文章均会以知识共享——XXXX命名。欢迎大家在我的主页下进行搜索阅读。

简介

本节为API管理平台增加基础功能-防XSS攻击-Cookie过滤,该功能主要为了确保我们接受到的请求具有一定的安全性,因此作为基础功能,我们优先纳入进来

参见文章

之前编写的技术学习文档系列之七、在拦截器中进行XSS与SQL注入拦截

开发环境

  • 系统:windows10
  • JDK:openjdk11
  • 开发工具:IDEA 教育版
  • 框架:SpringBoot
  • 包管理:Gradle
    后续涉及技术逐渐补充。

内容

本次涉及代码如图所示


image.png

1、在build.gradle中加入我们需要使用的软件包

    implementation 'org.springframework.boot:spring-boot-starter-aop'
// https://mvnrepository.com/artifact/org.projectlombok/lombok
    implementation 'org.projectlombok:lombok:1.18.20'

    // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
    implementation 'org.apache.commons:commons-lang3:3.12.0'
    // https://mvnrepository.com/artifact/org.jsoup/jsoup
    implementation 'org.jsoup:jsoup:1.14.2'

2、创建我们需要使用到的工具类

package com.cnhuashao.apimanagement.base.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
import org.jsoup.safety.Whitelist;

/**
 * 类 {@code FilterCoreUtil} 用于Xss非法标签过滤工具类 <br> 过滤html中的xss字符.
 *
 * 本软件仅对本次教程负责,版权所有 <a href="http://www.CN華少.com">中国,華少</a><br>
 *
 * @author CN華少
 * <a href="mailto:lz2392504@gmail.com
 * <p>
 * ">CN華少</a>
 */
@Slf4j
public class FilterCoreUtil {

    /**
     * 使用自带的basicWithImages 白名单
     * 允许的便签有a,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol,p,pre,q,small,span,
     * strike,strong,sub,sup,u,ul,img
     * 以及a标签的href,img标签的src,align,alt,height,width,title属性
     */
    private static final Safelist WHITE_LIST = Safelist.basicWithImages();
    /** 配置过滤化参数,不对代码进行格式化 */
    private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
    static {
        // 富文本编辑时一些样式是使用style来进行实现的
        // 比如红色字体 style="color:red;"
        // 所以需要给所有标签添加style属性
        WHITE_LIST.addAttributes(":all", "style");
    }

    /**
     * 过滤主方法入口
     * @param content 需要过滤的字符串
     * @return 过滤后的字符串
     */
    public static String clean(String content) {
        if(StringUtils.isNotBlank(content)){
            content = content.trim();
        }
        return Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS);
    }
}

3、创建我们需要使用的拦截器

package com.cnhuashao.apimanagement.base.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 类 {@code XssFilter} Xss防止注入拦截器 <br> 用于过滤web请求中关于xss相关攻击的特定字符.
 *
 * 本软件仅对本次教程负责,版权所有 <a href="http://www.CN華少.com">中国,華少</a><br>
 *
 * @author CN華少
 * <a href="mailto:lz2392504@gmail.com
 * <p>
 * ">CN華少</a>
 */
@Slf4j
public class XssFilter implements Filter {

    //private Logger log = LoggerFactory.getLogger(XssFilter.class);

    /**
     * 是否过滤富文本内容
     */
    private static boolean IS_INCLUDE_RICH_TEXT = false;

    /**
     * 预设定白名单地址
     * 将根据该变量中设置的相关目录进行直接放行操作。
     */
    public List<String> excludes = new ArrayList<>();

    /**
     * 拦截器核心处理单元
     * 用于处理所有需要过滤的请求,在此进行确认其合法性
     * @param request
     * @param response
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,ServletException {
        if(log.isDebugEnabled()){
            log.debug("-------------------- get into xss filter --------------------");
        }
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        //进行白名单过滤,如符合白名单,则直接放行
        if(handleExcludeUrl(req, resp)){
            filterChain.doFilter(request, response);
            return;
        }
        //开始进行深度过滤,判定其携带参数是否合法
        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request,IS_INCLUDE_RICH_TEXT);
        filterChain.doFilter(xssRequest, response);
    }

    /**
     *  白名单过滤器
     * @param request 拦截的请求
     * @param response 拦截的响应
     * @return  是否符合白名单
     */
    private boolean handleExcludeUrl(HttpServletRequest request, HttpServletResponse response) {
        //白名单为空时直接返回false,使其向下执行
        if (excludes == null || excludes.isEmpty()) {
            return false;
        }
        //提取访问的URL地址
        String url = request.getServletPath();
        log.info("开始进行过滤{} {}",new Date(),url);
        //开始根据白名单地址进行判定,如符合则直接放行
        for (String pattern : excludes) {
            Pattern p = Pattern.compile("^" + pattern);
            Matcher m = p.matcher(url);
            if (m.find()) {
                return true;
            }
        }

        return false;
    }

    /**
     * 初始化拦截器配置
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        if(log.isDebugEnabled()){
            log.debug("----------------- xss filter init -----------------");
        }
        //获取其初始化时预设置的深度过滤开关,根据其预设的true、false进行确定其是否开启深度拦截
        String isIncludeRichText = filterConfig.getInitParameter("isIncludeRichText");
        if(StringUtils.isNotBlank(isIncludeRichText)){
            IS_INCLUDE_RICH_TEXT = BooleanUtils.toBoolean(isIncludeRichText);
        }
        //获取其初始化时预设置的白名单字符串,根据【,】符号进行截取存储。
        String temp = filterConfig.getInitParameter("excludes");
        if (temp != null) {
            String[] url = temp.split(",");
            for (int i = 0; url != null && i < url.length; i++) {
                excludes.add(url[i]);
            }
        }
    }
}

package com.cnhuashao.apimanagement.base.filter;

import com.cnhuashao.apimanagement.base.util.FilterCoreUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 * 类 {@code XssHttpServletRequestWrapper} Xss核心匹配类 <br> .
 *
 * 本软件仅对本次教程负责,版权所有 <a href="http://www.CN華少.com">中国,華少</a><br>
 *
 * @author CN華少
 * <a href="mailto:lz2392504@gmail.com
 * <p>
 * ">CN華少</a>
 */
@Slf4j
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    /**
     * 需要进行过滤的请求
     */
    HttpServletRequest orgRequest = null;
    /**
     * 是否启用过滤
     */
    private boolean isIncludeRichText = false;

    /**
     * 深度过滤构造方法
     * @param request 需要过滤的请求
     * @param isIncludeRichText 是否进行过滤,默认false
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request, boolean isIncludeRichText) {
        super(request);
        orgRequest = request;
        this.isIncludeRichText = isIncludeRichText;
    }

    /**
     * 覆盖getParameter方法,将参数名和参数值都做xss过滤。<br/>
     * 如果需要获得原始的值,则通过super.getParameterValues(name)来获取<br/>
     * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
     */
    @Override
    public String getParameter(String name) {
        Boolean flag = ("content".equals(name) || name.endsWith("WithHtml"));
        if( flag && !isIncludeRichText){
            return super.getParameter(name);
        }
        name = FilterCoreUtil.clean(name);
        String value = super.getParameter(name);
        if (StringUtils.isNotBlank(value)) {
            value = FilterCoreUtil.clean(value);
        }
        return value;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] arr = super.getParameterValues(name);
        if(arr != null){
            for (int i=0;i<arr.length;i++) {
                arr[i] = FilterCoreUtil.clean(arr[i]);
            }
        }
        return arr;
    }


    /**
     * 覆盖getHeader方法,将参数名和参数值都做xss过滤。<br/>
     * 如果需要获得原始的值,则通过super.getHeaders(name)来获取<br/>
     * getHeaderNames 也可能需要覆盖
     */
    @Override
    public String getHeader(String name) {
        name = FilterCoreUtil.clean(name);
        String value = super.getHeader(name);
        if (StringUtils.isNotBlank(value)) {
            value = FilterCoreUtil.clean(value);
        }
        return value;
    }

    /**
     * 获取最原始的request
     *
     * @return
     */
    public HttpServletRequest getOrgRequest() {
        return orgRequest;
    }

    /**
     * 获取最原始的request的静态方法
     *
     * @return
     */
    public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
        if (req instanceof XssHttpServletRequestWrapper) {
            return ((XssHttpServletRequestWrapper) req).getOrgRequest();
        }

        return req;
    }
}

package com.cnhuashao.apimanagement.base.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

/**
 * 类 {@code CookieFilter} 会话Cookie拦截 <br> 用于对所有web会话的Cookie进行安全检查.
 *
 * 本软件仅对本次教程负责,版权所有 <a href="http://www.CN華少.com">中国,華少</a><br>
 *
 * @author CN華少
 * <a href="mailto:lz2392504@gmail.com
 * <p>
 * ">CN華少</a>
 */
@Slf4j
public class CookieFilter implements Filter{

    /**
     * 继承方法,拦截器初始化逻辑
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    /**
     * 自定义拦截器,用于Cookie全局设置
     * @param request 请求
     * @param response 响应
     * @param chain  filterchain是servlet容器提供给开发人员的对象
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        //日志打印
        if(log.isDebugEnabled()){
            String url = req.getServletPath();
            log.debug("-------- get into Cookie filter {} {}",new Date(),url);
        }
        //获取请求中的cookies
        Cookie[] cookies = req.getCookies();
        //如果不为空,则开始对其中的所有cookie进行设置
        if (cookies!=null){
            for (Cookie cookie : cookies){
                if (cookie!=null){
                    //设置cookie最大有效期,单位秒,当前设置一小时60*60
                    cookie.setMaxAge(3600);
                    //向浏览器指定,只允许https协议下才可以发送cookie
                    cookie.setSecure(true);
                    //设置cookie只能使用
                    cookie.setHttpOnly(true);
                    resp.addCookie(cookie);
                }
            }
        }
        //请求下发
        chain.doFilter(req,resp);
    }

    /**
     * 继承方法,在销毁filter时进行的操作
     */
    @Override
    public void destroy() {

    }
}

4、增加我们的Xss配置加载类

package com.cnhuashao.apimanagement.base.config;

import com.cnhuashao.apimanagement.base.filter.CookieFilter;
import com.cnhuashao.apimanagement.base.filter.XssFilter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * 类 {@code XssConfig} Xss配置加载类 <br> 用于将Xss拦截器在系统初始时加载至web服务器中.
 * 本软件仅对本次教程负责,版权所有 <a href="http://www.CN華少.com">中国,華少</a><br>
 *
 * @author CN華少
 * <a href="mailto:lz2392504@gmail.com
 * <p>
 * ">CN華少</a>
 */
@Configuration
@Slf4j
public class XssConfig {

    /**
     * cookie拦截器
     * 用于Cookie全局设置,主要设置有效期、https安全访问、httpOnly启用
     * @return
     */
    @Bean
    public FilterRegistrationBean cookieFilterRegistrationBean(){
        log.info("------------ Start Cookie Filter ------------");
        //1、启动拦截器
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        //注册Cookie拦截器
        filterRegistrationBean.setFilter(new CookieFilter());
        //设置bean加载顺序
        filterRegistrationBean.setOrder(1);
        //启用注册
        filterRegistrationBean.setEnabled(true);
        //添加URL为全部,使其拦截器全局拦截
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

    /**
     * 配置初始全局拦截器Xss过滤器
     * @return FilterRegistrationBean
     */
    @Bean
    public FilterRegistrationBean xssFilterRegistrationBean() {
        log.info("------------ Start Xss Filter ------------");
        //1、启动拦截器
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        //注册Xss拦截器
        filterRegistrationBean.setFilter(new XssFilter());
        //设置bean加载顺序
        filterRegistrationBean.setOrder(1);
        //启用注册
        filterRegistrationBean.setEnabled(true);
        //添加URL为全部,使其拦截器全局拦截
        filterRegistrationBean.addUrlPatterns("/*");
        //2、设置初始化方法
        Map<String, String> initParameters = new HashMap<String,String>(2);
        //设置白名单
        initParameters.put("excludes", "/static/*,/img/*,/js/*,/css/*");
        //是否启用深度过滤机制(文本过滤机制),默认fales
        initParameters.put("isIncludeRichText", "true");
        //为此注册设置init参数。调用此方法将替换任何*现有的init参数。
        filterRegistrationBean.setInitParameters(initParameters);
        return filterRegistrationBean;
    }

}

5、在resources/log目录下创建logback-spring.xml日志配置文件,使其打印至控制台

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 打印到控制台配置 -->
    <appender name="consoleOutput" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 设置打印级别 -->
        <filter  class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>info</level>
        </filter>
        <withJansi>true</withJansi>
        <!-- 设置打印格式,设置字符集 -->
        <!--格式化输出:%d:表示日期    %thread:表示线程名     %-5level:级别从左显示5个字符宽度  %msg:日志消息    %n:是换行符-->
        <encoder>
            <pattern>%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="consoleOutput"/>
    </root>

</configuration>

6、更换配置文件,使用yml格式,配置文件为application.yml

server:
  port: 8080
spring:
  profiles:
    #dev代表开发、prod代表生产,下方dev代表具体激活的配置文件
    active: dev

#日志配置
logging:
  level:
    root: debug
  config: classpath:log/logback-spring.xml

7、增加其他环境配置文件,用于区分生产与开发使用。application-dev.yml、application-prod.yml

image.png

这样我们就实现了该拦截器的功能,在实现期间可能会由于使用的gradle,造成初期jar包没有有效引入,可以使用idea右侧的gradle选项卡中的刷新按钮,来使其自动根据配置文件加载jar包。


image.png

其次本节使用了lombok,注意及时在idea中安装Lombok组件,并设置为注释编译,使其代码在运行前能有效编译,以免出现部分关键词缺失问题。

文章中的代码将同步更新至API接口管理平台仓库中,有需要的可以进行了解或下载需要的代码。
如果您觉得本文不错,欢迎Star支持

本文声明:

88x31.png

知识共享许可协议
本作品由 cn華少 采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容