为了账号安全,请及时绑定邮箱和手机立即绑定

踩了http协议下,重复提交数据的坑

标签:
Java

【业务场景】

    在小程序做真机测试的时候,面临了很尴尬的问题。就是双击提交按钮后,数据居然重复提交了。

【想当然地尝试】

    作为一个后端程序猿,首先想到是通过后端解决问题啦。

    根据抄袭cdsn大神的思路,我的思路基本是这样的:

    1.第一次请求会根据ip地址+请求路径,在redis服务器上面留下key=ipAdress+路径,value等于对这个key在redis使用increment函数的count值。也就是说第一次value从零自增到1。

    2.第二次访问会根据ip+请求路径访问同理,但每次访问都会判断count值。count值大于1就不是第一次访问了。

    结果是即使双击提交也能够成功拦截下来的:

    https://img1.sycdn.imooc.com//5ca582be0001586316001247.jpg

    https://img1.sycdn.imooc.com//5ca582c800010fbb16000407.jpg

【结尾(假的)】

    通过这个功能的开发,我深刻地......咳咳.....可能有的大佬看到这里已经开始吐痰了。

    因为这么做......可能功能的健壮性不够,万一ip地址获取出问题整个程序也跑不下去。

【真正的思路】

https://img1.sycdn.imooc.com//5ca589b9000111ac19500980.jpg

https://img1.sycdn.imooc.com//5ca59b310001571020660948.jpg

https://img1.sycdn.imooc.com//5ca59cd90001159019421036.jpg

https://img1.sycdn.imooc.com//5ca5a48700010cb619660954.jpg

https://img1.sycdn.imooc.com//5ca5a76a0001392e19620942.jpg

    那么,这么简单的程序怎么实现呢?由我,慕课网第一C(opy)V(iscidity)侠带大家来捋一捋。

========== 无齿的分割线 ==========

【实现小程序获取token】

1.    我需要在mysql有一张表,专门保存用过我这个小程序的用户的简单信息。不然什么妖艳贱货都来获取我的token。

https://img1.sycdn.imooc.com//5ca5accd000187d806080482.jpg

2.    我们先强迫每一个用户在使用我这个小程序的第一刻必须登陆

-- 小程序登陆

onLoad: function() {
    // 获取当前位置
    var me = this;
    me.reverseGeocoderSetLocation();
    var openid = common.getOpenid();
},

common.js

var config = require('../libs/config.js');

function getOpenid() {
    var openid = wx.getStorageSync('openid');
    if (openid == '' || openid == null || openid == undefined) {
        wx.login({
            success: function (res) {
                var code = res.code;
                wx.request({
                    url: config.Config.serverUrl + '/wechat/code2session',
                    method: "POST",
                    data: {
                        code: code
                    },
                    header: {
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    success: function (res) {
                        var data = res.data;
                        if (data.data != undefined && data.data.openid != undefined) {
                            wx.setStorageSync('openid', data.data.openid);
                            console.log('openid:' + data.data.openid +'保存到缓存');
                            return data.data.openid;
                        }
                    }
                });
            }
        })
    } else {
        return openid;
    }
}

module.exports = {
    getOpenid: getOpenid
}

-- 后端微信登陆接口,在登陆成功且用户信息不存在时创建用户

@RestController
@Api(value = "微信相关业务的接口", tags = {"微信相关业务的controller"})
@RequestMapping("/wechat")
public class WechatController {

    @Autowired
    private WxMaService wxMaService;

    @Autowired
    private UserService userService;

    @ApiOperation(value = "通过code获取登陆信息", notes = "通过code获取登陆信息的接口")
    @PostMapping(value = "/code2session")
    public IMoocJSONResult getOpenIdByCode(String code) {
        if(StringUtils.isBlank(code)) {
            return IMoocJSONResult.errorMsg("code不能为空");
        }
        WxMaJscode2SessionResult result = null;
        try {
            result = wxMaService.jsCode2SessionInfo(code);
            //检查用户是否存在,不存在则新增用户。
            if(StringUtils.isNotBlank(result.getOpenid()) &&
                    !userService.queryOpenidIsExist(result.getOpenid())) {
                User user = new User();
                user.setOpenid(result.getOpenid());
                user.setCreateTime(new Date());
                userService.saveUser(user);
            }
        } catch (Exception e) {
            return IMoocJSONResult.errorMsg(e.getMessage());
        }
        return IMoocJSONResult.ok(result);
    }
}

运用了StringUtils和WxMaService两个工具类,分别来自以下maven库

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>3.3.0</version>
</dependency>

3.    通过openid与数据库校验,才发放token(令牌)

@RestController
@Api(value = "令牌相关业务的接口", tags = {"令牌相关业务的controller"})
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private final String API_TOKEN = "api_token";

    @ApiOperation(value = "获取令牌", notes = "根据openid校对数据库,获取令牌")
    @PostMapping(value = "/openid")
    public IMoocJSONResult getTokenByOpenId(String openid) {
        //检查用户是否存在,和openid是否为空
        if(StringUtils.isNotBlank(openid) && userService.queryOpenidIsExist(openid)) {
            String key = API_TOKEN + ":" + openid + ":" + UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(key, "1");
            redisTemplate.expire(key, 30 * 60 * 1000, TimeUnit.MILLISECONDS);
            return IMoocJSONResult.ok(key);
        }
        return IMoocJSONResult.errorMsg("获取令牌出错");
    }
}

在这里引用了redisTemplate大概跟以下两个maven库有关

当然看到了<parent></parent>就知道,我的后端应用框架是springboot

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>1.5.2.RELEASE</version>
</dependency>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.6.RELEASE</version>
    <relativePath/>
</parent>

【实现提交数据令牌校验】

以下操作大量思路来自csdn各位大佬,勿喷。

1.    声明一个自定义注解

package com.imooc.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE}) // 作用到类,方法,接口上等
@Retention(RetentionPolicy.RUNTIME) // 在运行时可以获取
public @interface ApiToken {
    //前后端传递这个api_token的名字
    String name() default "API_TOKEN";
}

2.    定义一个切面(AOP)类,对被注解的类(或者方法)作出处理

package com.imooc.aspect;

import com.imooc.annotation.ApiToken;
import com.imooc.utils.IMoocJSONResult;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@Aspect
@Component
public class ApiTokenContract {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Around("@annotation(com.imooc.annotation.ApiToken)" )
    public Object around(ProceedingJoinPoint point) throws Throwable {
        try {
            Object result = null;
            //通过SpringBoot提供的RequestContextHolder获得request
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            HttpSession session = request.getSession();
            if(request.getMethod().equalsIgnoreCase("get")){
                //方法为get
                //TODO get方法留作请求html时,分配token给页面
            } else {
                //方法为post
                //获取自定义注解里的值
                MethodSignature signature = (MethodSignature) point.getSignature();
                ApiToken annotation = signature.getMethod().getAnnotation(ApiToken.class);
                //从request中取出提交时,携带的token
                String submitToken = request.getParameter(annotation.name());
                if (StringUtils.isNotBlank(submitToken)) {
                    long count = del(submitToken);
                    if (count == 1) {
                        //说明redis数据库里有这个key(token)
                        //执行方法
                        result = point.proceed();
                    } else {
                        return IMoocJSONResult.errorMsg( "令牌不正确");
                    }
                } else {
                    return IMoocJSONResult.errorMsg( "提交数据请使用令牌");
                }
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return IMoocJSONResult.errorMsg( "执行防止重复提交功能AOP失败,原因:" + e.getMessage());
        }
    }

    /**
     * 拓展redisTemplate的删除方法
     * 根据redis的特性:redis直接删除某个key,key存在则返回1,不存在返回0
     * @param keys
     */
    private long del(final String... keys) {
        return redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                long result = 0;
                for (int i = 0; i < keys.length; i++) {
                    result = connection.del(keys[i].getBytes());
                }
                return result;
            }
        });
    }
}

3.    使用自定义的注解

在小程序传递参数,在访问接口前事先获取了token

https://img1.sycdn.imooc.com//5ca991280001849213320672.jpg

在后端对应接口出使用自定义注解(@ApiToken),因为没有给name传值,所以参数名使用默认的API_TOKEN

https://img1.sycdn.imooc.com//5ca99100000185a817680674.jpg

【最终测试结果】

疯狂点击提交按钮,结果提交了三次,但只有一次是让数据保存了

https://img1.sycdn.imooc.com//5ca9925e0001d99320481596.jpg

https://img1.sycdn.imooc.com//5ca992710001da5e20481596.jpg

https://img1.sycdn.imooc.com//5ca99280000145c020481596.jpg

【写在最后】

到了这里,终于.....你们以为这样就完了吗?

咳咳......

中国的程序猿永不认输!

当然了,由于产品经理对于我水前端十分不满,给我找了一个帖子。最后给大家贡献以下,前端是怎么样避免重复点击的,原理是完全不懂(CV侠在此!)。

不屑于看我怎么CV的老哥直接看以下帖子:https://www.jianshu.com/p/52ec7ede1200

在commons.js定义一个防止短时间内反复调用一个方法的方法(防抖)

function throttle(fn, gapTime) {
    if (gapTime == null || gapTime == undefined) {
        gapTime = 1500
    }
    let _lastTime = null
    // 返回新的函数
    return function () {
        let _nowTime = + new Date()
        if (_nowTime - _lastTime > gapTime || !_lastTime) {
            fn.apply(this, arguments)   //将this和参数传给原函数
            _lastTime = _nowTime
        }
    }
}

并且exports这个防抖的方法

module.exports = {
    getOpenid: getOpenid,
    throttle: throttle
}

最后在小程序声明方法时,把方法嵌套进这个防抖的方法

bindSend: common.throttle(function() {
    console.log('提交');
}, 2000)

亲测有用!在两秒内只能提交一次!!!


咳咳,我不是程序猿,我只是代码的搬运工(狗头)

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
4
获赞与收藏
17

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消