天天看點

仿12306的圖檔驗證碼

由于要做一個新項目,是以打算做一個簡單的圖檔驗證碼。

先說說思路吧:在服務端,從一個檔案夾裡面找出8張圖檔,再把8張圖檔合并成一張大圖,在8個小圖裡面随機生成一個要使用者驗證的圖檔分類,如小狗、啤酒等。在前端,通路這個頁面時,把圖檔加載上去,使用者在圖檔上選擇提示所需要的圖檔,當使用者點登陸時,根據使用者選擇的所有坐标判斷所選的圖檔是不是實際上的驗證圖檔。

先放兩張效果圖:

仿12306的圖檔驗證碼
仿12306的圖檔驗證碼
為了讓檔案查找比較簡單,在圖檔檔案結構上可以這樣:
仿12306的圖檔驗證碼

這樣友善生成使用者要選擇的Key圖檔,和取出8張小圖合并成大圖。

上代碼:這是選擇8張圖檔,并且在每張圖檔選取時用遞歸保證選取的圖檔不會重複。

//選取8個圖檔
	public static List<Object> getEightImages() {
		//儲存取到的每一個圖檔的path,保證圖檔不會重複
		List<String> paths = new ArrayList<String>();
		
		File[] finalImages = new File[8];
		List<Object> object = new ArrayList<Object>();
		
		//儲存tips
		String[] tips = new String[8];
		
		for (int i = 0; i < 8; i++) {
			//擷取随機的二級目錄
			int dirIndex = getRandom(secondaryDirNumbers);
			File secondaryDir = getFiles()[dirIndex];
			
			//随機到的檔案夾名稱儲存到tips中
			tips[i] = secondaryDir.getName();
			
			//擷取二級圖檔目錄下的檔案
			File[] images = secondaryDir.listFiles();
			
			int imageIndex = getRandom(imageRandomIndex);
			File image = images[imageIndex];
			
			//圖檔去重
			image = dropSameImage(image, paths, tips, i);		
			
			paths.add(image.getPath());

			finalImages[i] = image;
			
		}
		object.add(finalImages);
		object.add(tips);
		return object;
	}    
      

在生成這8張圖檔中,用一個數組儲存所有的檔案分類。在這個分類裡面可以用随機數選取一個分類做為Key分類,就是使用者要選擇的所有圖檔。由于數組是有序的,可以周遊數組中的元素,擷取每個key分類圖檔的位置,友善在使用者驗證時,進行比對。

//擷取位置,傳回的是第幾個圖檔,而不是下标,從1開始,集合第一個元素為tip
	public static List<Object> getLocation(String[] tips) {
		List<Object> locations = new ArrayList<Object>();
	
		//擷取Key分類
		String tip = getTip(tips);
		locations.add(tip);
		
		int length = tips.length;
		for (int i = 0; i < length; i++) {
			if (tip.equals(tips[i])) {

				locations.add(i+1);
			}
		}
		return locations;
	}
      

選取了8張圖檔後,接下來就是合并圖檔。合并圖檔可以用到BufferedImage這個類的方法:setRGB()這個方法如果不明白可以看下api文檔,上面有詳細的說明。

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++) {
            File image = finalImages[i];
            
            BufferedImage bufferedImage = ImageIO.read(image);
            int width = bufferedImage.getWidth();
            int height = bufferedImage.getHeight();
            //從圖檔中讀取RGB
            int[] imageBytes = new int[width*height];
            imageBytes = bufferedImage.getRGB(0, 0, width, height, imageBytes, 0, width);
            if ( i < 4) {
                mergeImage.setRGB(i*200, 0, width, height, imageBytes, 0, width);
            } else {
                mergeImage.setRGB((i -4 )*200, 200, width, height, imageBytes, 0, width);
            }            
            
        }

     
        ImageIO.write(mergeImage, "jpg", response.getOutputStream());
        //ImageIO.write(mergeImage, "jpg", destImage);
    }
      

  在controller層中,先把key分類儲存到session中,為使用者選擇圖檔分類做提示和圖檔驗證做判斷。然後把圖檔流輸出到response中,就可以生成驗證圖檔了。

  

