前言:在实际开发中,我们可能会遇到需要对某个接口请求频率做一定时间间隔的限制,如生活中常见的应用上二维码刷新频率限制等。于是这里做了一个简单的切面限制频率案例,使用的是切面注解方式,减少侵入性。
一、切面实现请求接口频率限制
1.pom.xml引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.自定义一个注解
import java.lang.annotation.*;
/**
* 用于限制接口请求频率
*/
@Documented
@Target({
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReqLimit {
/**
* 请求频率限制(单位秒)默认3秒可自己调整
*/
int rateSecond() default 3;
}
3.创建一个@Aspect的切面类,用来处理核心逻辑
@Aspect
@Component
@Order(1)
public class ReqLimitAspect {
public static final String REQUEST_LIMIT = "requestLimit";
/**
* 频率限制切入点(注解类的路径)
*/
@Pointcut(value = "@annotation(com.alone.server.annotation.ReqLimit)")
public void reqLimitPointCut() {
}
/**
* 切面请求频率限制
*
* @param joinPoint joinPoint
*/
@Before("reqLimitPointCut()")
public void doBefore(JoinPoint joinPoint) {
HttpSession session = this.getCurrentUserSession();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
ReqLimit reqLimit = methodSignature.getMethod().getAnnotation(ReqLimit.class);
if (session == null) {
// 请求不合法
throw new CustomException(
ResultEnum.REQUEST_INVALID.getCode(),
ResultEnum.REQUEST_INVALID.getMsg());
}
if (session.getAttribute(REQUEST_LIMIT) == null) {
// 在session中存放请求的相关信息
session.setAttribute(REQUEST_LIMIT, new HashMap<String, Long>());
}
Map<String, Long> map = (Map<String, Long>) session.getAttribute(REQUEST_LIMIT);
String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
Long lastReqTime = map.get(methodName);
if (lastReqTime != null) {
int interval = (int) (System.currentTimeMillis() - lastReqTime) / 1000;
if (interval < reqLimit.rateSecond()) {
map.put(methodName, System.currentTimeMillis());
// 请求过于频繁抛出异常,项目中可以自己定一个全局异常来处理
throw new CustomException(
ResultEnum.REQUEST_LIMITED.getCode(),
ResultEnum.REQUEST_LIMITED.getMsg());
}
}
// 这里设置当前时间,作为下一次请求是获取的 lastReqTime(上一次请求时间)
map.put(methodName, System.currentTimeMillis());
}
/**
* 获取当前session
*
* @return
*/
private HttpSession getCurrentUserSession() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getSession();
}
}
2.注解在Controller中的实际应用
/**
* 刷新编码
*/
@ReqLimit(rateSecond = 5)//接口上加上注解就可以了,这里可以自己设定时间
@RequestMapping(value = "/refresh", method = RequestMethod.GET)
public Result<String> refresh(@RequestParam(value = "code", required = true) String code) {
return Result.success(codeService.refresh(code));
}
请求成功结果
{
"code": 200,
"msg": "成功",
"data": "ErdpJ20dnife"
}
请求频繁结果
{
"code": 201,
"msg": "请求过于频繁",
"data": null
}
二、HttpSession 问题拓展
以上实现是基于 HttpSession 存储相关请求时间,请求具体接口信息来做处理的。由于我的项目是前后端分离,前端请求服务端中间还经过 nginx 代理,前期发现不管请求多么频繁都没有错误的返回,从日志看也没有相关报错,初步判断是 session 问题无疑,如果未来你的项目也使用到 HttpSession 来做一些会话信息保存,如这里的频率限制,验证码,或者使用 shiro 安全框架,并且使用到了代理那都会遇到这样的问题,所以我们有必要对 session 进一步了解。
Session 是什么
session 我们一般翻译为会话,在web应用中用户从打开浏览器登陆网页,浏览到退出这个过程,我们视为一个会话。而在开发者看来,用户从登陆到退出这一过程,需要创建一个数据结构来存储用户的相关信息,这个结构就叫做session
为什么需要 Session
http协议是“无连接,无状态”的, 即每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,连接就断开了。如果用户在网页中A页面加入了商品在购物车中,点击支付跳转B页面时候就无法获取到B之前相关信息了,所以需要一个具有唯一性的标识的session来存储用户状态信息。
Session 是怎么创建的和传递的
当浏览器第一次访问服务器时,服务端会开辟一块内存,这块内存就叫做session。session是和浏览器关联的,如果你换了另外一个浏览器登录,那就会有另外一个session生成。服务端会为每一个session维护一份会话信息数据,而客户端和服务端依靠一个全局唯一的标识 sessionid 来访问会话信息数据,保存到客户端的只有sessionid,当客户端再次发送请求的时候,会将这个sessionid存放在cookie中带上,服务器接受到请求之后就会依据sessionid找到相应的session。tomcat中生成的sessionid叫做jsessionid,通过抓包在cookie中可以观察到。

Cookie,Session丢失
我们在请求数据抓包中,cookie参数里没有发现jsessionid那就是丢失了。cookie丢失了,那自然也服务端也就找不到对应的 sessionid了。一般是cookie_path与地址栏上的path不相符游览器就不会接受这个cookie导致的。服务端没有获取到sessionid会为每一次请求都创建一个新的session那自然无法获取到上一次请求的相关信息了。会导致验证码验证不通过,或者使用shiro框架,用户反复登录无效。
如何解决
原来错误的 ngxin 配置
location /api {
proxy_pass http://xxx.xxx.xxx.xxx:8580/alone-server/;
}
改正后正确的 nginx 配置
location /api/ {
proxy_pass http://xxx.xxx.xxx.xxx:8580/alone-server/;
proxy_cookie_path /alone-server /api;
}
注意事项:location 后的路径要和 proxy_cookie_path 后面的路径一致,且后要注意location后的/和 proxy_cookie_path 后项目地址与路径/api之间有空格。alone-server这里填的是你的项目地址,proxy_pass 中要和 proxy_cookie_path 中的一致。