当前位置: 首页 > 科技观察

因为一次重复提交,面试官疯狂diss

时间:2023-03-20 12:42:10 科技观察

在开发项目的时候,你有没有遇到过这样的困惑,用户一直点击按钮提交数据到后台,而你却束手无策!1.故事记得之前的面试当时面试官问了这样一个问题,就是后端如何防止订单重复提交?那时我刚工作一年多,工作经验还不是很丰富。我首先想到的是这个前端可以解决问题。嗯,然后面试官说这个问题一定要后台处理,然后面试就凉了。面试结束后,我开始在百度上搜索资料。除了头条上那些吸引人的广告,我找不到任何可行的答案。在咨询了多位高手后,终于找到了一个比较靠谱的解决方案。(后面会详细分享)前几天在群里看到有朋友在??讨论这个问题,让我想起了之前的经历。今天就和大家一起探讨如何防止重复提交。这个问题!二是问题场景重复提交。顾名思义,就是多次提交数据的意思。比如支付的时候,如果同一个订单支付了多次,就会造成多次扣款。后果可想而知!这样的案例比比皆是。总结一下场景,我们会发现主要有两种:第一种:由于用户误操作或网络卡顿,可能导致多次点击表单提交按钮或刷新提交页面。会造成重复提交;第二类:黑客或恶意用户使用postman、jmeter等工具恶意重复提交表单,攻击网站,造成重复提交;这两种严重的时候,甚至会直接导致系统宕机!3、说了这么多解决方案,如何防止重复提交数据?毫无疑问,它必须同时从前端和后端开始!3.1.前端解决方案使用JavaScript来阻止提交按钮。当用户点击提交按钮时,屏幕弹出遮罩层提示正在加载数据....!直到后端返回结果或者前端请求超时,再关闭遮罩层,防止表单重复提交!3.2.后端解决方案虽然前端通过屏蔽操作按钮,防止用户重复提交数据,但是如果黑客直接绕过前端向后端提交数据,那么后端也必须进行校验,防止重复提交意见书。方案一:在数据库中加入唯一键约束(不推荐)起初,首先想到的是在控制层验证数据,比如用户注册。当用户手机号或邮箱已经存在时,会直接提示提交失败。@RequestMapping(value="/register")publicbooleanregister(@RequestBodyUserDtouserDto)throwsException{//检查邮箱是否注册过QueryWrapperqueryWrapper=newQueryWrapper();queryWrapper.eq("user_email",userDto.getUserEmail());UserdbUser=userService.getOne(queryWrapper);if(dbUser!=null){thrownewCommonExecption("当前邮箱已注册,请使用新邮箱注册或找回密码!");}returnuserService.insert(userDto);}如果你想更安全,你可以在数据库中的关键字段上添加一个唯一的键约束。如果用户邮箱已经插入数据库,则直接抛出异常,说明当前邮箱已经注册!try{userService.insert(userDto);}catch(Exceptione){log.error("用户插入失败",e);thrownewCommonExecption("当前邮箱已注册,请使用新邮箱注册!");}这个解决方案在某些场景下是有效的,比如请求不是很频繁,可以采用这种方式。如果请求非常频繁,服务层需要处理大量的逻辑,这个方案就会遇到很大的瓶颈。以订单支付为例,用户在支付时,首先会对订单数据进行各种基础验证,然后通过风控系统识别是否为机器人操作。风控系统通过后,再连接到银行系统,检查用户的金额是否足够。如果足够,申请扣除。扣款成功后,更新订单状态,将订单数据推送到中央仓库,等待发货。当然,这只是一个基本的流程,实际的处理逻辑要比这复杂的多。这时候我们不能像上面描述的那样对某个关键字做唯一约束,整个处理逻辑需要的时间比较长,如果几个请求同时过来,结果可想而知!方案二:使用缓存ID防止重复提交(推荐)想象一下,当前端请求后端时,首先从后端缓存中获取一个唯一的ID。要求提交数据时,带上这个唯一ID。后端检查此ID是否存在于缓存中。如果存在,则进行业务处理。处理后,从缓存中删除此ID。如果正在处理中,则前端再次提交。此时缓存中的ID状态还没有被清除。直接提示:数据处理过程中,请勿重复提交....具体流程如下!先写一个缓存工具类/***缓存工具类*/publicclassCacheUtil{//hashMap线程安全类privatestaticMapcacheMap=newConcurrentHashMap<>();/***添加缓存*@paramkey*@paramvalue*/publicstaticvoidaddCache(Stringkey,Objectvalue){cacheMap.put(key,value);}/***设置缓存*@paramkey*@paramvalue*/publicstaticvoidsetValue(Stringkey,Objectvalue){cacheMap.put(key,value);}/***获取缓存*@paramkey*@return*/publicstaticObjectgetValue(Stringkey){returncacheMap.get(key);}/***判断key是否存在*@paramkey*@return*/publicstaticbooleancontainKey(Stringkey){returncacheMap.containsKey(key);}/***移除缓存*@paramkey*/publicstaticvoidremoveCache(Stringkey){cacheMap.remove(key);}}写个方法获取唯一ID@PostMapping("/getSubmitToken")publicObjectgetSubmitToken(){StringsubmitToken=UUID.randomUUID().toString();//将交易请求的唯一ID放入缓存池CacheUtil.addCache(submitToken,"false");//返回ID给前端JSONObjectresult=newJSONObject();result.put("submitToken",submitToken);returnresult;}然后为需要验证重复提交的方法写注解@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public@interfaceSubmitToken{booleanvalue()defaulttrue;}然后在类或者方法上用@SubmitToken注解写一个验证处理的拦截器)throwsException{//如果没有映射到方法,直接通过if(!(handlerinstanceofHandlerMethod)){returntrue;}//如果类或方法有SubmitToken注解,则重复提交验证.isAnnotationPre已发送(SubmitToken.class)){finalStringsubmitToken=request.getParameter("submitToken");if(StringUtils.isEmpty(submitToken)){thrownewCommonException("submitToken不能为空!");}if(!CacheUtil.containKey(submitToken)){thrownewCommonException("submitToken无效,请get!");}Objectvalue=CacheUtil.getValue(submitToken);if(!"false".equals(value)){thrownewCommonException("正在处理数据,请勿重复提交");}//验证通过后将submitToken对应的值设置为处理CacheUtil.setValue(submitToken,"true");}returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{//业务后处理后,从缓存中删除submitTokenfinalStringsubmitToken=request.getParameter("submitToken");if(StringUtils.isNotEmpty(submitToken)){CacheUtil.removeCache(submitToken);}}}最后,在方法上使用@SubmitToken注解或需要重复提交的课程/***使用SubmitToken进行方法或类的增删改查*/@SubmitToken@RequestMapping(value="/register")publicbooleanregister(@RequestBodyUserDtouserDto)throwsException{//......}在开发中,我们只需要使用@SubmitToken进行增删改查方法。前端提交数据的时候,首先通过/getSubmitToken接口获取一个submitToken,即唯一ID,然后在提交请求的时候,带上这个参数即可!当你实际使用的时候,你会发现缓存类还有很大的优化空间。本例中使用了ConcurrentHashMap作为缓存类,随着提交请求数的增加,缓存类占用的空间也越来越大,最后很可能会OOM。因此,有两种解决方案:第一种:写一个缓存实体类,里面存放有效期,然后弄一个线程去扫描缓存映射,到了就把过期的数据去掉。第二种方法:将需要缓存的数据写入redis,同时设置过期时间。如果是小项目,第一种方法基本可以解决。如果是中大型项目,建议使用redis搭建高可用缓存集群。同时,还要注意按键的设计。最好使用单独的前缀,比如submittoken-uuid-+项目名作为前缀,方便后期扩容时缓存数据迁移!4.小结本文主要总结一下如何防止后端重复提交数据。可能会有一些遗漏。欢迎广大网友评论吐槽!