防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中也可以自定义过滤器来实现,代码略

 

相关文章
|
7月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
643 1
|
3月前
|
存储 前端开发 安全
实现“永久登录”:针对蜻蜓Q系统的用户体验优化方案(前端uni-app+后端Laravel详解)-优雅草卓伊凡
实现“永久登录”:针对蜻蜓Q系统的用户体验优化方案(前端uni-app+后端Laravel详解)-优雅草卓伊凡
199 5
|
5月前
|
人工智能 前端开发 测试技术
如何让AI帮你做前端自动化测试?我们这样落地了
本文介绍了一个基于AI的UI自动化测试框架在专有云质量保障中的工程化实践。
2038 21
如何让AI帮你做前端自动化测试?我们这样落地了
|
8月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
497 70
|
9月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
470 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
7月前
|
存储 消息中间件 前端开发
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
校园圈子系统校园论坛小程序采用uni-app前端框架,支持多端运行,结合PHP后端(如ThinkPHP/Laravel),实现用户认证、社交关系管理、动态发布与实时聊天功能。前端通过组件化开发和uni.request与后端交互,后端提供RESTful API处理业务逻辑并存储数据于MySQL。同时引入Redis缓存热点数据,RabbitMQ处理异步任务,优化系统性能。核心功能包括JWT身份验证、好友系统、WebSocket实时聊天及活动管理,确保高效稳定的用户体验。
431 4
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
|
6月前
|
人工智能 监控 前端开发
AI工具:前端与后端的终极对决?谁将成为新时代的宠儿?
深入探讨AI工具对前端和后端开发的具体影响、各自的机遇与挑战,并分析未来开发者如何驾驭AI,实现能力跃迁。
299 0
|
9月前
|
前端开发 算法 NoSQL
前端uin后端php社交软件源码,快速构建属于你的交友平台
这是一款功能全面的社交软件解决方案,覆盖多种场景需求。支持即时通讯(一对一聊天、群聊、文件传输、语音/视频通话)、内容动态(发布、点赞、评论)以及红包模块(接入支付宝、微信等第三方支付)。系统采用前后端分离架构,前端基于 UniApp,后端使用 PHP 框架(如 Laravel/Symfony),配合 MySQL/Redis 和自建 Socket 服务实现高效实时通信。提供用户认证(JWT 集成)、智能匹配算法等功能,助力快速上线,显著节约开发成本。
263 1
前端uin后端php社交软件源码,快速构建属于你的交友平台
|
8月前
|
监控 前端开发 小程序
陪练,代练,护航,代打小程序源码/前端UNIAPP-VUE2.0开发 后端Thinkphp6管理/具备家政服务的综合型平台
这款APP通过技术创新,将代练、家政、娱乐社交等场景融合,打造“全能型生活服务生态圈”。以代练为切入点,提供模块化代码支持快速搭建平台,结合智能匹配与技能审核机制,拓展家政服务和商业管理功能。技术架构具备高安全性和扩展性,支持多业务复用,如押金冻结、录屏监控等功能跨领域应用。商业模式多元,包括交易抽成、增值服务及广告联名,同时设计跨领域积分体系提升用户粘性,实现生态共生与B端赋能。
765 12
|
10月前
|
JSON 前端开发 测试技术
大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
699 10
大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡

热门文章

最新文章