【SpringBoot商城秒杀系统项目实战24】安全优化 接口限流防刷

本文介绍接口限流防刷方法,通过缓存实现限制用户访问次数,如一分钟内超过限定值则返回失败。还给出获取访问路径、拼接key等具体操作步骤。此外,探讨通用限流防刷逻辑,通过新建注解、实现拦截器、判断用户登录等进行优化,并将拦截器注册到Spring配置类中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接口限流防刷:

限制同一个用户一秒钟或者一分钟之内只能访问固定次数,在服务端对系统做一层保护。

思路:利用缓存实现,用户每次点击之后访问接口的时候,在缓存中生成一个计数器,第一次将这个计数器置1后存入缓存,并给其设定有效期,比如一分钟,一分钟之内再访问,那么数值加一。一分钟之内访问次数超过限定数值,直接返回失败。下一个一分钟,数据重新从0开始计算。因为缓存具有一个有效期,一分钟之后自动失效。

  1. 获取访问路径
  2. 拼接用户用户的Id作为一个记录该用户访问次数的key
  3. 缓存里面取得该key,做判断
    如果缓存里面没有取到,代表是第一次访问,所以给缓存设置该key,并设置初始值value为1
    如果缓存里面取得值并且小于5,那么直接将该key对应的值value+1
    如果缓存里面的次数大于超过4(>=5),那么代表在限制时间内(在缓存还没有失效的时间内),访问次数达到限制

在这里插入图片描述

getMiaoshaPath代码:

	@RequestMapping(value ="/getPath")
	@ResponseBody
	public Result<String> getMiaoshaPath(HttpServletRequest request,Model model,MiaoshaUser user,
			@RequestParam("goodsId") Long goodsId,
			@RequestParam(value="vertifyCode",defaultValue="0") int vertifyCode) {
		model.addAttribute("user", user);
		//如果用户为空,则返回至登录页面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}
		//限制访问次数
		String uri=request.getRequestURI();
		String key=uri+"_"+user.getId();
		//限定key5s之内只能访问5次
		Integer count=redisService.get(AccessKey.access, key, Integer.class);
		if(count==null) {
			redisService.set(AccessKey.access, key, 1);
		}else if(count<5) {
			redisService.incr(AccessKey.access, key);
		}else {//超过5次
			return Result.error(CodeMsg.ACCESS_LIMIT);
		}		
		//验证验证码
		boolean check=miaoshaService.checkVCode(user, goodsId,vertifyCode );
		if(!check) {
			return Result.error(CodeMsg.REQUEST_ILLEAGAL);
		}
		System.out.println("通过!");
		//生成一个随机串
		String path=miaoshaService.createMiaoshaPath(user,goodsId);		
		return Result.success(path); 
	}

新建一个AccessKey作为访问限制的Key,设置一个固定有效期和一个动态设置有效期的Key前缀对象。

	public class AccessKey extends BasePrefix{
	//考虑页面缓存有效期比较短
	public AccessKey(int expireSeconds,String prefix) {
		super(expireSeconds,prefix);
	}
	//限制5s之内访问5次
	public static AccessKey access=new AccessKey(5,"access");
	//动态设置有效期
	public static AccessKey expire(int expireSeconds) {
		return new AccessKey(expireSeconds,"access");
	}
}

优化:如何做一个通用的限流防刷逻辑?

思路:
每个方法都需要该判断功能,那么把它抽出来,定义一个拦截器,利用拦截器来拦截这些请求,判断次数,进行操作。

  1. 新建一个注解
  @AccessLimit(seconds = 5,maxCount = 5,needLogin = true)

1.新建注解,用于限流作用(在固定时间内限制访问次数)

	@Retention(RetentionPolicy.RUNTIME)//运行期间有效
	@Target(ElementType.METHOD)//注解类型为方法注解
	public @interface AccessLimit {
    	int seconds(); //固定时间
   	 	int maxCount();//最大访问次数
   	 	boolean needLogin() default true;// 用户是否需要登录
	}

