1. 程式人生 > 實用技巧 >API 互動中怎麼做好圖片驗證碼?

API 互動中怎麼做好圖片驗證碼?

前言

在傳統的 Web 開發過程中,處理圖形驗證碼很簡單,只需要在後臺用隨機字串生成一個圖片,將驗證碼內容放進 Session 即可,使用者提交表單時從 Session[1] 取出判斷即可。

但是現如今,越來越推崇 API 互動,無狀態,在 Session 這一塊,雖然預設配置是不支援了,但是還是有很多曲線救國的方法。

基於 Session 實現

在 API 開發中,我們也可以給前端簽發 SessionID ,並且通過php的內建方法,來實現這一切。
比如 我們與前段約定,當在請求中包含有X-Session-Id,且不為空時,表示這個會話已經註冊過 SessionID ,否則就頒佈一個 SessionID 並返回在 Response Header 中的X-Session-Id讓前段記錄這個 SessionID ,下面簡單實現一下。

// code_session.php
session_start();
// 這裡假設已經通過 Header 獲取到了 SessionID,並儲存到了 $sessionId 變數中。
// 當 SessionID 不存在,或者 為空 則建立新的 SessionID 。
if(!isset($sessionId) || empty($sessionId)){
    $sessionId = session_create_id();
    // 因為前臺還沒有 SessionID ,所以下發一個,通知前端儲存。
    header('X-Session-Id: '.$sessionId);
}
// 設定當前會話的 SessionID 。
session_id($sessionId);
// 這裡我們就可以自由的讀寫 Session 了。
// 生成驗證碼
$code = mt_rand(1e3 ,1e4-1);
// create_image 請自行實現 或者使用現有的圖形驗證碼庫生成。
$image = create_image($code);
// 儲存進去 Session
$_SESSION['code'] = $code;
// 輸出一張圖片
$image->output();

上面基本實現了生成圖片,前端需要根據 只需要再提交表單時,在 headers 中帶上X-Session-ID即可。

// code_session_validate.php

session_start();
// 這裡假設已經通過 Header 獲取到了 SessionID,並儲存到了 $sessionId 變數中。
// 當 SessionID 不存在,或者 為空 則建立新的 SessionID 。
if(
  !isset($sessionId) 
|| empty($sessionId) 
|| !isset($_POST['code']) 
|| empty($_POST['code'])
){
    // 因為沒有提交 SessionID 過來 這個肯定就是不成立的了,所以直接終止即可。
    exit;
}
// 設定當前會話的 SessionID 。
session_id($sessionId);
if($_POST['code']!=$_SESSION['code']){
    // 驗證碼錯誤啦
    exit;
}
// 驗證通過了就刪掉 code,
unset($_SESSION['code']);

上面使用 Session ,我們基本就實現了一個簡單的驗證,而且是基於 API 互動的,不依賴瀏覽器cookie 。當我們需要一些複雜的比如共享 Session ,這些就不在本文的討論範圍了(其實現在也已經超綱了)

基於客戶端主動簽發

接下來的方法是無狀態的,但是需要用到 Redis 。這裡使用 PHPRedis 這個擴充套件來處理。

在大多數情況下,我們並不需要像上面使用 Session 那樣來建立過多的 Session ,造成有一些資源浪費,當然,Session 可以做的不止這些,下面我們就用 Redis 來做一個客戶端主動簽發的圖片驗證碼。

理論原理

由客戶端本地生成隨機字串,然後拼接在獲取驗證碼地址的後面,後端擷取客戶端生成的隨機字串,用此作為驗證憑證放入 Redis 中去,再客戶端提交時需要帶上先前生成的隨機字串一同進項驗證。

