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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

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

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

前言

本文一定要看完,前部分為邏輯說(shuō)明及簡(jiǎn)單實(shí)現(xiàn),文章最后有最終版解決方案(基于lua腳本),因?yàn)榍安糠质欠谰硬环佬∪?#xff0c;無(wú)法抵擋for循環(huán)調(diào)用。

目的

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

解決措施

利用Spring AOP理念,自定義注解實(shí)現(xiàn)接口級(jí)訪問(wèn)次數(shù)限制

訪問(wèn)次數(shù)記錄使用Redis存儲(chǔ),Redis的過(guò)期機(jī)制很適合當(dāng)前場(chǎng)景,而且可以在更大程度上提升性能

  • 定義注解

    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;/** 請(qǐng)求次數(shù) */int number() default 1;/** 默認(rèn)提示信息 */String msg() default "請(qǐng)勿重復(fù)點(diǎn)擊"; }

    默認(rèn)是5秒調(diào)用一次,現(xiàn)在網(wǎng)上一大堆腳本,貼吧發(fā)帖跟帖自動(dòng)化,實(shí)際上打字點(diǎn)擊發(fā)帖的正常頻率也不會(huì)超過(guò)2秒一次吧,但是機(jī)器很容易就超過(guò)這個(gè)速度了,在一定程度上也可以限制這種情況的發(fā)生。
    接口級(jí)限制,所以當(dāng)前注解只作用在方法上。

  • 定義接口訪問(wèn)頻次限制接口

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

    因?yàn)镮nterceptor攔截器最終返回值是true或false,所以當(dāng)前接口返回值為boolean類型。
    關(guān)于參數(shù),可以設(shè)法獲取設(shè)備Mac地址,對(duì)于某些明顯是攻擊的IP及設(shè)備封禁。

  • RateLimitService接口默認(rèn)實(shí)現(xiàn)類

    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("默認(rèn)的實(shí)現(xiàn),請(qǐng)自定義實(shí)現(xiàn)類覆蓋當(dāng)前實(shí)現(xiàn)");String key = "rate:" + ip + ":" + uri;// 緩存中存在key,在限定訪問(wèn)周期內(nèi)已經(jīng)調(diào)用過(guò)當(dāng)前接口if (redisTemplate.hasKey(key)) {// 訪問(wèn)次數(shù)自增1redisTemplate.opsForValue().increment(key, 1);// 超出訪問(wèn)次數(shù)限制if (redisTemplate.opsForValue().get(key) > rateLimit.number()) {return false;}// 未超出訪問(wèn)次數(shù)限制,不進(jìn)行任何操作,返回true} else {// 第一次設(shè)置數(shù)據(jù),過(guò)期時(shí)間為注解確定的訪問(wèn)周期redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);}return true;} }

    默認(rèn)實(shí)現(xiàn)類中使用Redis作為存儲(chǔ)策略,加上下面的Bean注入策略你就可以自定義接口實(shí)現(xiàn)類使用自己的存儲(chǔ)方式了。

  • 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;} }

    此配置意為讓用戶編寫(xiě)接口實(shí)現(xiàn)類覆蓋默認(rèn)實(shí)現(xiàn)。

  • 定義攔截器

    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 {// 判斷請(qǐng)求是否屬于方法的請(qǐng)求if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 獲取方法中的注解,看是否有該注解RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);if (rateLimit == null) {return true;}// 請(qǐng)求IP地址String ip = request.getRemoteAddr();// 請(qǐng)求url路徑String uri = request.getRequestURI();return rateLimitService.limit(ip, uri, rateLimit);}return true;} }

    重點(diǎn),只對(duì)添加了@RateLimit注解的接口進(jìn)行訪問(wèn)頻次限制。

  • 配置攔截器

    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方式注入,是為了防止接口注入失敗,報(bào)錯(cuò)空指針異常(應(yīng)該很多人遇到過(guò))。

使用

  • 使用注解

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

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

{"state": 1000,"msg": "請(qǐng)升級(jí)到新版本","data": null }

超出限制后無(wú)返回信息(RateLimitInterceptor攔截器中返回的是false,直接結(jié)束了這次請(qǐng)求,同時(shí)未向前端返回任何信息,實(shí)際開(kāi)發(fā)中應(yīng)該會(huì)返回提示信息,補(bǔ)充內(nèi)容中解決這個(gè)問(wèn)題)

補(bǔ)充

關(guān)于攔截器中接口調(diào)用超出限制頻次的自定義返回:

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 {// 判斷請(qǐng)求是否屬于方法的請(qǐng)求if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 獲取方法中的注解,看是否有該注解RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);if (rateLimit == null) {return true;}// 請(qǐng)求IP地址String ip = request.getRemoteAddr();// 請(qǐng)求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是封裝的返回值信息,前端可以根據(jù)這個(gè)給用戶友好的提示,后端也可以自定義提示信息。

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

不過(guò)建議是自定義失敗處理器,這樣所有的錯(cuò)誤統(tǒng)一走失敗處理器,更方便以后的代碼維護(hù),這里只是為了實(shí)現(xiàn)接口頻次限制,其他的這里就不描述了。

超頻之后返回值:

接口名注解返回值
open/public/rate@RateLimit(number = 4, cycle = 10){ "state": 1000, "msg": "請(qǐng)勿重復(fù)點(diǎn)擊","data": null}
open/public/rate1@RateLimit(number = 4, cycle = 10, msg = “調(diào)用頻次過(guò)高”){ "state": 1000, "msg": "調(diào)用頻次過(guò)高","data": null}

至此即實(shí)現(xiàn)接口訪問(wèn)頻次限制以及自定義返回提示信息。

我目前的服務(wù)端開(kāi)發(fā)用戶信息是無(wú)狀態(tài)的Token,基于JWT,使用的Security框架(前段時(shí)間的文章有一組筆記),用戶權(quán)限校驗(yàn)是單獨(dú)實(shí)現(xiàn)的。

關(guān)于性能:
使用了當(dāng)前注解的接口請(qǐng)求耗時(shí)會(huì)長(zhǎng)一點(diǎn),我的Redis在一臺(tái)學(xué)生機(jī)上,而且跨省,耗時(shí)大概增加了40ms,本地的話大概也就20ms左右,如果對(duì)性能還有要求的話建議使用lua腳本。

建議

  • 定義IP過(guò)濾器
    在使用Redis的情況下,可以定義IP過(guò)濾器,計(jì)算指定IP請(qǐng)求速率,在上文中更多的是防止重復(fù)提交,但是對(duì)于文章開(kāi)始所說(shuō)的超高頻次的調(diào)用并沒(méi)有處理,建議在過(guò)濾器中攔截所有請(qǐng)求,每個(gè)IP對(duì)于單獨(dú)接口在訪問(wèn)周期內(nèi)超出限制之后將當(dāng)前IP限制一段時(shí)間(是限制所有請(qǐng)求還是當(dāng)前請(qǐng)求自行決定)

  • 基于IP過(guò)濾器統(tǒng)計(jì)接口訪問(wèn)次數(shù)
    在IP過(guò)濾器中借助Redis計(jì)算接口訪問(wèn)次數(shù),每天同步一次,對(duì)于后面的服務(wù)擴(kuò)展,接口限流等還是很有好處的。

歡迎留言,共同探討。

lua腳本

自定義接口實(shí)現(xiàn)類:

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])"// 限制次數(shù)+ "local expire_time = ARGV[2]"// 過(guò)期時(shí)間+ "local result = redis.call('SETNX',KEYS[1],1);"// key不存在時(shí)設(shè)置value為1,返回1、否則返回0+ "if result == 1 then"// 返回值為1,key不存在此時(shí)需要設(shè)置過(guò)期時(shí)間+ " redis.call('expire',KEYS[1],expire_time)"// 設(shè)置過(guò)期時(shí)間+ " return 1 "// 返回1+ "else"// key存在+ " if tonumber(redis.call('GET', KEYS[1])) >= limit then"// 判斷數(shù)目比對(duì)+ " return 0"// 如果超出限制返回0+ " else" // + " redis.call('incr', KEYS[1])"// key自增+ " return 1 " // 返回1+ " end "// 結(jié)束+ "end";// 結(jié)束@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);// 參數(shù)一:redisScript,參數(shù)二:key列表,參數(shù)三:arg(可多個(gè))Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), rateLimit.number(),rateLimit.cycle());log.info("lua腳本返回值為:[{}]", result);if (result == 0) {return false;}return true;} }

此處使用的是直接編寫(xiě)lua腳本,當(dāng)然也可以編寫(xiě)lua文件。這樣可以確保限制生效,默認(rèn)的實(shí)現(xiàn)在for循環(huán)的調(diào)用情況下因?yàn)榫W(wǎng)絡(luò)開(kāi)銷會(huì)造成并不能準(zhǔn)確限制請(qǐng)求,我的測(cè)試中兩次請(qǐng)求間隔50ms沒(méi)問(wèn)題,但是10ms以內(nèi)限制極易不生效(鎖)。

總結(jié)

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

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。