response.setContentType("image/jpeg");  
        response.setHeader("Pragma", "No-cache");  
        response.setHeader("Cache-Control", "no-cache");  
        response.setDateHeader("Expires", 0);
        
        List<Object> object = ImageSelectedHelper.getEightImages();
        File[] finalImages = (File[]) object.get(0);
        
        String[] tips = (String[]) object.get(1);
        //所有key的圖檔位置,即使用者必須要選的圖檔
        List<Object> locations = ImageSelectedHelper.getLocation(tips);
        
        String tip = locations.get(0).toString();
        System.out.println(tip);
        session.setAttribute("tip", tip);
        locations.remove(0);
        
        int length = locations.size();
        for (int i = 0; i < length; i++) {
            System.out.println("實際Key圖檔位置:" + locations.get(i));
        }

        session.setAttribute("locations", locations);
        ImageMerge.mergeImage(finalImages, response);      

  在jsp中,為使用者的點選生成小圖檔标記。當使用者點圖檔擊時,在父div上添加一個子div标簽,并且把他定位為relative, 并且設定zIndex,然後再這個div上添加一個img标簽,定位為absolute。在使用者的點選時,可以擷取點選事件,根據點選事件擷取點選坐标,然後減去父div的坐标,就可以擷取相對坐标。可以根據自己的喜好定坐标原點,這裡的坐标原點是第8個圖檔的右下角。

  <div>
        <div id="base">
            <img src="<%=request.getContextPath()%>/identify" style="width: 300px; height: 150px;" onclick="clickImg(event)" id="bigPicture">
        </div>
        
    </div>

function clickImg(e) {
		var baseDiv = document.getElementById("base");
  		var topValue = 0;
		var leftValue = 0;
		var obj = baseDiv;
		while (obj) {
			leftValue += obj.offsetLeft;
			topValue +=obj.offsetTop;
			obj = obj.offsetParent;
		}
		//解決firefox擷取不到點選事件的問題
		var clickEvent = e ? e : (window.event ? window.event : null);
				
		var clickLeft = clickEvent.clientX + document.body.scrollLeft - document.body.clientLeft - 10;
		var clickTop = clickEvent.clientY + document.body.scrollTop - document.body.clientTop - 10;
		var divId = "img_" + index++;
		
		var divEle = document.createElement("div");
		
		divEle.setAttribute("id", divId);
		divEle.style.position = "relative";
		divEle.style.zIndex = index;
		divEle.style.width = "20px";
		divEle.style.height = "20px";
		divEle.style.display = "inline";
		
		divEle.style.top = clickTop - topValue - 150 + 10 + "px";
		divEle.style.left = clickLeft - leftValue - 300 + "px";
		
		divEle.setAttribute("onclick", "remove('" + divId + "')");
		baseDiv.appendChild(divEle);
		
		var imgEle = document.createElement("img");
		imgEle.src = "<%=request.getContextPath()%>/resources/timo.png";
		imgEle.style.width = "20px";
		imgEle.style.height = "20px";
		imgEle.style.top = "0px";
		imgEle.style.left = "0px";
		imgEle.style.position = "absolute";
		imgEle.style.zIndex = index;
		divEle.appendChild(imgEle);
	}
      

使用者選擇登入後,伺服器端根據使用者的選擇坐标進行判斷

public List<Integer> isPass(String result) {
		
		String[] xyLocations = result.split(",");
		//儲存使用者選擇的坐标落在哪些圖檔上
		List<Integer> list = new ArrayList<Integer>();
		//每一組坐标
		System.out.println("使用者選擇圖檔數:"+xyLocations.length);
		for (String xyLocation : xyLocations) {
			String[] xy = xyLocation.split("\\|\\|");
			int x = Integer.parseInt(xy[0]);
			int y = Integer.parseInt(xy[1]);
			
			//8,4圖檔區間
			if ( x > -75 && x <= 0) {

				if ( y > -75 && y <= 0) {		//8号
					list.add(8);
	
				} else if ( y >= -150 && y <= -75 ) {		//4号
					list.add(4);
				}
			} else if ( x > -150 && x <= -75) {		//7,3圖檔區間
				
				if ( y > -75 && y <= 0) {		//7号
					list.add(7);
	
				} else if ( y >= -150 && y <= -75 ) {		//3号
					list.add(3);
				}
			} else if ( x > -225 && x <= -150) {		//6,2圖檔區間
				
				if ( y > -75 && y <= 0) {		//6号
					list.add(6);
	
				} else if ( y >= -150 && y <= -75 ) {		//2号
					list.add(2);
				}
				
			} else if ( x >= -300 && x <= -225) {		//5,1圖檔區間
				
				if ( y > -75 && y <= 0) {		//5号
					list.add(5);
	
				} else if ( y >= -150 && y <= -75 ) {		//1号
					list.add(1);
				}
			} else {
				return null;
			}
		}
		return list;
	}
      

重新整理生成新的圖檔,由于ajax不支援二進制流,可以自己用原生的xmlHttpRequest對象加html5的blob來完成。

function refresh() {
        var url = "<%=request.getContextPath()%>/identify";
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = "blob";
        xhr.onload = function() {
            if (this.status == 200) {
                var blob = this.response;                
                //加載成功後釋放blob
                bigPicture.onload = function(e) {
                    window.URL.revokeObjectURL(bigPicture.src); 
                };
                bigPicture.src = window.URL.createObjectURL(blob);
            }
        }
        xhr.send();
      

 驗證碼整體代碼完成了,還有有一些細節要處理。由于圖檔容易被百度識圖,要對生成的圖檔做模糊處理,暫時還沒想到什麼好的辦法~