前後臺聯合驗證的驗證碼
作者:邵發
本文是Java學習指南系列教程的官方配套文檔。內容介紹另一種安全的驗證碼技術,即由後臺負責生成和驗證,使整個驗證流程不可輕易攻擊。本文附帶項目源碼及相關JAR包。
1. 驗證碼的作用
在上一篇文章已經說過,驗證碼是用於“防刷”的,防止用戶或機器人的高頻率的網頁刷新。舉一個例子,假設網站提供一個訂單查詢功能,示意圖如下。
( 項目演示http://127.0.0.1:8080/demo/test )
所謂的惡意刷新,就是瘋狂地點擊這個“查詢”按鈕。由於後臺的查詢操作往往要查詢數據庫,這個操作佔用CPU較高。如果對用戶的惡意刷新不加阻攔,則網站會由於負載太高而崩潰。
所以對高耗資源的服務接口,一般要使用驗證碼加以保護,使其無法輕易調用。加上驗證碼環節之後,用戶必須在人工輸入了正確的驗證碼之後,才能夠進入後面的查詢流程。輸入驗證碼需要一定時間,從而阻止了用戶的高頻刷新攻擊。
2. 後臺驗證碼的實現
驗證碼在工作原理上分爲兩種:純前端驗證,前端+後臺聯合驗證。本文介紹的是後臺驗證方式,這種方式依靠後臺來生成和校驗,過程安全可靠,不會輕易地被攻擊。
2.1 生成驗證碼
後臺添加一個接口,用於生成新的驗證碼,並放在Session裏。
@RequestMapping("/verify/refresh.do")
public Object refresh( HttpSession session)
{
// 產生新的隨機驗證碼,放在Session裏
VerifyPng v = new VerifyPng();
String verifyCode = v.randomChar(4);
session.setAttribute("verifyCode", verifyCode);
return new AfRestData("");
}
2.2 顯示驗證碼圖片
由後臺負責動態的生成一個驗證碼的圖片,交給前端顯示。
@GetMapping("/verify/show")
public void show ( HttpSession session
, HttpServletResponse response) throws Exception
{
// 取得當前校驗碼
String verifyCode = (String)session.getAttribute("verifyCode");
// 生成PNG發給客戶端
response.setContentType("image/png");
response.setHeader("Cache-Control", "no-cache");
VerifyPng v = new VerifyPng();
v.toPNG(verifyCode, response.getOutputStream());
}
其中,由於驗證碼圖片是動態變化的,所以應答頭部要設置Cache-Control: no-cache。這樣,前端只需要顯示 http://your.com/verify/show 這張圖片就可以了。
前端實現:
(1) 放一個 <img> 控件用於顯示驗證圖片
<img class='verifyImg' style='width:80px;height:30px;background-color: #f5f59e;'>
(2) 然後動態的刷新驗證碼:
var req = {};
Af.rest('[[@{/verify/refresh.do}]]' , req, function(data){
var verifyImgUrl = '[[@{/verify/show}]]?t=' + new Date().getTime();
$('.verifyImg').attr('src', verifyImgUrl);
})
也就是說,先調用 /verify/refresh.do 接口來刷新生成新的驗證碼,然後讓<img>控件顯示新的驗證碼圖片即可。
2.3 提交驗證
用戶輸入驗證碼,點查詢按鈕。
(1) 前端把用戶的輸入提交到後臺,交由後臺驗證。
var req = {};
req.verifyCode = $('.verifyCode').val().trim();
req.orderNumber = $('.orderNumber').val().trim();
Af.rest ('[[@{/query.do}]]' ,req, function(data){
alert('成功提交');
},
function(error, reason){
// 如果是驗證碼錯誤,則重置驗證碼
if(error == -8){
vf.reset();
}
alert(reason);
})
(2) 後臺負責校驗用戶的輸入是否正確
後臺從請求裏取得用戶的輸入,然後與當前Session的驗證碼比較。如果驗證碼不匹配,則返回相應的提示給客戶端。
@PostMapping( "/query.do")
public Object query(HttpSession session
, @RequestBody JSONObject jreq)
{
// 取得用戶傳上來的輸入值
String verifyCode = jreq.getString("verifyCode");
if(! VerifyController.verify(session, verifyCode))
return new AfRestError(-8, "驗證碼錯誤!");
return new AfRestData("");
}
3. 後臺驗證碼圖片的動態生成
在後臺可以用Java直接生成一張驗證碼圖片,技術上也不復雜,就是創建一個BufferedImage然後在其上繪製即可。在Java學習指南系列的《Swing高級篇》裏有充分的演示講解。
生成圖片的關鍵代碼如下。
Graphics2D g2d = image.createGraphics();
g2d.setPaint(new Color(0xB03060)); // 文本顏色
g2d.setFont(new Font("微軟雅黑", Font.PLAIN, height-6));
FontMetrics fm = g2d.getFontMetrics(g2d.getFont());
int fontSize = fm.getHeight(); // 字高
int textWidth = fm.stringWidth(text);
int leading = fm.getLeading();
int ascent = fm.getAscent(); // top -> baseline 的高度
int descent = fm.getDescent(); // bottom->baseline 的高度
Rectangle rect = new Rectangle(width,height);
int x = rect.x + (rect.width - textWidth)/2; // 水平居中
int y = rect.y + rect.height /2 + (fontSize-leading)/2 - descent; // 豎直居中
g2d.drawString(text, x, y);
還可以在上面疊加繪製一些半透明的干擾點,示例代碼如下:
Random rand = new Random();
for(int i=0;i<4;i++)
{
int r = rand.nextInt(255);
int g = rand.nextInt(255);
int b = rand.nextInt(255);
int posX = rand.nextInt(width-10);
int posY = rand.nextInt(height-10);
g2d.setPaint(new Color(r,g,b, 60));
g2d.fillOval(posX, posY, 10, 10);
}
以上演示所用的項目源碼和JAR包在此處可以獲取。
4. 安全性考慮
有人可能會問,黑客藉助圖像識別技術,寫一個程序來自動識別圖片裏的驗證碼,不還是能夠攻擊嗎?
看起來是這樣,不過考慮以下幾點:
(1) 專門寫一個程序,購置人工智能的服務器,需要時間和金錢成本。高性能服務器是要花錢的。
(2) 機器識別也需要時間。如果識別的時間較長,則失去攻擊的意義。因爲驗證碼技術不是要禁止別人的訪問,而是讓人訪問得“慢一點”。如果在識別的時候耽誤了時間,過程已經變慢了,此時攻擊的頻率已經顯著下降,已經沒有多大危害了。