// code_client.php
$salt = 'wertyujkdbaskndasda';
if(!isset($_GET['sign'])){
    // 客戶端沒有提供簽名,停止執行
    exit;
}
// 使用者傳來的一切資料都是不可靠的,我們需要對其加鹽後執行 md5
$sign = md5($_GET['sign'].$salt);
// 拼接上簽名作為 Redis 的 key
$key = 'code:'.$sign;
// 連線 Redis 
$cache = new \Redis();
// 生成驗證碼
$code = mt_rand(1e3,1e4-1);
// 儲存驗證碼到 Redis 並設定2分鐘的有效期。
if($cache->exists($key)){
    // 這個 Key 已經被佔用了,這裡先停止。
    exit;
}
$cache->set($key,$code,60*2);
// 建立圖片並返回
$image = create_image($code);
$image->output();

好了,接下來驗證一下。

// code_client_validate.php
$salt = 'wertyujkdbaskndasda';
if(
!isset($_POST['sign'])
|| !isset($_POST['code']) // 沒有提交驗證碼過來。
|| !empty($_POST['code'])
){
    // 客戶端沒有提供簽名,停止執行
    exit;
}
// 使用者傳來的一切資料都是不可靠的,我們需要對其加鹽後執行 md5
$sign = md5($_POST['sign'].$salt);
// 拼接上簽名作為 Redis 的 key
$key = 'code:'.$sign;
// 連線 Redis 
$cache = new \Redis();

if(!$cache->exists($key)){
    // 根本沒有這個 key
    eixt;
}

if($cache->get($key)!=$_POST['code']){
    // 驗證碼錯誤
}

// 驗證通過了就刪除

$cache->del($key);

看著是不是要複雜點兒,甚至還用上了 Redis ,雖然看著不咋地,但是他也實現了我們想要的,不過這個也不算是太好的方案,而且,還要考慮客戶端字串不夠隨機的情況,接下來我們改變一下方向,換成服務端簽發。

基於服務端簽發

剛剛的是基於客戶端簽發的實現,下面來提供另一種思路,但是大體上,這個是差不多的哈都。

理論原理

同樣是簽發 Sign ,只不過這次由服務端來簽發,然後將 Sign 通過 Header 傳送給客戶端,客戶端需要先取到圖片資源,注意這裡返回的應該是一個合法的二進位制流,然後從 header 中取出 Sign ,同時展示給使用者。

// code_server.php
$cache = new \Redis();
$salt = 'wertyujkdbaskndasda';
function generateSign(){
    global $cache,$salt;
    $sign = md5(mt_rand().$salt);
    // 拼接上簽名作為 Redis 的 key
    $key = 'code:'.$sign;
    if($cache->exists($key)){
        // 是的 你麼有看錯,就是如果生成的 Sign 已存在,就進行遞迴,直到生成出一個不存在的。
        return generateSign();
    }
    return $key;
}
// 連線 Redis 
$key = generateSign();
// 生成驗證碼
$code = mt_rand(1e3,1e4-1);
// 儲存驗證碼到 Redis 並設定2分鐘的有效期。
$cache->set($key,$code,60*2);
// 建立圖片並返回
$image = create_image($code);
// 哈哈 要剃掉字首喲
header('X-Captcha-Sign: ' . str_replace('code:','',$key));
$image->output();

看起來幾乎沒有變化,只是生成 Sign 的方式變了一下,但是,這樣搞的話,前端同學可能就不爽了,他們要先獲取這個資源和 headers 中的X-Captcha-Sign再 show 到介面上,當然 可以直接將結果 base64 或者 直接用用二進位制流生成點陣圖顯示都是可以的,我們只是需要可以驗證,驗證方法直接使用上面的即可。

資源搜尋網站大全 https://www.renrenfan.com.cn

特別注意

當你使用 ajax 獲取這個資源是,如果你的業務涉及到了跨域,你還需要在響應頭設定Access-Control-Expose-Headers - HTTP | MDN,否則 ajax 無法獲取自定義的響應頭。。

header('Access-Control-Expose-Headers: X-Captcha-Sign');

總結

看了這三種解決方案,基本都能滿足我們的需求,可能還有人想到了另一種方案。提供一個json 介面名,在後臺生成圖片然後儲存起來,返回 url 和 sign 給前端,這樣就好了,但是這樣做,我們的資源並不太可控,會造成一定的資源浪費,這裡我並沒有考慮 這種方案。