欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot中防止接口重復(fù)提交的有效方法

 更新時(shí)間:2024年05月23日 09:03:53   作者:哈__  
在Web應(yīng)用開(kāi)發(fā)過(guò)程中,接口重復(fù)提交問(wèn)題一直是一個(gè)需要重點(diǎn)關(guān)注和解決的難題,本文將從SpringBoot應(yīng)用的角度出發(fā),探討在單機(jī)環(huán)境和分布式環(huán)境下如何有效防止接口重復(fù)提交,希望通過(guò)本文的介紹,讀者能夠掌握在SpringBoot應(yīng)用中防止接口重復(fù)提交的有效方法

前言

在Web應(yīng)用開(kāi)發(fā)過(guò)程中,接口重復(fù)提交問(wèn)題一直是一個(gè)需要重點(diǎn)關(guān)注和解決的難題。無(wú)論是由于用戶誤操作、網(wǎng)絡(luò)延遲導(dǎo)致的重復(fù)點(diǎn)擊,還是由于惡意攻擊者利用自動(dòng)化工具進(jìn)行接口轟炸,都可能對(duì)系統(tǒng)造成嚴(yán)重的負(fù)擔(dān),甚至導(dǎo)致數(shù)據(jù)不一致、服務(wù)不可用等嚴(yán)重后果。特別是在SpringBoot這樣的現(xiàn)代化Java框架中,我們更需要一套行之有效的策略來(lái)防止接口重復(fù)提交。

本文將從SpringBoot應(yīng)用的角度出發(fā),探討在單機(jī)環(huán)境和分布式環(huán)境下如何有效防止接口重復(fù)提交。單機(jī)環(huán)境雖然相對(duì)簡(jiǎn)單,但基本的防護(hù)策略同樣適用于分布式環(huán)境的部署。

接下來(lái),我們將首先分析接口重復(fù)提交的原因和危害,然后詳細(xì)介紹在SpringBoot應(yīng)用中可以采取的防護(hù)策略,包括前端控制、后端校驗(yàn)、使用令牌機(jī)制(如Token)、利用數(shù)據(jù)庫(kù)的唯一約束等。對(duì)于分布式環(huán)境,我們還將探討如何使用分布式鎖、Redis等中間件來(lái)確保數(shù)據(jù)的一致性和防止接口被重復(fù)調(diào)用。

在深入解析各種防護(hù)策略的同時(shí),我們也將結(jié)合實(shí)際案例,展示如何在SpringBoot項(xiàng)目中具體實(shí)現(xiàn)這些策略,并給出一些優(yōu)化建議,以幫助讀者在實(shí)際開(kāi)發(fā)中更好地應(yīng)用這些技術(shù)。希望通過(guò)本文的介紹,讀者能夠掌握在SpringBoot應(yīng)用中防止接口重復(fù)提交的有效方法,為Web應(yīng)用的穩(wěn)定性和安全性提供堅(jiān)實(shí)的保障。

單機(jī)環(huán)境下防止接口重復(fù)提交

在這種單機(jī)的應(yīng)用場(chǎng)景下,我并沒(méi)有使用redis進(jìn)行處理,而是使用了本地緩存機(jī)制。在用戶對(duì)接口進(jìn)行訪問(wèn)的時(shí)候,我們獲取接口的一些參數(shù)信息,并且根據(jù)這些參數(shù)生成一個(gè)唯一的ID存儲(chǔ)到緩存中,下一次在發(fā)送請(qǐng)求的時(shí)候,先判斷這個(gè)緩存中是否有對(duì)應(yīng)的ID,若有則阻攔,若沒(méi)有那么就放行。

導(dǎo)入依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>

項(xiàng)目結(jié)構(gòu)

創(chuàng)建自定義注解

