日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java接口防刷策略(自定义注解实现)

發布時間:2024/1/1 java 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java接口防刷策略(自定义注解实现) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

本文一定要看完,前部分為邏輯說明及簡單實現,文章最后有最終版解決方案(基于lua腳本),因為前部分是防君子不防小人,無法抵擋for循環調用。

目的

  • 短信發送及短信驗證碼校驗接口防刷
    一方面防止用戶循環調用刷短信驗證碼
    另一方面防止用戶循環調用測短信驗證碼(一般短信驗證碼為6位純數字,一秒鐘上百次調用,如果不做限制很快就能試出來了)
  • 很多接口需要防止前端重復調用
    誤操作多次點擊,不屬于攻擊類型,正常用戶經常會觸發的,例如信息發布可能前端限制未做好,誤點擊了多次,這種情況實際上應該只記錄第一次的,后續的不應該繼續操作數據庫。
  • 極端的情況
    可能很多接口一天或者很長時間只能調用一次(類似簽到?個人想法是盡量不讓數據到了數據庫層再拋異常)

解決措施

利用Spring AOP理念,自定義注解實現接口級訪問次數限制

訪問次數記錄使用Redis存儲,Redis的過期機制很適合當前場景,而且可以在更大程度上提升性能

  • 定義注解

    package com.cong.core.rate;import java.lang.annotation.Documented; 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) @Documented public @interface RateLimit {/** 周期,單位是秒 */int cycle() default 5;/** 請求次數 */int number() default 1;/** 默認提示信息 */String msg() default "請勿重復點擊"; }

    默認是5秒調用一次,現在網上一大堆腳本,貼吧發帖跟帖自動化,實際上打字點擊發帖的正常頻率也不會超過2秒一次吧,但是機器很容易就超過這個速度了,在一定程度上也可以限制這種情況的發生。
    接口級限制,所以當前注解只作用在方法上。

  • 定義接口訪問頻次限制接口

    package com.cong.core.rate;public interface RateLimitService {/*** 接口頻次限制校驗* * @param ip* 客戶端IP* @param uri* 請求接口名* @param rateLimit* 限制頻次信息* @return* @author single-聰* @date 2020年6月1日* @version 1.6.1*/Boolean limit(String ip, String uri, RateLimit rateLimit); }

    因為Interceptor攔截器最終返回值是true或false,所以當前接口返回值為boolean類型。
    關于參數,可以設法獲取設備Mac地址,對于某些明顯是攻擊的IP及設備封禁。

  • RateLimitService接口默認實現類

    package com.cong.core.rate;import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.RedisTemplate; import lombok.extern.slf4j.Slf4j;@Slf4j public class DefaultRateLimitServiceImpl implements RateLimitService {private RedisTemplate<String, Integer> redisTemplate;public void setRedisTemplate(RedisTemplate<String, Integer> redisTemplate) {this.redisTemplate = redisTemplate;}@Overridepublic Boolean limit(String ip, String uri, RateLimit rateLimit) {log.info("默認的實現,請自定義實現類覆蓋當前實現");String key = "rate:" + ip + ":" + uri;// 緩存中存在key,在限定訪問周期內已經調用過當前接口if (redisTemplate.hasKey(key)) {// 訪問次數自增1redisTemplate.opsForValue().increment(key, 1);// 超出訪問次數限制if (redisTemplate.opsForValue().get(key) > rateLimit.number()) {return false;}// 未超出訪問次數限制,不進行任何操作,返回true} else {// 第一次設置數據,過期時間為注解確定的訪問周期redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);}return true;} }

    默認實現類中使用Redis作為存儲策略,加上下面的Bean注入策略你就可以自定義接口實現類使用自己的存儲方式了。

  • Bean配置

    package com.cong.core.rate;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate;@Configuration public class RateLimitBeanConfig {@Autowiredprivate RedisTemplate<String, Integer> redisTemplate;@Bean@ConditionalOnMissingBean(RateLimitService.class)public RateLimitService rateLimitService() {DefaultRateLimitServiceImpl defaultRateLimitServiceImpl = new DefaultRateLimitServiceImpl();defaultRateLimitServiceImpl.setRedisTemplate(redisTemplate);return defaultRateLimitServiceImpl;} }

    此配置意為讓用戶編寫接口實現類覆蓋默認實現。

  • 定義攔截器

    package com.cong.core.rate;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;@Component public class RateLimitInterceptor extends HandlerInterceptorAdapter {private RateLimitService rateLimitService;public void setRateLimitService(RateLimitService rateLimitService) {this.rateLimitService = rateLimitService;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {// 判斷請求是否屬于方法的請求if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 獲取方法中的注解,看是否有該注解RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);if (rateLimit == null) {return true;}// 請求IP地址String ip = request.getRemoteAddr();// 請求url路徑String uri = request.getRequestURI();return rateLimitService.limit(ip, uri, rateLimit);}return true;} }

    重點,只對添加了@RateLimit注解的接口進行訪問頻次限制。

  • 配置攔截器

    package com.cong.config;import com.cong.core.rate.RateLimitInterceptor; import com.cong.core.rate.RateLimitService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;@Configuration public class WebMvcConfig extends WebMvcConfigurationSupport {@Autowiredprivate RateLimitService rateLimitService;@Overrideprotected void addInterceptors(InterceptorRegistry registry) {RateLimitInterceptor rateLimitInterceptor = new RateLimitInterceptor();rateLimitInterceptor.setRateLimitService(rateLimitService);registry.addInterceptor(rateLimitInterceptor);} }

    文中的很多地方接口使用set方式注入,是為了防止接口注入失敗,報錯空指針異常(應該很多人遇到過)。

使用

  • 使用注解

    @RestController @RequestMapping("open/public") public class OpenPublicController {@RateLimit(number = 2, cycle = 10)@PostMapping("rate")public void rate() {throw new VersionException();} }

上述注解的作用是10秒內可以請求兩次,其他的請求就不處理了,VersionException是我自定義的異常,用于提示用戶升級新版本,在2次內返回用戶正常提示信息:

{"state": 1000,"msg": "請升級到新版本","data": null }

超出限制后無返回信息(RateLimitInterceptor攔截器中返回的是false,直接結束了這次請求,同時未向前端返回任何信息,實際開發中應該會返回提示信息,補充內容中解決這個問題)

補充

關于攔截器中接口調用超出限制頻次的自定義返回:

package com.cong.core.rate;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;import com.cong.core.support.ReturnData; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import com.fasterxml.jackson.databind.ObjectMapper;@Component public class RateLimitInterceptor extends HandlerInterceptorAdapter {private RateLimitService rateLimitService;public void setRateLimitService(RateLimitService rateLimitService) {this.rateLimitService = rateLimitService;}private ObjectMapper objectMapper;public void setObjectMapper(ObjectMapper objectMapper) {this.objectMapper = objectMapper;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {// 判斷請求是否屬于方法的請求if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 獲取方法中的注解,看是否有該注解RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);if (rateLimit == null) {return true;}// 請求IP地址String ip = request.getRemoteAddr();// 請求url路徑String uri = request.getRequestURI();if (!rateLimitService.limit(ip, uri, rateLimit)) {response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(new ReturnData(rateLimit.msg())));response.setStatus(HttpStatus.OK.value());return false;}}return true;} }

注入ObjectMapper 需要set一下。
ReturnData是封裝的返回值信息,前端可以根據這個給用戶友好的提示,后端也可以自定義提示信息。

@Data @NoArgsConstructor public class ReturnData {private Integer state = 1000;private String msg;private Object data;public ReturnData(String msg) {super();this.msg = msg;} }

不過建議是自定義失敗處理器,這樣所有的錯誤統一走失敗處理器,更方便以后的代碼維護,這里只是為了實現接口頻次限制,其他的這里就不描述了。

超頻之后返回值:

接口名注解返回值
open/public/rate@RateLimit(number = 4, cycle = 10){ "state": 1000, "msg": "請勿重復點擊","data": null}
open/public/rate1@RateLimit(number = 4, cycle = 10, msg = “調用頻次過高”){ "state": 1000, "msg": "調用頻次過高","data": null}

至此即實現接口訪問頻次限制以及自定義返回提示信息。

我目前的服務端開發用戶信息是無狀態的Token,基于JWT,使用的Security框架(前段時間的文章有一組筆記),用戶權限校驗是單獨實現的。

關于性能:
使用了當前注解的接口請求耗時會長一點,我的Redis在一臺學生機上,而且跨省,耗時大概增加了40ms,本地的話大概也就20ms左右,如果對性能還有要求的話建議使用lua腳本。

建議

  • 定義IP過濾器
    在使用Redis的情況下,可以定義IP過濾器,計算指定IP請求速率,在上文中更多的是防止重復提交,但是對于文章開始所說的超高頻次的調用并沒有處理,建議在過濾器中攔截所有請求,每個IP對于單獨接口在訪問周期內超出限制之后將當前IP限制一段時間(是限制所有請求還是當前請求自行決定)

  • 基于IP過濾器統計接口訪問次數
    在IP過濾器中借助Redis計算接口訪問次數,每天同步一次,對于后面的服務擴展,接口限流等還是很有好處的。

歡迎留言,共同探討。

lua腳本

自定義接口實現類:

package com.cong.service.impl;import java.util.Collections; import com.cong.core.rate.RateLimit; import com.cong.core.rate.RateLimitService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import lombok.extern.slf4j.Slf4j;@Slf4j @Service public class RateLimitServiceImpl implements RateLimitService {@Autowiredprivate RedisTemplate<String, Integer> redisTemplate;private static final String RATE_LIMIT_LOCK_LUA_SCRIPT = "local limit = tonumber(ARGV[1])"// 限制次數+ "local expire_time = ARGV[2]"// 過期時間+ "local result = redis.call('SETNX',KEYS[1],1);"// key不存在時設置value為1,返回1、否則返回0+ "if result == 1 then"// 返回值為1,key不存在此時需要設置過期時間+ " redis.call('expire',KEYS[1],expire_time)"// 設置過期時間+ " return 1 "// 返回1+ "else"// key存在+ " if tonumber(redis.call('GET', KEYS[1])) >= limit then"// 判斷數目比對+ " return 0"// 如果超出限制返回0+ " else" // + " redis.call('incr', KEYS[1])"// key自增+ " return 1 " // 返回1+ " end "// 結束+ "end";// 結束@Overridepublic Boolean limit(String ip, String uri, RateLimit rateLimit) {String key = "custom:rate:" + ip + ":" + uri;// 指定 lua 腳本,并且指定返回值類型DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RATE_LIMIT_LOCK_LUA_SCRIPT, Long.class);// 參數一:redisScript,參數二:key列表,參數三:arg(可多個)Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), rateLimit.number(),rateLimit.cycle());log.info("lua腳本返回值為:[{}]", result);if (result == 0) {return false;}return true;} }

此處使用的是直接編寫lua腳本,當然也可以編寫lua文件。這樣可以確保限制生效,默認的實現在for循環的調用情況下因為網絡開銷會造成并不能準確限制請求,我的測試中兩次請求間隔50ms沒問題,但是10ms以內限制極易不生效(鎖)。

總結

以上是生活随笔為你收集整理的Java接口防刷策略(自定义注解实现)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。