问题
- 表单防重复提交一直是一个必须解决的接口安全问题
- 接口限流也是时长要处理的问题,如短信验证码获取接口
如何优雅的解决呢
技术点
- 自定义注解
- 反射
- AOP
- Redis lua脚本
利用这四个技术的结合,既优雅又简化开发可方便用在任何接口
自定义注解
package com.cz.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 限流
*
* @author chaixuhong
* @date 2021年12月30日
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
//功能描述,不参与逻辑
String description() default "默认防重复请求";
// 时间的,单位秒
String period() default "5";
// 限制访问次数
String count() default "1";
// 限制类型
LimitType limitType() default LimitType.CUSTOMER;
enum LimitType {
CUSTOMER,
IP
}
}
- 默认放重复提交功能,接口5秒内只能请求一次,当然可自行根据业务调整
AOP + 反射
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cz.aspect;
import com.cz.annotation.Limit;
import com.cz.exception.BusinessException;
import com.cz.utils.RequestParseUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
/**
* @author chaixuhong
* @date 2021年12月30日
*/
@Aspect
@Component
@Slf4j
public class LimitAspect {
public static final String REDIS_PREFIX = "limit:";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.cz.annotation.Limit)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = RequestParseUtil.getHttpServletRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method signatureMethod = signature.getMethod();
Limit limit = signatureMethod.getAnnotation(Limit.class);
Limit.LimitType limitType = limit.limitType();
StringBuffer key = new StringBuffer(REDIS_PREFIX).append(joinPoint.getTarget().getClass().getCanonicalName()).append(".").append(signatureMethod.getName());
if (limitType == Limit.LimitType.IP) {
key.append(":").append(RequestParseUtil.getIp(request));
}
List<String> keys = Arrays.asList(key.toString());
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/Limit.lua")));
redisScript.setResultType(Number.class);
Number count = stringRedisTemplate.execute(redisScript, keys, limit.count(), limit.period());
if (null != count && count.intValue() <= Integer.parseInt(limit.count())) {
log.info("第{}次访问key为 {}的接口,功能描述:{}", count, keys, limit.description());
return joinPoint.proceed();
} else {
throw new BusinessException("访问次数受限制");
}
}
}
- 通过Redis lua脚本保证操作的原子性,多线程结果的一致性
lua脚本
local c
c = redis.call('get',KEYS[1])
if c and tonumber(c) > tonumber(ARGV[1]) then
return tonumber(c);
end
c = redis.call('incr',KEYS[1])
if tonumber(c) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
end
return c;
如何使用
- 只需要在接口上增加
@Limit
注解就可以了 - 示例
//@Limit 什么参数不写默认防重复提交功能
@Limit(description = "IP限流", period = "60", count = "10", limitType = Limit.LimitType.IP)
@GetMapping(value = "/child")
public ResultVO<DeptVO> child(@RequestParam(required = false) Integer pid, @RequestParam(required = false) String name) {
return ResponseUtil.success(sysDeptService.child(pid, name));
}