我們也說(shuō)過(guò)了,要根據(jù)接口的一些信息來(lái)生成一個(gè)ID,在單機(jī)環(huán)境下,我定義了一個(gè)注解,這個(gè)注解里邊保存著一個(gè)key作為ID,同時(shí),在把這個(gè)注解加到接口上,那么這個(gè)接口就以這個(gè)key作為ID,在訪問(wèn)接口的時(shí)候,存儲(chǔ)的也是這個(gè)ID值。

@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockCommit {
    String key() default "";
}

創(chuàng)建AOP切面

為了方便之后的接口限流,同時(shí)也想把這件事情做一個(gè)模塊化處理,我使用的是AOP切面,這樣做可以減少代碼耦合,方便維護(hù)。

此外使用了一個(gè)Cache本地緩存用于存儲(chǔ)我們接口的ID,同時(shí)設(shè)置緩存的最大容量和內(nèi)容的過(guò)期時(shí)間,在這里我設(shè)置的是5秒鐘,5秒鐘過(guò)后ID就會(huì)過(guò)期,這個(gè)接口就可以繼續(xù)訪問(wèn)。 

主要的就是這個(gè)環(huán)繞通知了,我先獲取了調(diào)用的接口,也就是具體的方法,之后獲取加在這個(gè)方法上的注解LockCommit,也就是我們上邊自定義的注解。之后拿到注解內(nèi)的key作為ID傳入緩存中。存入之前先判斷是否有這個(gè)ID,如果有就報(bào)錯(cuò),沒(méi)有就加入到緩存中,這個(gè)邏輯不難。

@Aspect
@Component
public class LockAspect {
    public static final Cache<String,Object> CACHES = CacheBuilder.newBuilder()
            .maximumSize(50)
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build();
    @Pointcut("@annotation(com.example.day_04_repeat_commit.annotation.LockCommit)&&execution(* com.example.day_04_repeat_commit.controller.*.*(..))")
    public void pointCut(){}
 
    @Around("pointCut()")
    public Object Lock(ProceedingJoinPoint joinPoint){
 
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        LockCommit lockCommit = method.getAnnotation(LockCommit.class);
        String key = lockCommit.key();
        if(key!=null &&!"".equals(key)){
            if(CACHES.getIfPresent(key)!=null){
                throw new RuntimeException("請(qǐng)勿重復(fù)提交");
            }
            CACHES.put(key,key);
        }
        Object object = null;
        try {
            object = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return object;
    }
}

創(chuàng)建Conotroller

可以看到我在接口上加上了key是stu,對(duì)接口訪問(wèn)后,stu就作為ID保存到CACHE中。這里需要多加注意,如果是多個(gè)人訪問(wèn)這個(gè)接口,那么都會(huì)出現(xiàn)防止重復(fù)提交的問(wèn)題,所以這個(gè)key的值并不能僅僅設(shè)置的這么簡(jiǎn)單??梢约尤胍恍┯脩鬒D,參數(shù)的值,IP等信息作為key的構(gòu)建參數(shù)。這里我僅僅是為了演示。

@RestController
@RequestMapping("/student")
public class StudentController {
    @RequestMapping("/get-student")
    @LockCommit(key = "stu")
    public String getStudent(){
        return  "張三";
    }
}

如果你不想要后臺(tái)報(bào)錯(cuò),而是把錯(cuò)誤的提示信息傳到前端的話,那么你就可以創(chuàng)建一個(gè)全局的異常捕獲器。我創(chuàng)建的這個(gè)異常捕獲器捕獲的是Exception異常,范圍比較大,如果在真實(shí)的開(kāi)發(fā)環(huán)境中,你可能需要自定義異常來(lái)拋出和捕獲。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public String handleException(Exception e){
        return e.getMessage();
    }
}

接著我們啟動(dòng)項(xiàng)目來(lái)測(cè)試一下。為了方便截圖我就不用瀏覽器打開(kāi)了,我是用PostMan進(jìn)行測(cè)試。

  • 第一次訪問(wèn)結(jié)果如下

  • 五秒內(nèi)再次訪問(wèn)結(jié)果如下

  • 五秒后訪問(wèn)結(jié)果如下

