02防止恶意刷单

2.防止恶意刷单

在生产场景下,很有可能会存在某些用户恶意刷单的情况出现。这样的操作对于系统而言,会导致业务出错、脏数据、后端访问压力大等问题的出现。
一般要解决这个问题的话,需要前端进行控制,同时后端也需要进行控制。后端实现可以通过Redis incrde 原子性递增来进行解决。

2.1 更新秒杀服务下单

在这里插入图片描述

2.2 防重方法实现

//防止重复提交
private String preventRepeatCommit(String username,Long id) {
	String redisKey = "seckill_user_" + username+"_id_"+id;
	long count = redisTemplate.opsForValue().increment(redisKey, 1);
	if (count == 1){
		//设置有效期五分钟
		redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES);
		return "success";
	} 
    if (count>1){
		return "fail";
	} 
    return "fail";
}

3.防止相同商品重复秒杀

3.1 修改下单业务层实现

在这里插入图片描述

3.2 dao层新增查询方法

public interface SeckillOrderMapper extends Mapper<SeckillOrder> {
	/**
	* 查询秒杀订单信息
	* @param username
	* @param id
	* @return
	*/
	@Select("select * from tb_seckill_order where user_id=#{username} and seckill_id=#{id}")
	SeckillOrder getSecKillOrderByUserNameAndGoodsId(String username, Long id);
}

4.秒杀下单接口隐藏

当前虽然可以确保用户只有在登录的情况下才可以进行秒杀下单,但是无法方法有一些恶意的用户在登录了之后,猜测秒杀下单的接口地址进行恶意刷单。所以需要对秒杀接口地址进行隐藏。
在用户每一次点击抢购的时候,都首先去生成一个随机数并存入redis,接着用户携带着这个随机数去访问秒杀下单,下单接口首先会从redis中获取该随机数进行匹配,如果匹配成功,则进行后续下单操作,如果匹配不成功,则认定为非法访问。

4.1 将随机数工具类放入common工程中

package com.changgou.util;

import java.util.Random;

public class RandomUtil {
    public static String getRandomString() {
        int length = 15;
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        String randomString = RandomUtil.getRandomString();
        System.out.println(randomString);
    }
}

4.2 秒杀渲染服务定义随机数接口

 @GetMapping("/getToken")
    @ResponseBody
    public String getToken(){
        String randomString = RandomUtil.getRandomString();

        String cookieValue = this.readCookie();

        redisTemplate.opsForValue().set("randomcode_"+cookieValue,randomString,5, TimeUnit.SECONDS);

        return randomString;
    }

    private String readCookie(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        String jti = CookieUtil.readCookie(request, "uid").get("uid");

        return jti;
    }

4.3 js修改

修改js下单方法

//秒杀下单
add:function(id){
	app.msg ='正在下单';
	//获取随机数
	axios.get("/api/wseckillorder/getToken").then(function (response) {
		var random=response.data;
		axios.get("/api/wseckillorder/add?time="+moment(app.dateMenus[0]).format("YYYYMMDDHH")+"&id="+id+"&random="+random).then(function (response) {
			if (response.data.flag){
			app.msg='抢单成功,即将进入支付!';
			}else{
				app.msg='抢单失败';
			}
		})
	})
}

4.4 秒杀渲染服务更改

修改秒杀渲染服务下单接口

@RequestMapping("/add")
  
    public Result add(@RequestParam("time") String time, @RequestParam("id")Long id,String random){

        String cookieValue = this.readCookie();
        String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_"+cookieValue);
        if (StringUtils.isEmpty(redisRandomCode)){
            return new Result(false, StatusCode.ERROR,"下单失败");
        }
        if (!random.equals(redisRandomCode)){
            return new Result(false, StatusCode.ERROR,"下单失败");
        }

        Result result = secKillOrderFeign.add(time, id);
        return result;
    }

5 秒杀下单接口限流

因为秒杀的特殊业务场景,生产场景下,还有可能要对秒杀下单接口进行访问流量控制,防止过多的请求进入到后端服务器。对于限流的实现方式,我们之前已经接触过通过nginx限流,网关限流。但是他们都是对一个大的服务进行访问限流,如果现在只是要对某一个服务中的接口方法进行限流呢?这里推荐使用google提供的guava工具包中的RateLimiter进行实现,其内部是基于令牌桶算法进行限流计算
1)添加依赖

<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>28.0-jre</version>
</dependency>

2)自定义限流注解

@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) //不仅保存到class文件中,并且jvm加载class之后,该注解仍然存在
public @interface AccessLimit {}

3)自定义切面类

package com.changgou.seckill.web.aspect;

import com.alibaba.fastjson.JSON;
import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.google.common.util.concurrent.RateLimiter;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Scope
@Aspect
public class AccessLimitAop {

    @Autowired
    private HttpServletResponse response;

    //设置令牌的生成速率
    private RateLimiter rateLimiter = RateLimiter.create(2.0); //每秒生成两个令牌存入桶中

    @Pointcut("@annotation(com.changgou.seckill.web.aspect.AccessLimit)")
    public void limit(){}

    @Around("limit()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint){

        boolean flag = rateLimiter.tryAcquire();
        Object obj = null; //返回值

        if (flag){
            //允许访问
            try {
                obj = proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }else{
            //不允许访问,拒绝
            String errorMessage = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,"fail"));
            //将信息返回到客户端上
            this.outMessage(response,errorMessage);
        }

        return obj;
    }

    private void outMessage(HttpServletResponse response,String errorMessage){

        ServletOutputStream outputStream = null;
        try {
            response.setContentType("application/json;charset=utf-8");
            outputStream = response.getOutputStream();
            outputStream.write(errorMessage.getBytes("utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4)使用自定义限流注解

@RequestMapping("/add")
    @AccessLimit
    public Result add(@RequestParam("time") String time, @RequestParam("id")Long id,String random){

        String cookieValue = this.readCookie();
        String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_"+cookieValue);
        if (StringUtils.isEmpty(redisRandomCode)){
            return new Result(false, StatusCode.ERROR,"下单失败");
        }
        if (!random.equals(redisRandomCode)){
            return new Result(false, StatusCode.ERROR,"下单失败");
        }

        Result result = secKillOrderFeign.add(time, id);
        return result;
    }
发布了211 篇原创文章 · 获赞 6 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u014736082/article/details/104887558