參考 12306 驗證碼交互模式:服務端從素材庫隨機抽取 8 張小圖拼合成大圖,並隨機指定一類目標圖片(如「小狗」、「啤酒」)作為驗證目標。前端展示大圖,用戶點擊所有屬於目標類別的小圖,提交坐標後端驗證。
BufferedImage.setRGB() 將 8 張 200x200 小圖拼成 800x400 大圖(2 行 4 列)。XMLHttpRequest + responseType=blob + createObjectURL 實現無刷新更新圖片。public static List<Object> getEightImages() {
List<String> paths = new ArrayList<>();
File[] finalImages = new File[8];
String[] tips = new String[8];
for (int i = 0; i < 8; i++) {
int dirIndex = getRandom(secondaryDirNumbers);
File secondaryDir = getFiles()[dirIndex];
tips[i] = secondaryDir.getName(); // 類別標籤
File[] images = secondaryDir.listFiles();
File image = images[getRandom(images.length)];
image = dropSameImage(image, paths, tips, i); // 遞歸去重
paths.add(image.getPath());
finalImages[i] = image;
}
List<Object> obj = new ArrayList<>();
obj.add(finalImages);
obj.add(tips);
return obj;
}
public static List<Object> getLocation(String[] tips) {
List<Object> loc = new ArrayList<>();
String tip = getTip(tips); // 隨機選一個類別作為 Key
loc.add(tip);
for (int i = 0; i < tips.length; i++) {
if (tip.equals(tips[i])) loc.add(i + 1); // 記錄位置(1-based)
}
return loc;
}
public static void mergeImage(File[] finalImages, HttpServletResponse response) throws IOException {
BufferedImage mergeImage = new BufferedImage(800, 400, BufferedImage.TYPE_INT_BGR);
for (int i = 0; i < 8; i++) {
BufferedImage bi = ImageIO.read(finalImages[i]);
int[] rgb = bi.getRGB(0, 0, bi.getWidth(), bi.getHeight(), null, 0, bi.getWidth());
if (i < 4) mergeImage.setRGB(i * 200, 0, bi.getWidth(), bi.getHeight(), rgb, 0, bi.getWidth());
else mergeImage.setRGB((i - 4) * 200, 200, bi.getWidth(), bi.getHeight(), rgb, 0, bi.getWidth());
}
ImageIO.write(mergeImage, "jpg", response.getOutputStream());
}
@GetMapping("/identify")
public void identify(HttpSession session, HttpServletResponse response) throws IOException {
response.setContentType("image/jpeg");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
List<Object> obj = ImageSelectedHelper.getEightImages();
File[] images = (File[]) obj.get(0);
String[] tips = (String[]) obj.get(1);
List<Object> loc = ImageSelectedHelper.getLocation(tips);
session.setAttribute("tip", loc.get(0)); // 目標類別
loc.remove(0);
session.setAttribute("locations", loc); // 目標位置列表
ImageMerge.mergeImage(images, response);
}
function clickImg(e) {
var baseDiv = document.getElementById("base");
var rect = baseDiv.getBoundingClientRect();
var clickX = e.clientX - rect.left;
var clickY = e.clientY - rect.top;
// 根據 clickX, clickY 計算小圖索引 (1-8),創建標記 div+img 顯示選中
// 將選中索引存入隱藏域或 JS 數組,提交時發送
}
public List<Integer> isPass(String result) {
String[] xyLocations = result.split(",");
List<Integer> userSel = new ArrayList<>();
for (String xy : xyLocations) {
int x = Integer.parseInt(xy.split("\\|\\|")[0]);
int y = Integer.parseInt(xy.split("\\|\\|")[1]);
// 根據 800x400 大圖中 200x200 小圖區域判斷索引
if (x > -75 && x <= 0) { if (y > -75 && y <= 0) userSel.add(8); else if (y >= -150 && y <= -75) userSel.add(4); }
else if (x > -150 && x <= -75) { if (y > -75 && y <= 0) userSel.add(7); else if (y >= -150 && y <= -75) userSel.add(3); }
else if (x > -225 && x <= -150) { if (y > -75 && y <= 0) userSel.add(6); else if (y >= -150 && y <= -75) userSel.add(2); }
else if (x >= -300 && x <= -225) { if (y > -75 && y <= 0) userSel.add(5); else if (y >= -150 && y <= -75) userSel.add(1); }
else return null;
}
return userSel;
}
function refresh() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/identify', true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status === 200) {
var blob = this.response;
bigPicture.onload = () => URL.revokeObjectURL(bigPicture.src);
bigPicture.src = URL.createObjectURL(blob);
}
};
xhr.send();
}
getBoundingClientRect() 替代手動累加 offset,兼容性更好。