分布式環(huán)境下防止接口重復(fù)提交

導(dǎo)入依賴

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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-aop</artifactId>
        </dependency>

項(xiàng)目結(jié)構(gòu)

創(chuàng)建自定義注解

分布式環(huán)境下的就要復(fù)雜一些了 

  • 創(chuàng)建CacheLock
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface CacheLock {
    /**
     * 鎖的前綴
     * @return
     */
    String prefix() default "";
 
    /**
     * 過(guò)期時(shí)間
     * @return
     */
    int expire() default 5;
 
    /**
     * 過(guò)期單位
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
 
    /**
     * key的分隔符
     * @return
     */
    String delimiter() default ":";
}
  • 這個(gè)CacheLock也是加鎖的注解,這個(gè)注解內(nèi)包含了很多的信息,這些信息都要作為Redis加鎖的參數(shù)。

  • 創(chuàng)建CacheParam

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Documented
public @interface CacheParam {
    /**
     * 參數(shù)的名稱
     * @return
     */
    String name() default "";
}

這個(gè)參數(shù)是需要加在具體的參數(shù)上邊的,代表著這個(gè)參數(shù)要作為key構(gòu)建的一部分,當(dāng)然也可以加在一個(gè)對(duì)象的屬性上邊。

創(chuàng)建key的生成工具類 

看到代碼的你一定慌了吧,不要急,在這之前我會(huì)先給你講一下我的思路。我們講的防止接口重復(fù)提交,是防止用戶對(duì)一個(gè)接口多次傳入相同的信息,這種情況我要進(jìn)行處理。我的構(gòu)建思路是想要構(gòu)建一個(gè)這樣的key。加了CacheParam的參數(shù)我獲取參數(shù)具體的值,并且把值作為key的一部分。

倘若我們的參數(shù)都沒(méi)有加CacheParam呢?這個(gè)時(shí)候就會(huì)去獲取這個(gè)參數(shù)的類,比如說(shuō)是Student類,我們就去看看這個(gè)傳來(lái)的Student類當(dāng)中有沒(méi)有屬性是加了CacheParam注解的,如果有就獲取值。 

@Component
public class RedisKeyGenerator {
    @Autowired
    HttpServletRequest request;
 
    public String getKey(ProceedingJoinPoint joinPoint) throws IllegalAccessException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 獲取方法
        Method method = methodSignature.getMethod();
        // 獲取參數(shù)
        Object [] args = joinPoint.getArgs();
        // 獲取注解
        final Parameter [] parameters = method.getParameters();
        CacheLock cacheLock =  method.getAnnotation(CacheLock.class);
        String prefix = cacheLock.prefix();
        StringBuilder sb = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();
        sb2.append(".").append(joinPoint.getTarget().getClass().getName()).append(".").append(method.getName());
        for(int i = 0;i<args.length;i++){
            CacheParam cacheParam = parameters[i].getAnnotation(CacheParam.class);
            if(cacheParam == null){
                continue;
            }
            sb.append(cacheLock.delimiter()).append(args[i]);
        }
        // 如果方法參數(shù)沒(méi)有CacheParam注解 從參數(shù)類的內(nèi)部嘗試獲取
        if(StringUtils.isEmpty(sb.toString())){
            for(int i = 0;i< parameters.length;i++){
                final Object object = args[i];
                Field [] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    final CacheParam annotation = field.getAnnotation(CacheParam.class);
                    if(annotation==null){
                        continue;
                    }
                    field.setAccessible(true);
                    sb.append(cacheLock.delimiter()).append(field.get(object));
                }
            }
        }
        return prefix+sb2+sb;
 
    }
}

創(chuàng)建Redis工具類

以下工具類來(lái)自引用DDKK.com。

