Fork me on GitHub

java后端使用token处理表单重复提交

 

  • 保证接口幂等性,表单重复提交

前台解决方案:
提交后按钮禁用、置灰、页面出现遮罩


后台解决方案:   使用token,每个token只能使用一次
1.在调用接口之前生成对应的Token,存放至redis


2.在调用接口时,将生成的令牌放入请求request


3.接口提交的时候获取对应的令牌,如果能够从redis中获得该令牌(获取后将当前令牌删除),
则继续执行访问的业务逻辑


4.接口提交的时候获取对应的令牌,如果获取不到改令牌,则直接返回请勿提交

 

工程源码:https://github.com/youxiu326/sb_more_submit

 

自定义注解

ApiToken注解用于将token保存至request,用于页面取token

ApiRepeatSubmit注解用于标明改方法需要验证token才能提交

package com.huarui.util;

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

/**
 * 生成token注解
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiToken {
}
ApiToken.java
package com.huarui.util;

import com.huarui.common.ConstantUtils;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @功能描述 防止重复提交标记注解
 */
@Target(ElementType.METHOD) // 作用到方法上
@Retention(RetentionPolicy.RUNTIME)// 运行时有效
public @interface ApiRepeatSubmit {
    ConstantUtils value();
} 
ApiRepeatSubmit.java
package com.huarui.common;

/**
 * 【定义从哪里取Token的枚举类】
 * head 即从请求头中取token,即客户端将token放入请求头来请求后端数据
 * body 即直接从请求体中取token
 */
public enum ConstantUtils {
    BOOD,HEAD
} 
ConstantUtils.java

 

spring.thymeleaf.cache=false

spring.redis.host=youxiu326.xin
spring.redis.port=6379
application.properties
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.19.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.huarui</groupId>
    <artifactId>sb_more_submit</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sb_more_submit</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
        <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml

 

 

切面拦截,

 

package com.huarui.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * redis工具类
 */
@Component
public class RedisTokenUtils {

    private long timeout = 2;//过期时间

    @Autowired
    private RedisTemplate redisTemplate;

    private static final String LUASCRIPT =
            "if redis.call('exists', KEYS[1]) == 0 then " +
                    "return 0; " +
                    "else " +
                    "redis.call('del', KEYS[1]); " +
                    "return 1; " +
                    "end;";

    /**
     * 获取Token 并将Token保存至redis
     * @return
     */
    public String getToken() {
        String token = "token_"+ UUID.randomUUID();
        redisTemplate.opsForValue().set(token,token,timeout, TimeUnit.MINUTES);
        return token;
    }

    /**
     * 判断Token是否存在 并且删除Token
     * @param tokenKey
     * @return
     */
    @Deprecated
    public boolean findTokenOld(String tokenKey){
        String token = (String) redisTemplate.opsForValue().get(tokenKey);
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        // token 获取成功后 删除对应tokenMapstoken
        redisTemplate.delete(tokenKey);
        return true;
    }

    /**
     * 判断Token是否存在 并且删除Token
     * @param tokenKey
     * @return
     */
    public boolean findToken(String tokenKey){
        Boolean existKey = (Boolean) redisTemplate.execute(
                (RedisConnection connection) -> connection.eval(
                        LUASCRIPT.getBytes(), //lua脚本
                        ReturnType.BOOLEAN,   //设置返回 布尔值
                        1, //设置key数量
                        tokenKey.getBytes()
                )
        );
        return existKey;
    }

}
RedisTokenUtils

 

package com.huarui.aop;

import javax.servlet.http.HttpServletRequest;
import com.huarui.common.ConstantUtils;
import com.huarui.util.ApiToken;
import com.huarui.util.ApiRepeatSubmit;
import com.huarui.util.RedisTokenUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.concurrent.TimeUnit;


/**
 * @功能描述 aop解析注解
 */
@Aspect
@Component
public class NoRepeatSubmitAop {

    private Log logger = LogFactory.getLog(getClass());

    @Autowired
    private RedisTokenUtils redisTokenUtils;

    /**
     * 将token放入请求
     * @param pjp
     * @param nrs
     */
    @Before("execution(* com.huarui.controller.*Controller.*(..)) && @annotation(nrs)")
    public void before(JoinPoint pjp, ApiToken nrs){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        request.setAttribute("token", redisTokenUtils.getToken());
    }


    /**
     * 拦截带有重复请求的注解的方法
     * @param pjp
     * @param nrs
     * @return
     */
    @Around("execution(* com.huarui.controller.*Controller.*(..)) && @annotation(nrs)")
    public Object arround(ProceedingJoinPoint pjp, ApiRepeatSubmit nrs) {

        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();

            String token = null;
            if (nrs.value() == ConstantUtils.BOOD){
                //从请求体中取Token
                token = (String) request.getAttribute("token");
            }else if (nrs.value() == ConstantUtils.HEAD){
                //从请求头中取Token
                token = request.getHeader("token");
            }
            if (StringUtils.isEmpty(token)){
                return "token 不存在";
            }
            if (!redisTokenUtils.findToken(token)){
                return "请勿重复提交";
            }
            Object o = pjp.proceed();
            return o;
        } catch (Throwable e) {
            e.printStackTrace();
            logger.error("验证重复提交时出现未知异常!");
            return "{\"code\":-889,\"message\":\"验证重复提交时出现未知异常!\"}";
        }

    }

}

 

package com.huarui.controller;

import com.huarui.common.ConstantUtils;
import com.huarui.util.ApiRepeatSubmit;
import com.huarui.util.ApiToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class TestController {


    /**
     * 进入页面
     * @return
     */
    @GetMapping("/")
    @ApiToken
    public String index(){
        return "index";
    }


    /**
     * 测试重复提交接口
     * 将Token放入请求头中
     * @return
     */
    @RequestMapping("/test")
    @ApiRepeatSubmit(ConstantUtils.HEAD)
    public @ResponseBody String test() {
        return ("程序逻辑返回");
    }

} 

 

前端页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <base th:href="${#httpServletRequest.getContextPath()+'/'}">
    <meta charset="UTF-8">
    <title>测试表单重复功能</title>
</head>
<body>

<td colspan="1"><button type="button" onclick="add()">加购</button></td>

</body>

<script src="/jquery-1.11.3.min.js"></script>
<script th:inline="javascript">

    function add(){
        //取得token参数
        var token = [[${token}]];
        console.log("获取到的token:" + token);
        $.ajax({
            type: 'POST',
            url: "/test",
            data: {},
            headers: {
                "token":token,
            },
            // dataType: "json",
            success: function(response){
                alert(response);
            },
            error:function(response){
                alert(response);
                console.log(response);
            }
        });
    }

</script>

</html>

 

 

工程源码:https://github.com/youxiu326/sb_more_submit

 

posted @ 2019-06-24 16:08  youxiu326  阅读(2398)  评论(4编辑  收藏  举报