• CSRF的防御解决过程


    CSRF是什么,就不多说,网络上的帖子多的去了,关于其定义。

    这里主要介绍我们项目中,是如何解决这个问题的。方案比较简单,重点是介绍和记录一下遇到的问题和一些小的心得。

    1. 解决方案

    A. 用户登录的时候,将创建一个token,此token存放于session当中。(是否在登录后创建token,依据各自系统需求变化)

    B. 基于Filter,对所有的Http请求进行拦截,捕获请求路径,确认路径URL是否在配置的CSRF安全拦截路径列表CsrfList中。

    C. 若在CsrfList中,则检查session中是否含有sToken字段以及Http请求头中是否含有rToken字段。

    D. 若sToken和rToken相等,则认为安全合法的请求,否则将请求拦截,拒绝此次请求。

    这里:

    1》. 主要是基于过滤器Filter来实现,另外,一个比较核心的思想,是将安全路径(需要校验的,比如系统参数相关的增删改相关的数据提交请求)通过配置的方式,以配置文件或者数据库表的形式配置在系统中(本案例,采用的是静态配置文件)。说白了,和Shiro或者Spring security的权限管理很像。

    2》. 另外一点,将后台生成的token数据传递前端,并在前端有数据提交的时候将这个token值带回到后台。笨一点的办法,就是在每次ajax数据提交的时候,都给调用beforeSend方法给XMLHttpRequest里面添加自定义的Header属性(当然,也可以通过其他方式实现token的回传到后台,我这里采用的是Http的自定义Header属性的模式)。最好是有一个全局的配置,至少是文件级别的配置,减少ajax提交数据的时候写入重复的beforeSend调用。

    2. 核心代码

    核心代码,分Filter后端的部分,以及前端的beforeSend调用部分。

    1》. Filter对应的后端部分

    package com.tk.logc.core.csrf;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.Map.Entry;
    import java.util.Properties;
    import java.util.Set;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.log4j.Logger;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.subject.Subject;
    
    import com.google.gson.Gson;
    import com.tk.logc.core.Constants;
    import com.tk.logc.core.ResultData;
    
    
    /**
     * @author shihuc
     * @date  2018年8月21日 上午9:23:02
     */
    public class CsrfFilter implements Filter{
        
        static Logger logger = Logger.getLogger(CsrfFilter.class);
        
        static Set<String> csrfUrls = new HashSet<String>();
        
        static {
            InputStream in = CsrfFilter.class.getResourceAsStream("/conf/csrf.properties");
            Properties properties = new Properties();
            try {
                properties.load(in);
            } catch (IOException e) {            
                e.printStackTrace();
            }
            Iterator<Entry<Object, Object>> it = properties.entrySet().iterator();
            while (it.hasNext()) {
                Entry<Object, Object> entry = it.next();
                Object key = entry.getKey();
                String keys = key.toString().trim();
                String urlPref[] = keys.split("_");
                String urlPrefix = "/";
                for(String pu: urlPref){
                    urlPrefix += pu + "/";
                }
                logger.info("Prefix: " + urlPrefix);
                Object value = entry.getValue();
                String urls = value.toString();
                String urlSuffix[] = urls.split(",");
                String realUrl = "";
                for(String suffix: urlSuffix){
                    suffix = suffix.trim();
                    realUrl = urlPrefix + suffix;
                    csrfUrls.add(realUrl);
                    logger.info("URL: " + realUrl);
                }
            }
        }
    
        /* (non-Javadoc)
         * @see javax.servlet.Filter#destroy()
         */
        @Override
        public void destroy() {
            // TODO Auto-generated method stub
            
        }
    
        /* (non-Javadoc)
         * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
         */
        @Override
        public void doFilter(ServletRequest req, ServletResponse rsp, FilterChain chain) throws IOException, ServletException {    
            HttpServletRequest hReq = (HttpServletRequest) req;
            Subject subject = SecurityUtils.getSubject();
            boolean isAuthed = subject.isAuthenticated();
            Session session = subject.getSession();
            if(isAuthed) {
                Object csrfToken = session.getAttribute(Constants.CURRENT_USER_JOB_KEY);            
                Object httpToken = hReq.getHeader(Constants.CURRENT_USER_JOB_KEY);
                String uri = hReq.getRequestURI().toString();
                String ctx = hReq.getContextPath().toString();
                String tarUri = uri.substring(ctx.length(), uri.length());
                logger.info("REQ URL:" + tarUri + ", sToken: " + csrfToken + ", rToken: " + httpToken);
                if(csrfUrls.contains(tarUri)){
                    if (csrfToken != null && !csrfToken.equals(httpToken)){
                        Gson gson = new Gson();
                        ResultData<HashMap<String, String>> rd = new ResultData<HashMap<String, String>>();
                        rd.setSuccess(false);
                        rd.setMsg(Constants.CURRENT_CSRF_ERRINFO);
                        rsp.setCharacterEncoding("UTF-8");
                        rsp.setContentType("text/html;charset=UTF-8");
                        rsp.getWriter().write(gson.toJson(rd));
                    }else{
                        chain.doFilter(req, rsp);
                    }            
                }else{
                    chain.doFilter(req, rsp);
                }
            }else{
                chain.doFilter(req, rsp);
            }        
        }
    
        /* (non-Javadoc)
         * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
         */
        @Override
        public void init(FilterConfig arg0) throws ServletException {
            // TODO Auto-generated method stub
            
        }
    
    }

    这里,配置文件在静态块里面加载,这里采用了一点点小技巧,方便配置简单化,因为一个后台系统配置功能页面,往往会有多个操作,例如:create,update,delete等,配置的时候,可以将key和value部分优化,然后后台加载时,进行URL路径组装重配。例如,我这里的配置文件:

    #
    #所有需要做CSRF拦截校验的URL,没有弄明白操作逻辑前,请勿修改
    #Key部分是url组成的一部分,依据下划线分隔,和Value部分逗号分开的部分组合成最终的URL
    #Value部分,反映的是一类业务中多个子类型的操作,每个都用逗号分隔
    #例如:a_b=u1,u2 对应的URL信息解析后是: /a/b/u1和/a/b/u2
    #
    system_role=create,update,initUpdate,delete,saveRolePermission,addRolePermission
    system_user=deleteUser,createUser,updateUser,userRole,saveUserRole

    2》. 前端JS的核心代码

    (function($){
      var _ajax = $.ajax;
      $.ajax = function(options){
        var fn = {
          beforeSend: function (XMLHttpRequest) {
            XMLHttpRequest.setRequestHeader("X-Job-Key", $("#csrfToken").val());
          }
        };
        if(options.beforeSend){
          fn.beforeSend = options.beforeSend;
        }
        var _options = $.extend(options,{
          beforeSend: fn.beforeSend
        })
        _ajax(_options);
      }
    })(jQuery);

    这段JS代码,是前端的核心,扩展了jQuery的ajax的行为,主要是将beforeSend函数扩展了,在每次只需ajax的时候,都要执行beforeSend,完成给Http请求头部添加一个自定义的属性值,供后台收到请求的时候,解析校验。

    3. 注意事项

    这里,主要涉及到几点,都是一些细节,容易落入坑里:

    1》.前端用http头部自定义的属性,比较用Cookie安全,为了高效,通过扩展ajax的请求,就像我上面的核心代码JS部分的例子一样,每一个JS文件里面,类似上面加入这段代码。注意:代码最后有一个分号,这个分号一定得加上,否则,在一个JS文件里面,若有多个(function($){})(jQuery)这样的代码段,就会出现下面的错误。

    Uncaught TypeError: (intermediate value)(intermediate value)(...) is not a function
        at VM71 xxxx.js:22

    为了效率,beforeSend的使用,若只有少量的地方使用,可以采用下面的模式,在需要的ajax调用里面使用。

    $.ajax({
        url: url, 
        data: {"id":roleId}, 
        dataType:"json",
        //stype:"GET",
        beforeSend: function (XMLHttpRequest) {        
            XMLHttpRequest.setRequestHeader("X-Job-Key", $("#randomx").val());
        },
        success: function(data){                            
            if(data.isSuccess){
                //初始化数据
                initRoleData(data.object);
            }else{
                bootbox.alert(data.msg);
            }
      }
    });

    2》.Http头部定义的属性变量,不建议使用带有下划线的变量

    这里,之所以这么说,主要是因为现在的web应用系统,很多会采用Nginx作为反向代理,Nginx会对Http请求头部的带有下划线的属性进行过滤处理,丢弃掉了。这样一来,带有下划线的属性,ajax或者其他模式发起的HTTP请求,就会被Nginx默认给丢弃了,后台应用服务器上,就获取不到该变量。

    下面是Nginx官方文档对变量定义中下划线的描述:

    underscores_in_headers
    
    Context: http, server
    Allows or disallows underscores
    in custom HTTP header names. If this directive is set to on, the following example header is considered valid by Nginx: test_ header: value.
    Syntax: on or off Default value: off

    这个问题,被坑的现象是,在本地调试一点问题没有,上测试环境,就总是失败,获取rToken值总是null,才想起有这么一个坑。。。

  • 相关阅读:
    干净卸载mysql (注册表)
    计算机中丢失 MSVCR100.dll
    ORM框架SQLAlchemy学习笔记
    mac sourcetree required password
    ConstantBuffer
    Unity通用渲染管线Shader日志输出工具
    Unity SRP Batcher的工作原理
    Unity中的深度测试相关知识与问题
    渲染杂谈:early-z、z-culling、hi-z、z-perpass到底是什么?
    Unity Compute Shader入门初探
  • 原文地址:https://www.cnblogs.com/shihuc/p/9547922.html
Copyright © 2020-2023  润新知