Article directory
-
- summary
- Overall architecture process
- Explanation of technical terms
- technical details
- summary
Summary
Use aop aspect + redis to implement interface current limit, IP limit, and prevent repeated submission
Overall architecture process
Tips: Use aop aspect + redis to implement interface current limit, IP limit, and prevent repeated submission
Interface file
package com.bzfar.config; import com.bzfar.enums.LimitType; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter{ /** * Current limiting key */ String key() default "rate_limit:"; /** * Current limiting time, unit seconds */ int time() default 60; /** * Number of current limits */ int count() default 100; /** * Current limiting type */ LimitType limitType() default LimitType.DEFAULT; }
package com.bzfar.config; import com.aspose.words.net.System.Data.DataException; import com.bzfar.HeadContext; import com.bzfar.enums.LimitType; import com.bzfar.util.RedisUtil; import com.bzfar.utils.IpUtil; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; @Aspect @Component @Slf4j public class RateLimiterAspect { @Autowired private RedisTemplate redisTemplate; @Autowired private RedisUtil redisUtil; @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { String key = rateLimiter.key(); Long time = new Long(rateLimiter.time()); int count = rateLimiter.count(); String combineKey = getCombineKey(rateLimiter, point); String keyCode = key + combineKey.hashCode(); int number = 1; if(redisUtil.hasKey(keyCode)){ number = (Integer)redisUtil.get(keyCode); + + number; } redisUtil.set(keyCode, number, time); if(number > count){ throw new DataException("The access is too frequent, please try again later"); } } @After("@annotation(rateLimiter)") public void doAfter(JoinPoint point, RateLimiter rateLimiter) throws Throwable { String combineKey = getCombineKey(rateLimiter, point); redisTemplate.delete(combineKey); } public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); if (rateLimiter.limitType() == LimitType.IP) { stringBuffer.append(IpUtil.getIp()).append("-"); } if(rateLimiter.limitType() == LimitType.USER){ stringBuffer.append(HeadContext.getToken()).append("-"); } MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); return stringBuffer.toString(); } }
type file
package com.bzfar.enums; import io.swagger.annotations.ApiModel; import lombok.Getter; @ApiModel("current limit type") @Getter public enum LimitType { /** Default policy global traffic restriction */ DEFAULT, /** IP current limit */ IP, /** User ID current limit */ USER }
package com.bzfar.submit; import java.lang.annotation.*; @Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { }
package com.bzfar.submit; import com.bzfar.util.AssertUtil; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; @Component public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request)) { AssertUtil.assertNull("", "Repeated submissions are not allowed, please try again later"); return false; } } return true; } else { return super.preHandle(request, response, handler); } } /** * Verify whether repeated submissions are made by subclasses to implement specific anti-recurring submission rules. * * @param request * @return * @throwsException */ public abstract boolean isRepeatSubmit(HttpServletRequest request) throws Exception; }
ip tool class
package com.bzfar.utils; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Objects; @Slf4j public class IpUtil { // After multiple reverse proxies, there will be multiple separators for IP values. private static final String IP_UTILS_FLAG = ","; // Unknown IP private static final String UNKNOWN = "unknown"; // local IP private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1"; private static final String LOCALHOST_IP1 = "127.0.0.1"; public static String getIp(){ // Get the request IP address based on HttpHeaders HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); String ip = request.getHeader("X-Forwarded-For"); if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("x-forwarded-for"); if (ip != null & amp; & amp; ip.length() != 0 & amp; & amp; !UNKNOWN.equalsIgnoreCase(ip)) { // After multiple reverse proxies, there will be multiple IP values. The first IP is the real IP. if (ip.contains(IP_UTILS_FLAG)) { ip = ip.split(IP_UTILS_FLAG)[0]; } } } 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.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } //Compatible with k8s cluster to obtain ip if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = Objects.requireNonNull(request.getRemoteAddr()); if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) { //Get the IP configured on this machine based on the network card InetAddress iNet = null; try { iNet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { log.error("getClientIp error: ", e); } assert iNet != null; ip = iNet.getHostAddress(); } } return ip; } }
Controller layer annotations
@PostMapping("addOrUpdateUser") @ApiOperation("Add or modify user information and faces") @RateLimiter(time = 2, count = 1, limitType = LimitType.IP) public HttpResult addOrUpdateUserFace(@Validated @RequestBody AddUserDto dto){ return HttpResult.ok( userService.addUserFace(dto)); }
Summary
Tips:
@RateLimiter(time = 2, count = 1, limitType = LimitType.IP) annotation will allow access once every 2 seconds based on the fixed ip