@Component
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLockHelper {
    private static final String DELIMITER = "|";
 
    /**
     * 如果要求比較高可以通過(guò)注入的方式分配
     */
    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);
 
    private final StringRedisTemplate stringRedisTemplate;
 
    @Autowired
    public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
 
    /**
     * 獲取鎖(存在死鎖風(fēng)險(xiǎn))
     *
     * @param lockKey lockKey
     * @param value   value
     * @param time    超時(shí)時(shí)間
     * @param unit    過(guò)期單位
     * @return true or false
     */
    public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
        return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
    }
 
    /**
     * 獲取鎖
     *
     * @param lockKey lockKey
     * @param uuid    UUID
     * @param timeout 超時(shí)時(shí)間
     * @param unit    過(guò)期單位
     * @return true or false
     */
    public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {
        final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
        boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid,timeout,TimeUnit.SECONDS);
        if (success) {
 
        } else {
            String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
            final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
            if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
                return true;
            }
        }
        return success;
    }
    /**
     * @see <a  rel="external nofollow" >Redis Documentation: SET</a>
     */
    public void unlock(String lockKey, String value) {
        unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
    }
 
    /**
     * 延遲unlock
     *
     * @param lockKey   key
     * @param uuid      client(最好是唯一鍵的)
     * @param delayTime 延遲時(shí)間
     * @param unit      時(shí)間單位
     */
    public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
        if (StringUtils.isEmpty(lockKey)) {
            return;
        }
        if (delayTime <= 0) {
            doUnlock(lockKey, uuid);
        } else {
            EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
        }
    }
 
    /**
     * @param lockKey key
     * @param uuid    client(最好是唯一鍵的)
     */
    private void doUnlock(final String lockKey, final String uuid) {
        String val = stringRedisTemplate.opsForValue().get(lockKey);
        final String[] values = val.split(Pattern.quote(DELIMITER));
        if (values.length <= 0) {
            return;
        }
        if (uuid.equals(values[1])) {
            stringRedisTemplate.delete(lockKey);
        }
    }
 
}

創(chuàng)建Student類

public class Student {
    @CacheParam
    private String name;
    @CacheParam
    private Integer age;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
}

創(chuàng)建AOP切面類

注意下邊我注釋掉的一行代碼,如果加上了以后你就看不到防止重復(fù)提交的提示了,下邊的代碼和單機(jī)環(huán)境的思路是一樣的,只不過(guò)加鎖用的是Redis。

@Aspect
@Component
public class Lock {
    @Autowired
    private RedisLockHelper redisLockHelper;
    @Autowired
    private RedisKeyGenerator redisKeyGenerator;
    @Pointcut("execution(* com.my.controller.*.*(..))&&@annotation(com.my.annotation.CacheLock)")
    public void pointCut(){}
 
    @Around("pointCut()")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws IllegalAccessException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);
        if (StringUtils.isEmpty(cacheLock.prefix())) {
            throw new RuntimeException("鎖的前綴不能為空");
        }
 
        int expireTime = cacheLock.expire();
        TimeUnit timeUnit = cacheLock.timeUnit();
        String key = redisKeyGenerator.getKey(joinPoint);
        System.out.println(key);
        String value = UUID.randomUUID().toString();
        Object object;
        try {
            final boolean tryLock = redisLockHelper.lock(key,value,expireTime,timeUnit);
            if(!tryLock){
                throw new RuntimeException("重復(fù)提交");
            }
            try {
                object = joinPoint.proceed();
            }catch (Throwable e){
                throw new RuntimeException("系統(tǒng)異常");
            }
        } finally {
           // redisLockHelper.unlock(key,value);
        }
        return object;
    }
}

創(chuàng)建Controller 

@RestController
@RequestMapping("/student")
public class StudentController {
    @RequestMapping("/get-student")
    @CacheLock(prefix = "stu2",expire = 5,timeUnit = TimeUnit.SECONDS)
    public String getStudent(){
 
        return  "張三";
    }
 
