當前位置:
首頁 >
前端技术
> javascript
>内容正文
javascript
Spring自定义注解+redis实现接口限流
生活随笔
收集整理的這篇文章主要介紹了
Spring自定义注解+redis实现接口限流
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
在實際開發中,有時候我們需要對某些接口進行限流,防止有人惡意攻擊或者是因為某些接口自身的原因,比如發短信接口,IO處理的接口。
這里我們通過自定義一個注解,并利用Spring的AOP攔截器功能來實現限流的功能。限流需要用到redis。
代碼:
Limit.java
這里我們有兩種限流類型,一種是根據接口本身來進行限流,一種是根據ip來進行限流
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Limit { ?// 資源名稱,用于描述接口功能String name() default ""; ?// 資源 keyString key() default ""; ?// key prefixString prefix() default ""; ?// 時間的,單位秒int period() default 60; ?// 限制訪問次數int count() default 60; ?// 限制類型LimitType limitType() default LimitType.IP; ? ?/*** 使用實例:* 測試限流注解,下面配置說明該接口 60秒內最多只能訪問 10次,保存到redis的鍵名為 limit_test,* 即 prefix + "_" + key,也可以根據 IP 來限流,需指定limitType = LimitType.IP*/ // ? @Limit(key = "test", period = 60, count = 10, name = "resource", prefix = "limit") // ? @GetMapping("/test") // ? public int testLimiter() { // ? ? ? return ATOMIC_INTEGER.incrementAndGet(); // ? } ? } ?LimitAspect.java
@Aspect @Component public class LimitAspect { ?private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class); ?@Autowiredprivate RedisTemplate<String, Object> limitRedisTemplate; ? ?@Pointcut("@annotation(com.yfy.annotation.Limit)")public void pointcut() {// do nothing} ?@Around("pointcut()")public Object around(ProceedingJoinPoint point) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); ?MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Limit limitAnnotation = method.getAnnotation(Limit.class);LimitType limitType = limitAnnotation.limitType();String name = limitAnnotation.name();String key;int limitPeriod = limitAnnotation.period();int limitCount = limitAnnotation.count();switch (limitType) {case IP:key = IPUtils.getIpAddr(request);break;case CUSTOMER:key = limitAnnotation.key();break;default:key = StringUtils.upperCase(method.getName());}ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix() + "_", key + "_" + request.getRequestedSessionId()));String luaScript = buildLuaScript();RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);logger.info("第{}次訪問key為 {},描述為 [{}] 的接口", count, keys, name);if (count != null && count.intValue() <= limitCount) {return point.proceed();} else {throw new LimitAccessException("接口訪問超出頻率限制");} ?} ?/*** 限流腳本* 調用的時候不超過閾值,則直接返回并執行計算器自加。** @return lua腳本*/private String buildLuaScript() {return "local c" +"\nc = redis.call('get',KEYS[1])" +"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +"\nreturn c;" +"\nend" +"\nc = redis.call('incr',KEYS[1])" +"\nif tonumber(c) == 1 then" +"\nredis.call('expire',KEYS[1],ARGV[2])" +"\nend" +"\nreturn c;";} ? }IPUtils.java
public class IPUtils { ?private static final String UNKNOWN = "unknown"; ?protected IPUtils(){ ?} ?/*** 獲取IP地址* 使用 Nginx等反向代理軟件, 則不能通過request.getRemoteAddr()獲取IP地址* 如果使用了多級反向代理的話,X-Forwarded-For的值并不止一個,而是一串IP地址,X-Forwarded-For中第一個非 unknown的有效IP字符串,則為真實IP地址*/public static String getIpAddr(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;} ? }Report.java
@Data public class Report ?implements Serializable { ?private Integer status;private Object data;private Object notice;private Object msg; }ExceptionTemplet.java
public interface ExceptionTemplet {Report report(); }BaseServiceException.java
public abstract class BaseServiceException extends RuntimeException implements ExceptionTemplet {private Report report;public BaseServiceException(String message) {super(message);}public BaseServiceException report(Report report){this.report = report;return this;}; ?@Overridepublic Report report() {return report;} }LimitAccessException.java
public class LimitAccessException extends BaseServiceException { ?private static final long serialVersionUID = -3608667856397125671L; ?public LimitAccessException(String message) {super(message);}@Overridepublic Report report() {return ReportFactory.C_1404_CLIENT_REQUEST_DATAERROR.error(getMessage());} }測試:
我們寫一個controller接口
? ?@GetMapping("/limit")@ResponseBody@Limit(key = "test", period = 60, count = 10, name = "resource", prefix = "limit")public String testLimit() {return "success";}該接口中的注解表明該接口在60秒內,同一個ip地址最多只能訪問10次,如果訪問超過10次,則拋出異常。
對于該異常,如果我們希望給用戶友好的提示,可以利用Spring的全局異常處理類來對異常進行特殊處理。
Report類為前后端分離中與前端自定義的返回值類
ControllerExceptionAdvice.java
@RestControllerAdvice public class ControllerExceptionAdvice {private Logger logger = LoggerFactory.getLogger(ControllerExceptionAdvice.class); ?@ExceptionHandler(value = LimitAccessException.class)@ResponseBodypublic Report handle(LimitAccessException e) {return e.report();} }?
總結
以上是生活随笔為你收集整理的Spring自定义注解+redis实现接口限流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用netty实现一个类似于微信的聊天功
- 下一篇: Activiti工作流入门