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;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章