秒殺接口地址隱藏可以防止惡意用戶通過頻繁調用接口來請求的操作,但是無法防止機器人,刷票軟件還是可以惡意頻繁點擊按鈕來刷請求秒殺地址接口
高併發下場景,在剛剛開始秒殺的那一瞬間,迎來的併發量是最大的,減少同一時間點的併發量,將併發量分流也是一種減少數據庫以及系統壓力的措施(使得1s中來10萬次請求過渡爲10s中來10萬次請求)
本篇博客記錄如何使用數學圖形驗證碼來進行限流,限流削峯其他操作可參考:
實現思路:
點擊秒殺之前,需先輸入驗證碼,來分散用戶的請求。具體實現是後端生成類似1+2-3的驗證碼,把結果計算出來存至redis,再把驗證碼圖片發至客戶端,此後客戶端在請求秒殺地址前輸入驗證碼值發請求驗證,後端去緩存裏面取值驗證是否與用戶輸入的值相同,驗證通過纔會動態生成秒殺地址給前端
步驟:
- 在商品詳情頁面加入驗證碼圖片標籤,指定id,再加入驗證碼輸入框input組件,並初始化它們的屬性爲不可見的,因爲一開始驗證碼和輸入框是不可見的(只有秒殺開始纔會可見),可以點擊刷新圖片,所以定義refreshVCode函數來刷新圖片
refreshVCode函數代碼如下:
function refreshVerifyCode() {
$("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId=" + $("#goodsId").val() + "×tamp=" + new Date().getTime());
}
注:在圖片上定義oncilck操作,點擊後會請求獲取圖片驗證碼的接口,但是瀏覽器會有緩存,所以加上timestamp這個參數,瀏覽器纔會真正發送請求,不然會去緩存裏面拿
- 在倒計時方法裏面正在進行秒殺分支判斷中加入顯示驗證碼以及驗證碼輸入框的代碼邏輯,開始秒殺的時候,設置其可見並且指定attr()方法動態指定src,發送請求到後端,動態生成圖片;注意秒殺結束之後,又需要將其設置爲不可見的
- 請求中傳參爲goodsId,然後根據用戶id和goodsId生成數學公式驗證碼,然後將這個驗證碼圖片用response的輸出流輸出至前端
後端生成驗證碼圖片接口代碼:
@RequestMapping(value = "/verifyCode", method = RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaVerifyCod(HttpServletResponse response,MiaoshaUser user,
@RequestParam("goodsId") long goodsId) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
try {
BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.flush();
out.close();
return null;
} catch (Exception e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
圖片是利用BufferedImage類生成的,指定高度與寬度,利用Graphics做畫筆,填充顏色,畫出邊界線等操作,然後利用drawString方法將驗證碼隨機拼接成字符串寫在生成的圖片上,還要把計算出字符串的值存在redis裏面
createMiaoshaVertifyCode方法代碼:
/**
* 生成驗證碼
*/
public BufferedImage createVerifyCode(MiaoshaUser user, long goodsId) {
if (user == null || goodsId <= 0) {
return null;
}
int width = 80;
int height = 32;
//create the image
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
//set the background color
g.setColor(new Color(0xDCDCDC));
g.fillRect(0, 0, width, height);
//draw the border
g.setColor(Color.black);
g.drawRect(0, 0, width - 1, height - 1);
//create a random instance to generate the codes
Random rdm = new Random();
//make some confusion
for (int i = 0; i < 50; i++) {
int x = rdm.nextInt(width);
int y = rdm.nextInt(height);
g.drawOval(x, y, 0, 0);
}
//generate a random code
String verifyCode = generateVerifyCode(rdm);
g.setColor(new Color(0, 100, 0));
g.setFont(new Font("Candara", Font.BOLD, 24));
g.drawString(verifyCode, 8, 24);
g.dispose();
//把驗證碼存到redis中
int rnd = calc(verifyCode);
redisService.set(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, rnd);
//輸出圖片
return image;
}
對於數學公式的生成,是使用generateVerifyCode方法實現的,生成3個0到9之間的隨機數,然後在生成一個字符數組,用於存放 + - * (加減乘)三個數學運算符,隨機選中兩個字符,然後將其進行拼接成一個字符串,數+運算符+數+運算符+數,返回這個字符串,generateVerifyCode方法代碼如下:
private static char[] ops = new char[]{'+', '-', '*'};
/**
* 生成加減乘的算式
* + - *
*/
private String generateVerifyCode(Random rdm) {
int num1 = rdm.nextInt(10);
int num2 = rdm.nextInt(10);
int num3 = rdm.nextInt(10);
char op1 = ops[rdm.nextInt(3)];
char op2 = ops[rdm.nextInt(3)];
String exp = "" + num1 + op1 + num2 + op2 + num3;
return exp;
}
- 利用scriptEngine類,調用JavaScript的eval() 方法,計算這個字符串公式的值,並將這個值保存到redis裏面去(用戶下次發送請求的時候,直接去緩存裏面取出並驗證即可);注意eval()方法計算得到的是double類型的值,但我們需要的是int類型的值,所以需要強轉
calc方法代碼如下:
/**
* 將算式進行計算
*/
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;
}
}
- 前端得到這個驗證碼圖片顯示該驗證碼,然後用戶需要輸入驗證碼將這個驗證碼作爲參數,與獲取秒殺地址請求一起傳輸給後端(校驗的操作在獲取秒殺地址之前),後端接收到參數後進行驗證碼比對,從緩存中取出該驗證碼進行校驗;如果不通過,不生成秒殺接口地址,直接返回驗證碼錯誤信息
後端驗證邏輯:
checkVerifyCode方法代碼:
/**
* 校驗數字公式的驗證碼結果
* @param user
* @param goodsId
* @param verifyCode
* @return
*/
public boolean checkVerifyCode(MiaoshaUser user, long goodsId, int verifyCode) {
if (user == null || goodsId <= 0) {
return false;
}
Integer codeOld = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, Integer.class);
if (codeOld == null || codeOld - verifyCode != 0) {
return false;
}
//刪除緩存裏的數據
redisService.delete(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId);
return true;
}