2.实现拦截器,自定义AccessInterceptor继承HandlerInterceptorAdapter拦截器基类,通过实现这个接口,拿到方法上的注解

  • 判断用户登录
    这里将之前原先定义在解析用户参数的代码封装。然后在将用这个封装的用户信息,set到ThreadLocal 中,本地线程副本,该变量与线程绑定,存取只会存取在本地线程中。然后之前获取用户的代码直接取到该用户即可。
  • 判断访问次数与失效时间(缓存时间)
    判断访问次数count ,从缓存中存取,然后根据注解时间,动态设置缓存的过期时间。
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
	@Autowired
	MiaoshaUserService miaoshaUserService;
	@Autowired
	RedisService redisService;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		if(handler instanceof HandlerMethod) {
			//先去取得用户做判断
			MiaoshaUser user=getUser(request,response);		
			System.out.println("@AccessInterceptor---user"+user);
			//将user保存下来
			UserContext.setUser(user);
			HandlerMethod hm=(HandlerMethod)handler;
			AccessLimit aclimit=hm.getMethodAnnotation(AccessLimit.class);
			//无该注解的时候,那么就不进行拦截操作
			if(aclimit==null) {
				return true;
			}
			//获取参数
			int seconds=aclimit.seconds();
			int maxCount=aclimit.maxCount();
			boolean needLogin=aclimit.needLogin();
			String key=request.getRequestURI();
			System.out.println("------------:"+key);
			if(needLogin) {
				if(user==null) {
					//需要给客户端一个提示
					render(response,CodeMsg.SESSION_ERROR);
					return false;
				}
				//需要的登录
				key+="_"+user.getId();
			}else {//不需要登录
				//不需要操作
			}
			//限制访问次数
			String uri=request.getRequestURI();
			//String key=uri+"_"+user.getId();
			//限定key5s之内只能访问5次,动态设置有效期
			AccessKey akey=AccessKey.expire(seconds);
			Integer count=redisService.get(akey, key, Integer.class);
			if(count==null) {
				redisService.set(akey, key, 1);
			}else if(count<maxCount) {
				redisService.incr(akey, key);
			}else {//超过5次
				//Result.error(CodeMsg.ACCESS_LIMIT);
				render(response,CodeMsg.ACCESS_LIMIT);
				//结果给前端
				return false;
			}
		}
		return super.preHandle(request, response, handler);
	}
	
	private void render(HttpServletResponse response, CodeMsg cm) throws IOException {
		//指定输出的编码格式,避免乱码
		response.setContentType("application/json;charset=UTF-8");
		OutputStream out=response.getOutputStream();
		String jres=JSON.toJSONString(Result.error(cm));
		out.write(jres.getBytes("UTF-8"));
		out.flush();
		out.close();
	}

	private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
		String paramToken=request.getParameter(MiaoshaUserService.COOKIE1_NAME_TOKEN);
		String cookieToken=getCookieValue(request,MiaoshaUserService.COOKIE1_NAME_TOKEN);
		if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken))
		{
			return null;
		}
		String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
		MiaoshaUser user=miaoshaUserService.getByToken(token,response);
		return user;
	}
	public String getCookieValue(HttpServletRequest request, String cookie1NameToken) {//COOKIE1_NAME_TOKEN-->"token"
		//遍历request里面所有的cookie
		Cookie[] cookies=request.getCookies();
		if(cookies!=null) {
			for(Cookie cookie :cookies) {
				if(cookie.getName().equals(cookie1NameToken)) {
					System.out.println("getCookieValue:"+cookie.getValue());
					return cookie.getValue();
				}
			}
		}
		System.out.println("No getCookieValue!");
		return null;
	}
}

UserContext 封装用户信息:

public class UserContext {
	private static ThreadLocal<MiaoshaUser> userHolder=new ThreadLocal<MiaoshaUser>();
	
	public static void setUser(MiaoshaUser user) {
		userHolder.set(user);
	}
	
	public static MiaoshaUser getUser() {
		return userHolder.get();
	}
}

3.将拦截器 注册到WebConfig中,这个类继承WebMvcConfigurerAdapter ,Spring框架的配置类。

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
	@Autowired
	UserArgumentResolver userArgumentResolver;
	@Autowired
	AccessInterceptor accessInterceptor;
	
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		//将UserArgumentResolver注册到config里面去	
		argumentResolvers.add(userArgumentResolver);
	}	
	/**
	 * 注册拦截器
	 */
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		//注册
		registry.addInterceptor(accessInterceptor);
		super.addInterceptors(registry);
	}	
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小台

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值