    @RequestMapping("/get-student2")
    @CacheLock(prefix = "stu2",expire = 5,timeUnit = TimeUnit.SECONDS)
    public String getStudent2(Student student){
        return  "張三";
    }
}

調(diào)用get-student測(cè)試

  • 第一次調(diào)用

  • 第二次調(diào)用 

調(diào)用get-student2測(cè)試 

  • 第一次調(diào)用

  • 第二次調(diào)用

最后,上邊的key生成還有待商榷,分布式環(huán)境下key的生成并不是一個(gè)輕松的問(wèn)題。本文的內(nèi)容僅建議作為學(xué)習(xí)使用。

以上就是SpringBoot中防止接口重復(fù)提交的有效方法的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot接口重復(fù)提交的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 解決MyEclipse中的Building workspace問(wèn)題的三個(gè)方法

    解決MyEclipse中的Building workspace問(wèn)題的三個(gè)方法

    這篇文章主要介紹了解決MyEclipse中的Building workspace問(wèn)題的三個(gè)方法,需要的朋友可以參考下
    2015-11-11
  • JAVA堆排序算法的講解

    JAVA堆排序算法的講解

    這篇文章主要介紹了JAVA堆排序算法的知識(shí)點(diǎn),文中代碼非常詳細(xì),配合上圖片講解,幫助大家更好的參考和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • java選擇框、單選框和單選按鈕

    java選擇框、單選框和單選按鈕

    本文給大家介紹的是java中選擇框、單選框和單選按鈕的操作方法,十分的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下。
    2015-06-06
  • SpringBoot中配置多數(shù)據(jù)源的方法詳解

    SpringBoot中配置多數(shù)據(jù)源的方法詳解

    這篇文章主要為大家詳細(xì)介紹了SpringBoot中配置多數(shù)據(jù)源的方法的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-02-02
  • Java設(shè)計(jì)模式之外觀模式示例詳解

    Java設(shè)計(jì)模式之外觀模式示例詳解

    外觀模式為多個(gè)復(fù)雜的子系統(tǒng),提供了一個(gè)一致的界面,使得調(diào)用端只和這個(gè)接口發(fā)生調(diào)用,而無(wú)須關(guān)系這個(gè)子系統(tǒng)內(nèi)部的細(xì)節(jié)。本文將通過(guò)示例詳細(xì)為大家講解一下外觀模式,需要的可以參考一下
    2022-03-03
  • 打造一款代碼命名工具的詳細(xì)教程

    打造一款代碼命名工具的詳細(xì)教程

    這篇文章主要介紹了來(lái),我們一起打造一款代碼命名工具,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-09-09
  • Mybatis設(shè)置sql打印日志的多種方法

    Mybatis設(shè)置sql打印日志的多種方法

    這篇文章主要介紹了Mybatis設(shè)置sql打印日志,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-08-08
  • Mybatis如何分割字符串

    Mybatis如何分割字符串

    這篇文章主要介紹了Mybatis如何分割字符串問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • java中對(duì)字符串每個(gè)字符統(tǒng)計(jì)的方法

    java中對(duì)字符串每個(gè)字符統(tǒng)計(jì)的方法

    java中對(duì)字符串每個(gè)字符統(tǒng)計(jì)的方法,需要的朋友可以參考一下
    2013-03-03
  • java遠(yuǎn)程調(diào)用接口、URL的方式代碼

    java遠(yuǎn)程調(diào)用接口、URL的方式代碼

    我們都知道接口有自己本地的,也有遠(yuǎn)程別人寫好的,而調(diào)用遠(yuǎn)程接口的就需要使用遠(yuǎn)程調(diào)用啦,這篇文章主要給大家介紹了關(guān)于java遠(yuǎn)程調(diào)用接口、URL的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-11-11

最新評(píng)論