SpringBoot が Aop+Redis を組み合わせてインターフェイスの繰り返し送信を防ぐ方法

王林
リリース: 2023-05-31 10:40:06
転載
1253 人が閲覧しました

実際の開発プロジェクトでは、外部に公開されたインターフェイスが多くのリクエストに直面することがよくあります。冪等性の概念を説明しましょう: 複数の実行の影響は 1 回の実行の影響と同じです。この意味によると、最終的な意味は、データベースへの影響は 1 回限りであり、繰り返し処理することはできないということです。べき等性を確認する方法には通常、次の方法が含まれます:

1. データベース内に一意のインデックスを確立して、最終的に 1 つのデータのみがデータベースに挿入されるようにします。

2. トークンのメカニズム. 各インターフェイス リクエストの前にトークンを取得し、次回リクエストのヘッダー本文にこのトークンを追加します. 検証はバックグラウンドで実行されます. 検証に合格すると、トークンは次へ リクエストごとに再度トークンの判定を行います。

3. 悲観的ロックまたは楽観的ロック. 悲観的ロックを使用すると、他の SQL が更新のたびにデータを更新できないようにできます (データベース エンジンが innodb の場合、テーブル全体がロックされないように、選択条件は一意のインデックスである必要があります)。 )

4. 最初にクエリを実行してから判断します。まず、データベースにクエリを実行してデータが存在するかどうかを確認します。データが存在する場合は、リクエストが行われたことが証明され、リクエストは直接拒否されます。存在しない場合は初入荷であることを証明し、そのまま公開します。

インターフェイスの繰り返しの送信を防止する必要があるのはなぜですか?
新しいデータ インターフェイスや支払いインターフェイスなど、一部の機密性の高い操作インターフェイスでは、ユーザーが送信ボタンを複数回誤ってクリックすると、これらのインターフェイスが複数回要求され、最終的にシステム例外が発生する可能性があります。

フロントエンドはどのように制御できますか?
フロントエンドは js を通じて制御できます。ユーザーが送信ボタンをクリックすると、##1. ボ​​タンが数秒間クリックできないように設定されます。##2. ボタンがクリックされた後、読み込みが行われます。プロンプト ボックスがポップアップ表示され、インターフェイス リクエストが返されるまで再度クリックする必要はありません。その後
3. ボタンをクリックして新しいページにジャンプします。

ただし、ユーザーの動作を決して信用しないことを覚えておいてください。ユーザーがどのような奇妙な操作を行うかはわかりません。そのため、最も重要なことは、バックエンドで処理する必要があるということです。

インターセプト処理に aop redis を使用する

1. アスペクト クラスのRepeatSubmitAspectを作成します

実装プロセス: インターフェイス リクエストの後、トークン リクエスト パスをキー値として使用して、redis からデータを読み取ります。キーが見つかると、キーが繰り返し送信されたことが証明され、その逆も同様です。繰り返し送信されない場合は、直接解放され、キーが Redis に書き込まれ、一定期間内に期限切れになるように設定されます (ここでは有効期限を 5 秒に設定しています)

#従来の Web プロジェクトでは、繰り返しの送信を防ぐために、バックエンドで一意の送信トークン (uuid) を生成し、サーバーに保存します。ページがリクエストを開始すると、そのトークンが送信されます。バックエンドは、リクエストの一意性を保証するためにリクエストを検証した後にトークンを削除します。

ただし、アピール方法はフロントエンドとバックエンドの両方に変更が必要で、プロジェクトの初期段階であれば実現可能ですが、プロジェクト後半になると多くの機能が実装され、大規模な変更は不可能です。


アイデア
1. 注釈 @NoRepeatSubmit をカスタマイズして、コントローラーで送信されたすべてのリクエストをマークします

2. @NoRepeatSubmit でマークされたすべてのメソッドを AOP を通じてインターセプトします

3. ビジネス メソッドを実行します前に、現在のユーザーのトークンまたは JSessionId の現在のリクエストアドレスを一意のキーとして取得し、redis 分散ロックを取得します。このとき同時取得を行うと 1 つのスレッドのみが取得できます。
4. ビジネスが実行されたら、ロックを解放します。

Redis 分散ロックについて
Redis を使用するのは負荷分散デプロイメントのためです。スタンドアロン プロジェクトの場合は、ローカルのスレッドセーフ キャッシュが使用されます。 Redis の代わりに使用できます

Code
カスタム アノテーション

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

/**
 * @ClassName NoRepeatSubmit
 * @Description 这里描述
 * @Author admin
 * @Date 2021/3/2 16:16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {

    /**
     * 设置请求锁定时间
     *
     * @return
     */
    int lockTime() default 10;

}
ログイン後にコピー

AOP
package com.hongkun.aop;

/**
 * @ClassName RepeatSubmitAspect
 * @Description 这里描述
 * @Author admin
 * @Date 2021/3/2 16:15
 */

import com.hongkun.until.ApiResult;
import com.hongkun.until.Result;
import com.hongkun.until.RedisLock;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author liucheng
 * @since 2020/01/15
 * 防止接口重复提交
 */
@Aspect
@Component
public class RepeatSubmitAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);

    @Autowired
    private RedisLock redisLock;

    @Pointcut("@annotation(noRepeatSubmit)")
    public void pointCut(NoRepeatSubmit noRepeatSubmit) {
    }

    @Around("pointCut(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {

        int lockSeconds = noRepeatSubmit.lockTime();

        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();

        Assert.notNull(request, "request can not null");

        // 此处可以用token或者JSessionId
        String token = request.getHeader("token");
        String path = request.getServletPath();
        String key = getKey(token, path);
        String clientId = getClientId();

        boolean isSuccess = redisLock.lock(key, clientId, lockSeconds,TimeUnit.SECONDS);
        LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);

        if (isSuccess) {
            LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
            // 获取锁成功
            Object result;
            try {
                // 执行进程
                result = pjp.proceed();
            } finally {
                // 解锁
                redisLock.unlock(key, clientId);
                LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
            }

            return result;

        } else {
            // 获取锁失败,认为是重复提交的请求
            LOGGER.info("tryLock fail, key = [{}]", key);
            return ApiResult.success(200, "重复请求,请稍后再试", null);
        }

    }

    private String getKey(String token, String path) {
        return "00000"+":"+token + path;
    }

    private String getClientId() {
        return UUID.randomUUID().toString();
    }


}
ログイン後にコピー

以上がSpringBoot が Aop+Redis を組み合わせてインターフェイスの繰り返し送信を防ぐ方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:yisu.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!