防CSRF攻擊:一場由重復提交的問題引發的前端后端測試口水戰


重復提交,這是一直以來都會存在的問題,當在網站某個接口調用緩慢的時候就會有可能引起表單重復提交的問題,不論form提交,還是ajax提交都會有這樣的問題,最近在某社交app上看到這么一幕,這個團隊沒有做重復提交的驗證,從而導致了數據有很多的重復提交,在這里我們不討論誰對誰錯,問題解決即可。

 

 首先的一種方式,在前端加入loading,或者是blockUI,在ios以及安卓上也是類似,效果如下: 

這個時候整個頁面不能再用鼠標點擊,只能等待請求響應以后才能操作

具體可以參考blockUI這個插件

此外就是后端了,其實后端在一定程度上也要進行防止重復提交的驗證,某些無所謂的情況下可以在前端加,某些重要的場景下比如訂單等業務就必須再前后端都要做,為了測試方便,blockUI就直接注釋

在后台我們線程sleep5秒

 

@RequestMapping("/CSRFDemo/save")
    @ResponseBody
    public LeeJSONResult saveCSRF(Feedback feedback) {
        
        log.info("保存用戶反饋成功, 入參為 Feedback.title: {}", feedback.getTitle());
        
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        return LeeJSONResult.ok();
    }

多次點擊,效果如下

 

 

步驟1:頁面生成token,每次進入都需要重新生成

設置自定義標簽

package com.javasxy.web.tag;

import java.io.IOException;
import java.io.StringWriter;
import java.util.UUID;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.SimpleTagSupport;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;

import com.javasxy.components.JedisClient;
import com.javasxy.pojo.ActiveUser;
import com.javasxy.web.utils.SpringUtils;

/**
 * 
 * @Title: TokenTag.java
 * @Package com.javasxy.web.tag
 * @Description: 生成頁面token
 * Copyright: Copyright (c) 2016
 * Company:DINGLI.SCIENCE.AND.TECHNOLOGY
 * 
 * @author leechenxiang
 * @date 2017年4月11日 下午3:29:13
 * @version V1.0
 */
public class TokenTag extends SimpleTagSupport {

    private String id;

    private String name;
    
    private static final String CACHE_MAKE_CSRF_TOKEN_ = "cache_make_csrf_token_";

    StringWriter sw = new StringWriter();
    
    private JedisClient jedis = SpringUtils.getContext().getBean(JedisClient.class);
    
    public void doTag() throws JspException, IOException {
        
        // 生成token
        String token = UUID.randomUUID().toString();
        
        // 構建token隱藏框
        String tokenHtml = "<input type='hidden' ";
        if (StringUtils.isNotEmpty(id)) {
            tokenHtml += " id='" + id + "' ";
        } 
        if (StringUtils.isNotEmpty(name)) {
            tokenHtml += " name='" + name + "' ";
        } 
        tokenHtml += " value='" + token + "' ";
        tokenHtml += " />";
        
        // 獲取當前登錄用戶信息
        ActiveUser activeUser = (ActiveUser)SecurityUtils.getSubject().getPrincipal();
        // 設置token到redis(如果是單應用項目設置到session中即可)
        jedis.set(CACHE_MAKE_CSRF_TOKEN_ + ":" + activeUser.getUsername(), token);
        jedis.expire(CACHE_MAKE_CSRF_TOKEN_ + ":" + activeUser.getUsername(), 1800);
        
        JspWriter out = getJspContext().getOut();
        out.println(tokenHtml);
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

頁面生成

 

 查看redis

 攔截器代碼:

 

package com.javasxy.web.controller.interceptor;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.javasxy.common.utils.JsonUtils;
import com.javasxy.common.utils.LeeJSONResult;
import com.javasxy.common.utils.NetworkUtil;
import com.javasxy.components.JedisClient;
import com.javasxy.pojo.ActiveUser;
import com.javasxy.web.controller.filter.ShiroFilterUtils;
import com.javasxy.web.utils.SpringUtils;

public class CSRFTokenInterceptor extends HandlerInterceptorAdapter {
    
    final static Logger log = LoggerFactory.getLogger(CSRFTokenInterceptor.class);
    
    private static final String CACHE_MAKE_CSRF_TOKEN_ = "cache_make_csrf_token_";

    private JedisClient jedis = SpringUtils.getContext().getBean(JedisClient.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        // 獲取從頁面過來的token
        String pageCSRFToken = request.getHeader("pageCSRFToken");
        
        // 獲取IP
        String ip = NetworkUtil.getIpAddress(request);
        if (StringUtils.isEmpty(pageCSRFToken)) {
            String msg = "禁止訪問";
            log.error("ip: {}, errorMessage: {}", ip, msg);
            returnErrorResponse(response, LeeJSONResult.errorTokenMsg(msg));
            return false;
        }
        
        // 當前登錄用戶
        ActiveUser activeUser = (ActiveUser)SecurityUtils.getSubject().getPrincipal();
        
        String CSRFToken = jedis.get(CACHE_MAKE_CSRF_TOKEN_ + ":" + activeUser.getUsername());
        if (StringUtils.isEmpty(CSRFToken)) {
            String msg = "操作頻繁,請稍后再試";
            log.error("ip: {}, errorMessage: {}", ip, msg);
            returnErrorResponse(response, LeeJSONResult.errorTokenMsg(msg));
            return false;
        }
        
        if (!pageCSRFToken.equals(CSRFToken)) {
            String msg = "禁止訪問";
            log.error("ip: {}, errorMessage: {}", ip, msg);
            returnErrorResponse(response, LeeJSONResult.errorTokenMsg(msg));
            return false;
        }
        
        // 清除token
        jedis.del(CACHE_MAKE_CSRF_TOKEN_ + ":" + activeUser.getUsername());
        return true;
    }
    
    public void returnErrorResponse(HttpServletResponse response, LeeJSONResult result) throws IOException, UnsupportedEncodingException {
        OutputStream out=null;
        try{
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/json");
            out = response.getOutputStream();
            out.write(JsonUtils.objectToJson(result).getBytes("utf-8"));
            out.flush();
        } finally{
            if(out!=null){
                out.close();
            }
        }
    }
    
    /*public boolean returnError(HttpServletRequest request, HttpServletResponse response, String errorMsg) throws IOException, UnsupportedEncodingException {
        if (ShiroFilterUtils.isAjax(request)) {
            returnErrorResponse(response, LeeJSONResult.errorTokenMsg(errorMsg));
            return false;
        } else {
            // TODO 跳轉頁面
            return false;
        }
    }*/

}

測試:

 

這樣重復提交的問題就解決了,同時也解決了CSRF攻擊的問題,關於什么是CSRF可以自行百度

注意:

1、token生成也可以在異步調用的時候生成,也就是一次請求一個token,而不是一個頁面一個token,但是這樣做可能會被第三方獲取

2、這里使用了springmvc的攔截器,當然在shiro中也可以自定義過濾器來實現,代碼略

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM