如今的驗證碼真是愈來愈高級了,12306 的找圖驗證碼,極驗的拖動式驗證碼,還有國外的一些黑科技,能智能判斷你是否是機器人的驗證碼。php
驗證碼的更新迭代讓我忽然對傳統驗證碼一會兒不知足了,出於挑戰自我和對本身技能的修煉,我用了一週的時間寫了一個簡單的 demo ,而後又花了一週時間將其優化成插件的形式,因而 Clicaptcha 就誕生了。html
簡單介紹下 Clicaptcha ,它是由 click 和 captcha 這兩個單子合併而成,顧名思義,這是一個點擊驗證碼,那怎麼個點擊驗證呢?整個操做流程只需根據提示文字信息,點擊圖中文字所在位置,便可完成驗證,效果圖下圖:前端
具體的功能實現這裏就不一步步給你們回顧了,感興趣的能夠直接上 oschina 或者 github ,搜索 Clicaptcha 就能夠看到這個項目。git
下面我主要是記錄一下在開發過程當中幾個難點,以及個人解決思路,若是你有更好的,但願你能和我交流交流。github
首先咱們要作一些準備工做:算法
準備好這些後,就能夠開始考慮咱們的隨機佈局算法了,其實並不複雜,若是有看過我以前寫的《絕對定位的層判斷是否有相互覆蓋的解決算法》,其實思路是差很少的。chrome
能夠看下上面這張圖,假設中間帶背景色的區域是已經固定的一個區域,當第2個區域要進行隨機生成的時候,大概會有4種狀況,也就是圖中的這4種,咱們只需依次判斷如下4種條件,只要有一項符合,則這個隨機生成的x,y座標就可使用。後端
x2 + w2 < x1數組
x1 + w1 < x2安全
y2 + h2 < y1
y1 + h1 < y2
其實這倒不算個難點,就是個小細節。
GD 庫裏的 imagefttext 方法中,設置字體大小並非以像素(px)爲單位的,而是以磅(point)爲單位。因此在具體使用的時候,須要進行轉換,也就是乘以 0.75 ,好比你須要在圖片上展現 50px 大小的字體,則須要 50px * 0.75 = 37.5point 。至於爲何是乘以 0.75 ,能夠見下表:
八號 = 5磅(7px) = (5/72)*96 = 6.67 = 6px
七號 = 5.5磅 = (5.5/72)*96 = 7.3 = 7px
小六 = 6.5磅 = (6.5/72)*96 = 8.67 = 8px
六號 = 7.5磅 = (7.5/72)*96 = 10px
小五 = 9磅 = (9/72)*96 = 12px
五號 = 10.5磅 = (10.5/72)*96 = 14px
小四 = 12磅 = (12/72)*96 = 16px
四號 = 14磅 = (14/72)*96 = 18.67 = 18px
小三 = 15磅 = (15/72)*96 = 20px
三號 = 16磅 = (16/72)*96 = 21.3 = 21px
小二 = 18磅 = (18/72)*96 = 24px
二號 = 22磅 = (22/72)*96 = 29.3 = 29px
小一 = 24磅 = (24/72)*96 = 32px
一號 = 26磅 = (26/72)*96 = 34.67 = 34px
小初 = 36磅 = (36/72)*96 = 48px
初號 = 42磅 = (42/72)*96 = 56px
咱們都知道,在 PHP 中,經過設置 header 的參數,能夠輸出各類文件類型,但一次只能輸出一種數據格式到客戶端。
在我這個項目中,除了圖片須要輸出,同時還須要將提示文字也輸出,否則用戶就不知道依次點哪些文字進行驗證了。
解決這個問題我想到有兩種解決方案:
最終我是選擇了第二套方案,由於這是個驗證碼插件,若是每次生成的驗證圖片都保存下來,對服務器硬盤資源佔用將是個大問題。
在我將後端代碼所有開發完成,前端也封裝好了一個 jQuery 插件後,發現了一個大問題,就是若是用戶經過特殊手段跳過驗證碼驗證,直接提交表單或者相關業務操做怎麼辦?
由於驗證碼是以插件的形式存在,因此在調用的參數裏有一個 callback 參數,用於驗證成功後執行網站自己業務邏輯的代碼。這樣就可能會有個問題,我用 chrome 按 F12 打開開發者工具,直接在任務臺裏輸入了提交表單的代碼並回車執行,而後表單順利提交了,完徹底全跳過了驗證。
解決這個問題也不復雜,我思考了傳統驗證碼的驗證流程,核心一點就是它是隨表單一塊兒提交併作驗證的,但因爲我這個驗證碼的特殊性,因此只能增長一個後端二次驗證,也就是前端初步驗證後,將驗證信息隨表單提交到後端進行二次驗證便可,同時,後端的二次驗證成功後,將 session 清除,避免重複刷新提交表單形成能跳過二次驗證的問題。
以上就是我對這個項目的難點總結,若是你看到這了,但願對感興趣的你有點啓發,這個項目我同時放在的 OSchina 和 Github 上,在線演示,有興趣的能夠關注下。
如下是針對前兩個難點寫的一個小demo,若是對完整的源碼一時半會難理解的話,能夠 copy 如下代碼到本地,替換下字體和圖片,而後運行一下看看效果。
header("Content-type: text/html; charset=utf-8"); error_reporting(E_ERROR | E_WARNING | E_PARSE); $imagePath = 'bg.jpg'; $fontPath = 'msyh.ttc'; //爲何要乘0.75?由於 imagefttext 方法裏的 size 參數使用磅(point)作爲單位的,因此須要進行轉換,轉換爲像素 $fontSize = 50 * 0.75; //以「博客園」三個字舉例,將文字、尺寸等信息存入數組 foreach(array('博', '客', '園') as $v){ $fontarea = imagettfbbox($fontSize, 0, $fontPath, $v); $textWidth = $fontarea[2] - $fontarea[0]; $textHeight = $fontarea[1] - $fontarea[7]; $tmp['text'] = $v; $tmp['size'] = $fontSize; $tmp['width'] = $textWidth; $tmp['height'] = $textHeight; $textArr[] = $tmp; } //獲取背景底圖寬高和類型信息 list($imageWidth, $imageHeight, $imageType) = getimagesize($imagePath); //隨機生成漢字位置,並附加存入數組 foreach($textArr as &$v){ list($x, $y) = randPosition($textArr, $imageWidth, $imageHeight, $v['width'], $v['height']); $v['x'] = $x; $v['y'] = $y; } unset($v); //建立圖片的實例 $image = imagecreatefromstring(file_get_contents($imagePath)); //字體顏色 $color = imagecolorallocate($image, 0, 0, 0); //繪畫文字 foreach($textArr as $v){ imagefttext($image, $v['size'], 0, $v['x'], $v['y'], $color, $fontPath, $v['text']); } //生成圖片 switch($imageType){ case 1://GIF header('Content-Type: image/gif'); imagegif($image); break; case 2://JPG header('Content-Type: image/jpeg'); imagejpeg($image); break; case 3://PNG header('Content-Type: image/png'); imagepng($image); break; default: break; } imagedestroy($image); //隨機生成位置佈局 function randPosition($textArr, $imgW, $imgH, $fontW, $fontH){ $return = array(); $x = rand(0, $imgW - $fontW); $y = rand($fontH, $imgH); if(!checkPosition($textArr, $x, $y, $fontW, $fontH)){ $return = randPosition($textArr, $imgW, $imgH, $fontW, $fontH); }else{ $return = array($x, $y); } return $return; } function checkPosition($textArr, $x, $y, $w, $h){ $flag = true; foreach($textArr as $v){ if(isset($v['x']) && isset($v['y'])){ //分別判斷X和Y是否都有交集,若是都有交集,則判斷爲覆蓋 $flagX = true; if($v['x'] > $x){ if($x + $w > $v['x']){ $flagX = false; } }else if($x > $v['x']){ if($v['x'] + $v['width'] > $x){ $flagX = false; } }else{ $flagX = false; } $flagY = true; if($v['y'] > $y){ if($y + $h > $v['y']){ $flagY = false; } }else if($y > $v['y']){ if($v['y'] + $v['height'] > $y){ $flagY = false; } }else{ $flagY = false; } if(!$flagX && !$flagY){ $flag = false; } } } return $flag; }
參考資料: