SpringBoot秒殺系統實戰23-安全優化 數學圖形驗證碼

秒殺接口地址的隱藏可以防止惡意用戶通過頻繁調用接口來請求的操作,但是無法防止機器人,刷票軟件惡意頻繁點擊按鈕來刷請求秒殺地址接口的操作。

高併發下場景,在剛剛開始秒殺的那一瞬間,迎來的併發量是最大的,減少同一時間點的併發量,將併發量分流也是一種減少數據庫以及系統壓力的措施(使得1s中來10萬次請求過渡爲10s中來10萬次請求)

思路:點擊秒殺之前,先輸入驗證碼,分散用戶的請求。具體實現是服務端生成類似1+2-3的驗證碼,把結果計算出來存至服務端(緩存),把驗證碼圖片發至客戶端,此後客戶端在請求秒殺地址前輸入驗證碼值發請求驗證,(去緩存裏面取得值驗證是否與用戶輸入相同),驗證通過纔會動態生成秒殺地址給前端。

步驟:

1、 在商品詳情頁面加入驗證碼圖片標籤,指定Id,再加入驗證碼輸入框input組件,並初始化它們的屬性爲不可見的,因爲一開始驗證碼和輸入框是不可見的(只有秒殺開始纔會可見),圖片可以點擊刷新圖片,所以定義refreshVCode方法來刷新圖片。

2、 在倒計時方法裏面正在進行秒殺分支判斷中加入顯示驗證碼以及驗證碼輸入框的代碼邏輯,開始秒殺的時候,設置其可見並且指定attr()方法動態指定src,發送請求到後端,動態生成圖片。注意:秒殺結束之後,又需要將其設置爲不可見的。

3、 請求中傳參爲goodsId,然後可以根據用戶id和goodsId生成數學公式驗證碼,然後將這個驗證碼圖片response的輸出流輸出至前端。

後端接收生成圖片請求接口:

/**
 * 生成圖片驗證碼
 */
@RequestMapping(value ="/vertifyCode")
@ResponseBody
public Result<String> getVertifyCode(Model model,MiaoshaUser user,
        @RequestParam("goodsId") Long goodsId,HttpServletResponse response) {
    model.addAttribute("user", user);
    //如果用戶爲空,則返回至登錄頁面
    if(user==null){
        return Result.error(CodeMsg.SESSION_ERROR);
    }
    BufferedImage img=miaoshaService.createMiaoshaVertifyCode(user, goodsId);
    try {
        OutputStream out=response.getOutputStream();
        ImageIO.write(img,"JPEG", out);
        out.flush();
        out.close();
        return null; 
    } catch (IOException e) {
        e.printStackTrace();
        return Result.error(CodeMsg.MIAOSHA_FAIL);
    }
}

4、 圖片是利用BufferedImage 類生成,指定高度與寬度,利用Graphics做畫筆,填充顏色,畫出邊界線等操作,然後利用drawString方法將我們隨機拼接成字符串寫在生成的圖片上,還要計算出字符串的值存在緩存裏面。

createMiaoshaVertifyCode生成驗證碼圖片方法:

public BufferedImage createMiaoshaVertifyCode(MiaoshaUser user, Long goodsId) {
    if(user==null||goodsId<=0) {
        return null;
    }
    int width=80;
    int height=30;
    BufferedImage img=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
    Graphics g=img.getGraphics();
    g.setColor(new Color(0xDCDCDC));
    g.fillRect(0, 0, width, height);
    g.setColor(Color.BLACK);
    g.drawRect(0, 0, width-1, height-1);
    Random rdm=new Random();
    for(int i=0;i<50;i++) {
        int x=rdm.nextInt(width);
        int y=rdm.nextInt(height);
        g.drawOval(x, y, 0, 0);
    }
    //生成驗證碼
    String vertifyCode=createVertifyCode(rdm);
    g.setColor(new Color(0,100,0));
    g.setFont(new Font("Candara",Font.BOLD,24));
    //將驗證碼寫在圖片上
    g.drawString(vertifyCode, 8, 24);
    g.dispose();
    //計算存值
    int rnd=calc(vertifyCode);
    //將計算結果保存到redis上面去
    redisService.set(MiaoshaKey.getMiaoshaVertifyCode, ""+user.getId()+"_"+goodsId, rnd);
    return img;
}

注意:對於數學公式的生成,方法createVertifyCode實現,生成3個0到9之間的隨機數,然後在生成一個字符數組,用於存放 + – * (加減乘)三個數學運算符,隨機選中兩個字符,然後對其進行拼接 成一個字符串,數+運算符+數+運算符+數,返回這個字符串。

private static char[]ops=new char[] {'+','-','*'};
private String createVertifyCode(Random rdm) {
    //生成10以內的
    int n1=rdm.nextInt(10);
    int n2=rdm.nextInt(10);
    int n3=rdm.nextInt(10);
    char op1=ops[rdm.nextInt(3)];//0  1  2
    char op2=ops[rdm.nextInt(3)];//0  1  2
    String exp=""+n1+op1+n2+op2+n3;
    return exp;
}

5、 利用scriptEngine類,調用JavaScript的eval() 方法,計算這個字符串公式的值,將這個值保存到redis上面去(用戶下次發送驗證請求的時候,直接去緩存裏面取出並驗證即可)注意:eval() 計算得到的是double 值,但我們需要的int 值,需要強轉

private static int calc(String exp) {
    try {
        ScriptEngineManager manager=new ScriptEngineManager();
        ScriptEngine engine=manager.getEngineByName("JavaScript");
        return (Integer) engine.eval(exp);
    }catch(Exception e){
        e.printStackTrace();
        return 0;
    }
}

6、 前端得到這個驗證碼圖片,顯示該驗證碼,然後用戶需要輸入驗證碼將這個驗證碼作爲參數,與獲取秒殺地址請求一起傳輸給後端(校驗的操作在獲取秒殺地址之前),後端接收到參數,進行驗證碼比對,緩存中取出該驗證碼進行校驗。如果不通過,不生成秒殺接口地址,直接返回驗證碼錯誤信息。

注意:在圖片上定義個oncilck 操作,點擊後在請求獲取圖片驗證碼的接口,但是瀏覽器會有緩存,要加上timestamp 這個參數,瀏覽器纔會真正發送請求,不然只是去緩存裏面拿。

//刷新驗證碼,瀏覽器具有緩存---所以加一個參數timestamp
	function refreshVCode(){
		$("#vertifyCodeImg").attr("src","/miaosha/vertifyCode?goodsId="+$("#goodsId").val()+"&timestamp="+new Date().getTime());
	}

驗證邏輯:

/**
 * 驗證驗證碼,取緩存裏面取得值,驗證是否相等
 */
public boolean checkVCode(MiaoshaUser user, Long goodsId, int vertifyCode) {
    Integer redisVCode=redisService.get(MiaoshaKey.getMiaoshaVertifyCode, user.getId()+"_"+goodsId, Integer.class);
    if(redisVCode==null||redisVCode-vertifyCode!=0) {
        return false;
    }
    //刪除緩存裏面的數據
    redisService.delete(MiaoshaKey.getMiaoshaVertifyCode, user.getId()+"_"+goodsId);
    return true;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章