防CSRF攻击:一场由重复提交的问题引发的前端后端测试口水战

简介: 重复提交,这是一直以来都会存在的问题,当在网站某个接口调用缓慢的时候就会有可能引起表单重复提交的问题,不论form提交,还是ajax提交都会有这样的问题,最近在某社交app上看到这么一幕,这个团队没有做重复提交的验证,从而导致了数据有很多的重复提交,在这里我们不讨论谁对谁错,问题解决即可。

重复提交,这是一直以来都会存在的问题,当在网站某个接口调用缓慢的时候就会有可能引起表单重复提交的问题,不论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中也可以自定义过滤器来实现,代码略

 

相关文章
|
1月前
|
前端开发 JavaScript 测试技术
前端测试技术中,如何提高集成测试的效率?
前端测试技术中,如何提高集成测试的效率?
|
4天前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
26 3
|
20天前
|
前端开发 JavaScript 测试技术
前端自动化测试
前端自动化测试是通过使用工具和脚本自动执行测试用例的过程,旨在提高测试效率、减少人为错误,并确保Web应用的功能在不同环境和设备上的一致性与稳定性。
|
1月前
|
前端开发 JavaScript 测试技术
前端小白逆袭之路:如何快速掌握前端测试技术,确保代码质量无忧!
【10月更文挑战第30天】前端开发技术迭代迅速,新手如何快速掌握前端测试以确保代码质量?本文将介绍前端测试的基础知识,包括单元测试、集成测试和端到端测试,以及常用的测试工具如Jest、Mocha、Cypress等。通过实践和学习,你也能成为前端测试高手。
51 4
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
115 1
|
1月前
|
前端开发 数据管理 测试技术
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第27天】本文介绍了前端自动化测试中Jest和Cypress的实战应用与最佳实践。Jest适合React应用的单元测试和快照测试,Cypress则擅长端到端测试,模拟用户交互。通过结合使用这两种工具,可以有效提升代码质量和开发效率。最佳实践包括单元测试与集成测试结合、快照测试、并行执行、代码覆盖率分析、测试环境管理和测试数据管理。
59 2
|
1月前
|
前端开发 JavaScript 数据可视化
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第26天】前端自动化测试在现代软件开发中至关重要,Jest和Cypress分别是单元测试和端到端测试的流行工具。本文通过解答一系列问题,介绍Jest与Cypress的实战应用与最佳实践,帮助开发者提高测试效率和代码质量。
45 2
|
2月前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
127 1
|
2月前
|
前端开发 小程序 Java
java基础:map遍历使用;java使用 Patten 和Matches 进行正则匹配;后端传到前端展示图片三种情况,并保存到手机
这篇文章介绍了Java中Map的遍历方法、使用Pattern和matches进行正则表达式匹配,以及后端向前端传输图片并保存到手机的三种情况。
26 1
|
2月前
|
Java 测试技术 程序员
「测试线排查的一些经验-上篇」&& 后端工程师
「测试线排查的一些经验-上篇」&& 后端工程师
23 1

热门文章

最新文章

下一篇
DataWorks