關于圖片驗證碼,筆者當初學習 Java Web 時用純 JSP 實作過一次(《JSP 實用程式之簡易圖片驗證碼》),如今利用 AJAXJS 庫重寫一遍,
簡介
驗證碼(Captcha)的作用是防止別有用心的人通過撰寫“自動注冊機”這類手段來大量發送非法的請求,這類請求通常涉及資料庫寫的操作,因此要在后臺設立一道防線來識別是否自然人的訪問還是機器的操作,典型的一種方法就是圖片驗證碼,如下圖所示是一個加入干擾碼的數字驗證圖片,

該組件結構如下圖所示,

配套視頻
- https://www.bilibili.com/video/av91382240/
- https://www.bilibili.com/video/av91364142/ (粵語)
- https://youtu.be/VgfiqQ4WBxE
- https://youtu.be/dw7dnfbusM8 (粵語)
使用方法
前端訪問
前端 <img /> 元素訪問固定的 URL 地址: /Captcha 以便獲取圖片,與此同時到服務端保存驗證碼到 Session 中,/Captcha 由 控制器 com.ajaxjs.web.captcha.CaptchaController指定,UI 代碼如下(https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-demo/WebContent/web/captcha.html),
<form action="../CheckCaptcha" method="POST">
名 稱:
<input type="text" name="username" size="20" class="ajaxjs-input" placeholder="請輸入名稱" required="required" />
<br />
<br />
驗 證 碼: <input type="text" name="CAPTCHA_CODE" size="20" class="ajaxjs-input"
placeholder="請輸入右側驗證碼" required="required" style="width:120px;" />
<img src="../Captcha?d=888" style="cursor: pointer;vertical-align: middle;"
onclick="this.src=this.src.replace(/d=\d+/, 'd=' + new Date().valueOf());" />
<span style="font-size:9pt; color:gray">點擊驗證碼圖片重繪</span>
<button class="ajaxjs-btn">提交</button>
</form>
在表單中插入一個<input />輸入框元素,提交時連同這個輸入框的值一起送入到后臺作驗證碼校驗,驗證碼的方式最簡單的是一張數字圖片,復雜的可以是漢字、語音、拼圖甚至一個小游戲等等,驗證碼圖片也是一種特殊的請求,返回動態的圖片,同時在后端產生會話 Session 存盤者驗證碼 code,只要 POST 過來的時候比對客戶端的 code 與 Session 中的是否一致便可判斷是否合法請求,下面就是引入驗證碼圖片的呼叫方式,
<img src="../Captcha?d=143988" style="cursor: pointer;"
onclick="this.src=this.src.replace(/d=\d+/, 'd=' + new Date().valueOf());" />
僅僅引入圖片還不夠還要求用戶點擊圖片時候重繪驗證碼,于是加入下面單擊圖片的事件,采用行內 inline 登記事件的方式(謂行內,就是在 HTML 陳述句中定義如 onclick="xxx" 的方式,以區別于在 *.js 檔案中給出 js 代碼,),
行內陳述句中的 new Date().valueOf() 的作用是回傳當前的時間戳,附加在圖片地址上使得每次請求驗證碼圖片的地址都不一樣,保證了瀏覽器不會快取同一個地址,如下圖所示,

注意驗證碼輸入框 name 屬性要與 Session 的 key 相吻合,一般為 name="CAPTCHA_CODE",或者通過 JSP 設定:
<%
request.setAttribute("CAPTCHA_CODE", com.ajaxjs.web.captcha.CaptchaController.CAPTCHA_CODE);
%>
通過 Vue.js 封裝的組件參考,
<aj-page-captcha field-name="${CAPTCHA_CODE}"></aj-page-captcha>
這樣 JSP 的寫法啰嗦,不如用 EL 運算式簡便,當 import 某個類之后就可以在 EL 運算式上直接讀取類的屬性,
<%@page pageEncoding="UTF-8" import="com.ajaxjs.web.captcha.CaptchaController"%>
……
<aj-page-captcha field-name="${CaptchaController.CAPTCHA_CODE}"></aj-page-captcha>
……
例子在:https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-demo/WebContent/web/captcha2.jsp,
后臺校驗驗證碼
這是一個普通的 Servlet 例子(https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-demo/src/main/java/com/demo/web/CheckCaptchaController.java),
package com.demo.web;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ajaxjs.framework.filter.CaptchaFilter;
import com.ajaxjs.web.mvc.MvcOutput;
import com.ajaxjs.web.mvc.MvcRequest;
import com.ajaxjs.web.mvc.filter.FilterContext;
@WebServlet("/CheckCaptcha")
public class CheckCaptchaController extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
MvcOutput resp = new MvcOutput(response);
FilterContext cxt = new FilterContext();
cxt.request = new MvcRequest(request);
cxt.response = resp;
try {
if (new CaptchaFilter().before(cxt)) {
resp.output("驗證碼通過!");
// 你的業務邏輯……
}
} catch (Throwable e) {
resp.output(e.toString());
}
}
}
推薦使用 MVC 控制器模式,配置過濾器 @MvcFilter(filters = { CaptchaFilter.class}),例子在 https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-demo/src/main/java/com/demo/web/CheckCaptchaMvcController.java,
package com.demo.web;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.ajaxjs.framework.BaseController;
import com.ajaxjs.framework.filter.CaptchaFilter;
import com.ajaxjs.web.mvc.IController;
import com.ajaxjs.web.mvc.filter.MvcFilter;
@Path("/CheckCaptcha-MVC")
public class CheckCaptchaMvcController implements IController {
@POST
@MvcFilter(filters = { CaptchaFilter.class})
@Produces(MediaType.APPLICATION_JSON)
public String upload() {
// 你的業務邏輯……
return BaseController.jsonOk("ok");
}
}
原理分析
驗證碼圖片哪里來的?產生驗證碼的類是 CaptchaController,統一URL路徑為 GET /Captcha,訪問該路徑即可產生驗證碼圖片,只要需要用到用到圖片驗證碼的地方,訪問這個路徑就可以獲得服務,圖片是怎么生成的?參見 CaptchaController.init() 原始碼,
/**
* 顯示驗證碼圖片并將認證碼存入 Session
*
* @param response 回應物件
* @param session 會話物件
*/
public static void init(HttpServletResponse response, HttpSession session) {
String code = getRandom();
MvcOutput re = response instanceof MvcOutput ? (MvcOutput) response : new MvcOutput(response);
re.noCache().setContent_Type("image/jpeg").go(getCaptcha(60, 20, code));
session.setAttribute(CAPTCHA_CODE, code);
}
getRandom() 回傳隨機碼,輸入到 getCaptcha() 中產生圖片,通過 MvcOutput 輸出圖片,MvcOutput 是 response 子類,擴展了不產生快取的 noCache() 方法和自動識別回應型別的 go() 方法,最后一步是保存隨機碼到服務端的 Session 會話,
要在圖片上寫入字符這是通過 Java 原生 AWT(抽象視窗工具集 Abstract Window Toolkit,它是最早的 SUN 提供的 GUI 庫圖形用戶界面)繪圖功能實作的,當前只是生成四位數字加干擾線的圖片,比較簡單,對于安全性較高的環境則需要更換為復雜的圖片生成機制,原理詳見原始碼 getCaptcha() 如下所示,
/**
* 生成驗證碼圖片
*
* @param width 圖片寬度
* @param height 圖片高度
* @param randomStr 隨機字串
* @return 圖片物件
*/
public static RenderedImage getCaptcha(int width, int height, String randomStr) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);// 在記憶體中創建影像
Graphics g = image.getGraphics(); // 獲取圖形背景關系
g.setColor(getRandColor(200, 250)); // 設定背景
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.PLAIN, 18)); // 設定字體
g.setColor(getRandColor(160, 200));
Random random = new Random();// 隨機產生干擾線
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width), y = random.nextInt(height);
int xl = random.nextInt(12), yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String[] arr = randomStr.split("");
for (int i = 0; i < 4; i++) {
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110))); // 將認證碼顯示到圖象中
g.drawString(arr[i], 13 * i + 6, 16);// 呼叫函式出來的顏色相同,可能是因為種子太接近,所以只能直接生成
}
g.dispose();// 圖象生效
return image;
}
服務端已完成產生驗證碼的任務,接下來是等待客戶端提交的引數是否正確的驗證程序,
客戶端提交表單時,必然附帶驗證碼引數,服務端方面自然要進行判斷是否具備然后再驗證,考慮到對 Session 存取是比較常規的操作,于是抽取其公共邏輯形成如下所示的抽象類,原始碼在這里,
package com.ajaxjs.web.mvc.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 存取 Session 中的值的基類
*
* @author sp42 frank@ajaxjs.com
*
*/
public abstract class SessionValueFilter implements FilterAction {
/**
* 獲取客戶端提交過來的驗證碼引數
*
* @param request 請求物件
* @param paramName 引數名稱
* @return 找到的引數值,若找不到則拋出非法引數的例外
*/
public String getClientSideArgs(HttpServletRequest request, String paramName) {
if (request.getParameter(paramName) == null)
throw new IllegalArgumentException("客戶端沒有提供引數: " + paramName);
return request.getParameter(paramName).trim();
}
/**
* 獲取服務端的 Session 中的值
*
* @param request 請求物件
* @param sessionKey Session 健名稱
* @return 找到的 Session 值,若找不到則拋出空指標例外
*/
public String getServerSideValue(HttpServletRequest request, String sessionKey) {
HttpSession session = request.getSession();
Object value = session.getAttribute(sessionKey);
if (value == null)
throw new NullPointerException("Session 中找不到 對應的 key 的值, key: " + sessionKey);
else
return (String) value;
}
}
這里順便也探討下過濾器的運作機制,如下代碼所示,根據過濾器介面定義,實作 before() 方法須回傳 boolean,可是當前 CaptchaFilter.before() 中要么回傳 true(表示通過),要么拋出例外,就是沒有回傳 false——那是為何呢?在合法的 Java 語法中,方法除了回傳 true/false 指定的型別外其實還允許拋出例外,雖然這里沒有明確回傳 true/false,但是實際上我們用throw 拋出的例外代替了回傳 false,目的是提供更豐富的語意資訊給呼叫者,好知道到底發生了具體哪些錯誤,
package com.ajaxjs.web.captcha;
import com.ajaxjs.web.mvc.filter.FilterAfterArgs;
import com.ajaxjs.web.mvc.filter.FilterContext;
import com.ajaxjs.web.mvc.filter.SessionValueFilter;
/**
* 圖形驗證碼的攔截器
*
* @author sp42 frank@ajaxjs.com
*
*/
public class CaptchaFilter extends SessionValueFilter {
@Override
public boolean before(FilterContext cxt) {
try {
String captchaCode = getClientSideArgs(cxt.request, CaptchaController.CAPTCHA_CODE),
sessionValue = getServerSideValue(cxt.request, CaptchaController.CAPTCHA_CODE);
// 判斷用戶輸入的驗證碼是否通過
if (captchaCode.equalsIgnoreCase(sessionValue)) {
cxt.request.getSession().removeAttribute(CaptchaController.CAPTCHA_CODE);// 通過之后記得要 清除驗證碼
return true;
} else {
// 是例外但不記錄到 FileHandler,例如密碼錯誤之類的
cxt.model.put(NOT_LOG_EXCEPTION, true);
throw new IllegalAccessError("驗證碼不正確");
}
} catch (Throwable e) {
if (e instanceof NullPointerException) {
cxt.model.put(NOT_LOG_EXCEPTION, true);
throw new NullPointerException("驗證碼已經過期,請重繪");
}
throw e;
}
}
@Override
public boolean after(FilterAfterArgs args) {
return true;
}
}
使用 CaptchaFilter 時候,把過濾器類參考放到 MVC 請求方法的注解上,一般無須其他的配置而且不影響其他邏輯,如下圖所示,

結語
不妨思考一個問題:到底什么時候應該要加入驗證碼?筆者認為,凡是有寫操作行為的時候應要加入驗證碼——不過也有例外:當后臺累計某些用戶行為的時候,此時也屬于寫的操作但卻不必要多此一舉使用驗證碼,
最后要說明的這個圖片驗證碼在高明的黑客面前如同虛設,在線上專案中應該使用更復雜的圖片干擾,本文旨在簡單說明驗證碼原理“拋磚引玉”所用,請用戶注意這個問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/265846.html
標籤:AI
