この記事では主に、SpringBootプロジェクトがAOPをRedis + Luaスクリプトと組み合わせて使用して、頻繁な悪意のあるアクセスからAPIを保護することを目的とした分散電流制限を実現する方法を示します。
1.Redis構成ファイル
@Configuration public class RedisConfig { @Bean public RedisScript <Long> limitRedisScript(){ DefaultRedisScript redisScript = new DefaultRedisScript <>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource( "scripts / redis / limit.lua"))); redisScript.setResultType(Long.class); redisScriptを返します。 } }
2.Luaスクリプト
- 添え字は1つの ローカルキーから始まります= KEYS [1] local now = tonumber(ARGV [1]) local ttl = tonumber(ARGV [2]) local expired = tonumber(ARGV [3]) -最大訪問量 local max = tonumber (ARGV [4]) -古いデータをクリアします -指定されたスコア範囲内のすべての要素を削除します 。expiredは期限切れのスコアです -現在のタイムミリ秒-タイムアウトミリ秒に従って、期限切れの時間を取得しますredis .call( 'zremrangebyscore'、key 、0、expired) -zset内の要素の現在の数を取得します local current = tonumber(redis.call( 'zcard'、key)) local next = current + 1 if next> max then- 現在の制限サイズに到達し、0を返します 0を返す。 そうでない -値の要素を追加し、スコアZSETに現在のタイムスタンプされ、[値、スコア] redis.call(「zadd」、キー、今、今) - -Reset ZSETの有効期限あなたが訪問するたびに、ミリ秒単位 redis.call( "pexpire"、key、ttl)は 次の 終わりを返します
3.注釈
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { long DEFAULT_REQUEST = 10; / ** * maxリクエストの最大数 * / @AliasFor( "max") long value()default DEFAULT_REQUEST ; / ** *リクエストの最大の最大数 * / @AliasFor( "値") 長いMAX()デフォルトDEFAULT_REQUEST; / ** *現在制限キー * / 文字列のキー()デフォルト""; / ** *タイムアウト期間、デフォルト1分 * / long timeout()デフォルト1; / ** *タイムアウト単位、デフォルト分 * / TimeUnit timeUnit()デフォルトTimeUnit.MINUTES; }
4.麺を切る
@Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) @ Slf4j public class LimiteAspect { private final static String SEPARATOR = ":"; プライベート最終静的文字列REDIS_LIMIT_KEY_PREFIX = "limit:"; プライベートファイナルStringRedisTemplatestringRedisTemplate; プライベートファイナルRedisScript <Long> limitRedisScript; @Pointcut( "@ annotation(com.juejueguai.springbootdemoratelimitredis.annotation.RateLimiter)") public void rateLimit(){ } @Around( "rateLimit()") public Object pointcut(ProceedingJoinPoint point)throws Throwable { MethodSignature sign =(MethodSignature )point.getSignature(); メソッドmethod = Signature.getMethod(); //AnnotationUtils.findAnnotationを介してRateLimiterアノテーションを 取得RateLimiterrateLimiter = AnnotationUtils.findAnnotation(method、RateLimiter.class); if(rateLimiter!= null){ String key = rateLimiter.key(); // デフォルトでは、クラス名+メソッド名が現在の制限のキープレフィックスとして使用されますif(StrUtil.isBlank(key)){ key = method.getDeclaringClass()。getName()+ StrUtil.DOT + method.getName() ; } //最後に現在の制限キーはプレフィックス+ IPアドレスです // TODO:現時点では、LANでのマルチユーザーアクセスの状況を考慮する必要があるため、後でキーにメソッドパラメータを追加する方が合理的です。 key = key + SEPARATOR + IpUtil.getIpAddr(); long max = rateLimiter .max(); long timeout = rateLimiter.timeout(); TimeUnit timeUnit = rateLimiter.timeUnit(); boolean Limited = shouldLimited(key、max、timeout、timeUnit); if(limited){ throw new RuntimeException( "Hand speed is too fast、slow儿吧〜 "); } } return point.proceed(); } private boolean shouldLimited(String key、long max、long timeout、TimeUnit timeUnit){ //最終的なキー形式は次のとおりです。//limit:custom key:IP //制限:クラス名。メソッド名:IP キー= REDIS_LIMIT_KEY_PREFIX +キー; //統一使用単位ミリ秒 長ttl = timeUnit.toMillis(timeout); //現在の時刻(ミリ秒) long now = Instant.now()。toEpochMilli(); long expired = now-ttl; //これはStringに変換する必要があることに注意してください。変換しないと、エラーが報告されますjava.lang.Longをjava.langにキャストできません。 String Long executeTimes = stringRedisTemplate .execute(limitRedisScript、Collections.singletonList(key)、now + ""、ttl + ""、expired + ""、max + ""); if(executeTimes!= null){ if(executeTimes = = 0){ log .error( "[{}]単位時間あたり{}ミリ秒以内にアクセス制限に達しました。現在のインターフェイス制限は{}"、key、ttl、max); return true; } else { log .info( "[{}]単位時間{}ミリ秒で{}回アクセス"、key、ttl、executeTimes); falseを返します。 } } falseを返します。 } }
5.グローバル例外
@ Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public Dict handler(RuntimeException ex){ return Dict.create()。set( "msg"、ex.getMessage()); } }
6、IPツール
@ Slf4j public class IpUtil { private final static String UNKNOWN = "unknown"; private final static int MAX_LENGTH = 15; / ** * IPアドレスを取得する * Nginxなどのリバースプロキシソフトウェアを使用している場合、リクエストを通じてIPを取得することはできません.getRemoteAddr()アドレス *マルチレベルリバースプロキシが使用されている場合、X-Forwarded-Forの値は1つだけでなく、IPアドレスの文字列です。X-Forwarded-Forの最初の不明な有効なIP文字列真のIPアドレス * / public static String getIpAddr(){ HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())。getRequest(); String ip = null; try { ip = request.getHeader( "x-forwarded-for "); if(StrUtil.isEmpty(ip)|| UNKNOWN.equalsIgnoreCase(ip)){ ip = request.getHeader( "Proxy-Client-IP"); } if(StrUtil.isEmpty(ip)|| ip.length()== 0 || UNKNOWN.equalsIgnoreCase(ip)){ ip = request.getHeader( "WL-Proxy-Client-IP"); } if(StrUtil.isEmpty(ip)|| UNKNOWN.equalsIgnoreCase(ip)){ ip = request.getHeader( "HTTP_CLIENT_IP"); } if(StrUtil.isEmpty(ip)|| UNKNOWN.equalsIgnoreCase(ip)){ ip = request.getHeader( "HTTP_X_FORWARDED_FOR"); } if(StrUtil.isEmpty(ip)|| UNKNOWN.equalsIgnoreCase(ip)){ ip = request.getRemoteAddr(); } catch(Exception e){ log.error( "IPUtils ERROR"、e); } //プロキシを使用し、最初のIPアドレスを取得 if(!StrUtil.isEmpty(ip)&& ip.length()> MAX_LENGTH){ if(ip.indexOf(StrUtil.COMMA)> 0){ ip = ip.substring(0、ip.indexOf(StrUtil.COMMA)); } } return ip; } }
7.インタフェース試験 @RestController @RequestMapping @ SLF4J パブリッククラスTestController { @RateLimiter(値= 5) @GetMapping( "/ TEST1") 公衆のdict TEST1(){ log.infoは( "[TEST1]が実行されました···"。 ); return Dict.create()。set( "msg"、 "hello、world!")。set( "description"、 "いつも私に会いたくない。信じられない場合は、更新してください。すぐに〜 "); } @GetMapping(" / test2 ") public Dict test2(){ log.info(" [test2]が実行されました... "); return Dict.create()。set(" msg "、 "hello、world!")。set( "description"、 "私はずっとここにいた、私はここにいた、私はここを離れる"); } @RateLimiter(value = 2、key = "カスタムキーをテストする」) @GetMapping( "/ test3") public Dict test3(){ log.info( "[test3]が実行されました..."); return Dict.create()。set( "msg"、 "hello、world!")。set( "description"、 "考えないでくださいいつも会いましょう、信じられないならすぐにリフレッシュしてください〜 "); } }
8. JMeter、ストレステストツール