SpringBoot2.x 防重复提交token(注解方式)

【简介】

在开发过程中经常需要做防止重复提交处理,例如:下订单,保存信息等等

  • 前端处理思路:
    点击按钮后,立即将按钮置灰且不可使用,然后调用处理逻辑接口,当接口有响应后重新使按钮重新亮起可用

  • 后端处理思路:
    思路一、建立数据库唯一索引,通过数据库唯一索引,保证数据唯一
    思路二、通过token方式,调用业务接口前先调用接口获取token,调用业务接口时传入token,先进行token校验和处理,当token正确时删除该token(第二次传入相同token就会校验不通过),然后处理正常的业务逻辑


【项目GitHub地址】

https://github.com/qidasheng2012/springboot2.x_redis

本文token生成后存入Redis,所以在Redis构建的项目基础上进行处理的,Redis项目构建文章:
https://blog.csdn.net/qidasheng2012/article/details/96475737


【项目结构】

SpringBoot2.x 防重复提交token(注解方式)_第1张图片


【项目搭建】

  • 【pom.xml】

<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.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.0.RELEASEversion>
        <relativePath/> 
    parent>

    <groupId>com.examplegroupId>
    <artifactId>springboot2.x_redisartifactId>
    <version>1.0.0version>
    <name>springboot2.x_redisname>
    <description>SpringBoot2.x demo project for Redisdescription>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>

        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.11.0version>
        dependency>

        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.8.1version>
        dependency>

        <dependency>
            <groupId>commons-iogroupId>
            <artifactId>commons-ioartifactId>
            <version>2.4version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

    dependencies>

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

project>

注意需要Redis相关依赖和AOP的依赖,分布式锁的依赖可以根据项目需求进行添加或删除

  • 【ActionToken】

用于生成和删除token处理

package com.example.springboot_redis.token;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 生成token和删除token
 */
@Slf4j
@Component
public class ActionToken {

    // 默认缓存时间
    private final Long TOKEN_EXPIRE_TIME = 60 * 60 * 24L;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /*
     * 生成token
     */
    public String createToken(String sessionId) {
        if (StringUtils.isBlank(sessionId)) {
            return null;
        }

        // 使用UUID当token
        String token = UUID.randomUUID().toString();
        // 存入缓存并设置有效期 TimeUnit.SECONDS 单位:秒
        stringRedisTemplate.opsForValue().set(token, sessionId, TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);

        if (StringUtils.isBlank(stringRedisTemplate.opsForValue().get(token))) {
            throw new RuntimeException("生成token缓存redis失败");
        }

        return token;
    }

    /*
     * 校验token
     */
    public String tokenVerify(String token) {
        if (StringUtils.isBlank(token)) {
            log.info("token 为空");
            return "请勿重复提交";
        }

        String sessionId = stringRedisTemplate.opsForValue().get(token);
        if (StringUtils.isBlank(sessionId)) {
            log.info("Redis 中 key 为 token 的不存在");
            return "请勿重复提交";
        }

        // token 存在,移除Redis中的token,进入业务逻辑
        stringRedisTemplate.delete(token);
        log.info("redis 删除key为token:[{}]成功,进入业务逻辑", token);

        return "";
    }
}
  • 【TokenVerify】

防重复提交注解

package com.example.springboot_redis.token;

import java.lang.annotation.*;

/*
 * 防重复提交注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TokenVerify {
    String value() default "";
}
  • 【TokenAspect】

处理防重复提交的切面

package com.example.springboot_redis.token;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
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.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * 处理防重复提交token切面
 */
@Slf4j
@Aspect
@Component
public class TokenAspect {

    @Autowired
    private ActionToken actionToken;

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    // 切入点签名
    @Pointcut("@annotation(com.example.springboot_redis.token.TokenVerify)")
    private void tokenPoint() {
    }

    // 环绕通知
    @Around(value = "tokenPoint()")
    public Object tokenVerify(ProceedingJoinPoint joinPoint) throws Throwable {

        String msg = actionToken.tokenVerify(request.getParameter("token"));

        if (StringUtils.isBlank(msg)) {
            // 删除token成功,进入业务逻辑
            try {
                return joinPoint.proceed();
            } catch (Exception e) {
                log.error("删除token成功,业务处理异常:", e);
            }
        } else {
            PrintWriter printWriter = null;
            try {
                response.setCharacterEncoding("utf-8");
                response.setContentType("text/html;charset=utf-8");
                printWriter = response.getWriter();
                printWriter.write(msg);
                printWriter.flush();
            } catch (Exception e) {
                log.error("处理token,返回错误信息时异常", e);
            } finally {
                IOUtils.closeQuietly(printWriter);
            }
        }

        return null;
    }

}
  • 【TokenController】

获取token接口和使用@TokenVerify防止重复提交

package com.example.springboot_redis.contoller;

import com.example.springboot_redis.token.ActionToken;
import com.example.springboot_redis.token.TokenVerify;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private ActionToken actionToken;

    // 生成token
    @RequestMapping("/createToken")
    public String createToke(HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        return actionToken.createToken(sessionId);
    }

    // 测试@TokenVerify
    @TokenVerify
    @RequestMapping("/testToken")
    public void testToken() {
        log.info("正常业务逻辑");
    }
}

【测试】

1、 获取token

http://127.0.0.1/token/createToken
SpringBoot2.x 防重复提交token(注解方式)_第2张图片
Redis
SpringBoot2.x 防重复提交token(注解方式)_第3张图片

2、调用测试@TokenVerify方法

http://127.0.0.1/token/testToken?token=9922613d-603d-4fee-bca3-98efb6ba9ed1

第一次访问:
SpringBoot2.x 防重复提交token(注解方式)_第4张图片
Redis中的数据已经删除

第二次访问:
SpringBoot2.x 防重复提交token(注解方式)_第5张图片
日志
SpringBoot2.x 防重复提交token(注解方式)_第6张图片
OK,大功告成!成功的防止重复调用


【推荐好文】

Spring Boot+Redis+拦截器+自定义Annotation实现接口自动幂等

你可能感兴趣的:(SpringBoot)