本文主要介紹了掃碼登入的原理及整體流程,包含了二維條碼的生成/獲取、過期失效的處理、登入狀態的監聽。
為方便理解,我簡單畫了一個 UML 時序圖,用以描述掃碼登入的大致流程!
總結下核心流程:
請求業務伺服器獲取用以登入的二維條碼和 UUID。
通過 websocket 連線 socket 伺服器,並定時(時間間隔依據伺服器設定時間調整)傳送心跳保持連線。
使用者通過 APP 掃描二維條碼,傳送請求到業務伺服器處理登入。根據 UUID 設定登入結果。
socket 伺服器通過監聽獲取登入結果,建立 session 資料,根據 UUID 推播登入資料到使用者瀏覽器。
使用者登入成功,伺服器主動將該 socker 連線從連線池中剔除,該二維條碼失效。
也就是 UUID,這是貫穿整個流程的紐帶,一個閉環登入過程,每一步業務處理都是圍繞該次的 UUD 進行處理的。UUID 的生成有根據 session_id 的也有根據用戶端 ip 地址的。個人還是建議每個二維條碼都有單獨的 UUID,適用場景更廣一些!
前端肯定是要和伺服器保持一直通訊的,用以獲取登入結果和二維條碼狀態。看了下網上的一些實現方案,基本各個方案都有用的:輪詢、長輪詢、長連結、websocket。也不能肯定的說哪個方案好哪個方案不好,只能說哪個方案更適用於當前應用場景。個人比較建議使用長輪詢、websocket 這種比較節省伺服器效能的方案。
掃碼登入的好處顯而易見,一是人性化,再就是防止密碼洩漏。但是新方式的接入,往往也伴隨著新的風險。所以,很有必要再整體過程中加入適當的安全機制。例如:
程式碼實現和原始碼後面會給出。
可以看到使用者請求的二維條碼資源,並獲取到了 qid
。
獲取二維條碼時候會建立相應快取,並設定過期時間:
之後會連線 socket 伺服器,定時傳送心跳。
此時 socket 伺服器會有相應連線紀錄檔輸出:
伺服器驗證並處理登入,建立 session,建立對應的快取:
Socket 伺服器讀取到快取,開始推播資訊,並關閉剔除連線:
前端獲取資訊,處理登入:
注意:本 Demo 只是個人學習測試,所以並未做太多安全機制!
使用 Nginx 作為代理 socke 伺服器。可使用域名,方便做負載均衡。本次測試域名:loc.websocket.net
websocker.conf
server { listen 80; server_name loc.websocket.net; root /www/websocket; index index.php index.html index.htm; #charset koi8-r; access_log /dev/null; #access_log /var/log/nginx/nginx.localhost.access.log main; error_log /var/log/nginx/nginx.websocket.error.log warn; #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } location / { proxy_pass http://php-cli:8095/; proxy_http_version 1.1; proxy_connect_timeout 4s; proxy_read_timeout 60s; proxy_send_timeout 12s; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } }
使用 PHP 構建的 socket 伺服器。實際專案中大家可以考慮使用第三方應用,穩定性更好一些!
QRServer.php
<?php require_once dirname(dirname(__FILE__)) . '/Config.php'; require_once dirname(dirname(__FILE__)) . '/lib/RedisUtile.php'; require_once dirname(dirname(__FILE__)) . '/lib/Common.php';/** * 掃碼登陸伺服器端 * Class QRServer * @author BNDong */class QRServer { private $_sock; private $_redis; private $_clients = array(); /** * socketServer constructor. */ public function __construct() { // 設定 timeout set_time_limit(0); // 建立一個通訊端(通訊節點) $this->_sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Could not create socket" . PHP_EOL); socket_set_option($this->_sock, SOL_SOCKET, SO_REUSEADDR, 1); // 係結地址 socket_bind($this->_sock, Config::QRSERVER_HOST, Config::QRSERVER_PROT) or die("Could not bind to socket" . PHP_EOL); // 監聽通訊端上的連線 socket_listen($this->_sock, 4) or die("Could not set up socket listener" . PHP_EOL); $this->_redis = libRedisUtile::getInstance(); } /** * 啟動服務 */ public function run() { $this->_clients = array(); $this->_clients[uniqid()] = $this->_sock; while (true){ $changes = $this->_clients; $write = NULL; $except = NULL; socket_select($changes, $write, $except, NULL); foreach ($changes as $key => $_sock) { if($this->_sock == $_sock){ // 判斷是不是新接入的 socket if(($newClient = socket_accept($_sock)) === false){ die('failed to accept socket: '.socket_strerror($_sock)."n"); } $buffer = trim(socket_read($newClient, 1024)); // 讀取請求 $response = $this->handShake($buffer); socket_write($newClient, $response, strlen($response)); // 傳送響應 socket_getpeername($newClient, $ip); // 獲取 ip 地址 $qid = $this->getHandQid($buffer); $this->log("new clinet: ". $qid); if ($qid) { // 驗證是否存在 qid if (isset($this->_clients[$qid])) $this->close($qid, $this->_clients[$qid]); $this->_clients[$qid] = $newClient; } else { $this->close($qid, $newClient); } } else { // 判斷二維條碼是否過期 if ($this->_redis->exists(libCommon::getQidKey($key))) { $loginKey = libCommon::getQidLoginKey($key); if ($this->_redis->exists($loginKey)) { // 判斷使用者是否掃碼 $this->send($key, $this->_redis->get($loginKey)); $this->close($key, $_sock); } $res = socket_recv($_sock, $buffer, 2048, 0); if (false === $res) { $this->close($key, $_sock); } else { $res && $this->log("{$key} clinet msg: " . $this->message($buffer)); } } else { $this->close($key, $this->_clients[$key]); } } } sleep(1); } } /** * 構建響應 * @param string $buf * @return string */ private function handShake($buf){ $buf = substr($buf,strpos($buf,'Sec-WebSocket-Key:') + 18); $key = trim(substr($buf, 0, strpos($buf,"rn"))); $newKey = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true)); $newMessage = "HTTP/1.1 101 Switching Protocolsrn"; $newMessage .= "Upgrade: websocketrn"; $newMessage .= "Sec-WebSocket-Version: 13rn"; $newMessage .= "Connection: Upgradern"; $newMessage .= "Sec-WebSocket-Accept: " . $newKey . "rnrn"; return $newMessage; } /** * 獲取 qid * @param string $buf * @return mixed|string */ private function getHandQid($buf) { preg_match("/^[sn]?GETs+/?qid=([a-z0-9]+)s+HTTP.*/", $buf, $matches); $qid = isset($matches[1]) ? $matches[1] : ''; return $qid; } /** * 編譯傳送資料 * @param string $s * @return string */ private function frame($s) { $a = str_split($s, 125); if (count($a) == 1) { return "x81" . chr(strlen($a[0])) . $a[0]; } $ns = ""; foreach ($a as $o) { $ns .= "x81" . chr(strlen($o)) . $o; } return $ns; } /** * 解析接收資料 * @param resource $buffer * @return null|string */ private function message($buffer){ $masks = $data = $decoded = null; $len = ord($buffer[1]) & 127; if ($len === 126) { $masks = substr($buffer, 4, 4); $data = substr($buffer, 8); } else if ($len === 127) { $masks = substr($buffer, 10, 4); $data = substr($buffer, 14); } else { $masks = substr($buffer, 2, 4); $data = substr($buffer, 6); } for ($index = 0; $index < strlen($data); $index++) { $decoded .= $data[$index] ^ $masks[$index % 4]; } return $decoded; } /** * 傳送訊息 * @param string $qid * @param string $msg */ private function send($qid, $msg) { $frameMsg = $this->frame($msg); socket_write($this->_clients[$qid], $frameMsg, strlen($frameMsg)); $this->log("{$qid} clinet send: " . $msg); } /** * 關閉 socket * @param string $qid * @param resource $socket */ private function close($qid, $socket) { socket_close($socket); if (array_key_exists($qid, $this->_clients)) unset($this->_clients[$qid]); $this->_redis->del(libCommon::getQidKey($qid)); $this->_redis->del(libCommon::getQidLoginKey($qid)); $this->log("{$qid} clinet close"); } /** * 紀錄檔記錄 * @param string $msg */ private function log($msg) { echo '['. date('Y-m-d H:i:s') .'] ' . $msg . "n"; } } $server = new QRServer(); $server->run();
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>掃碼登入 - 測試頁面</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="./public/css/main.css"> </head> <body translate="no"> <p class='box'> <p class='box-form'> <p class='box-login-tab'></p> <p class='box-login-title'> <p class='i i-login'></p><h2>登入</h2> </p> <p class='box-login'> <p class='fieldset-body' id='login_form'> <button onclick="openLoginInfo();" class='b b-form i i-more' title='Mais Informa??es'></button> <p class='field'> <label for='user'>使用者賬戶</label> <input type='text' id='user' name='user' title='Username' placeholder="請輸入使用者賬戶/郵箱地址" /> </p> <p class='field'> <label for='pass'>使用者密碼</label> <input type='password' id='pass' name='pass' title='Password' placeholder="情輸入賬戶密碼" /> </p> <label class='checkbox'> <input type='checkbox' value='TRUE' title='Keep me Signed in' /> 記住我 </label> <input type='submit' id='do_login' value='登入' title='登入' /> </p> </p> </p> <p class='box-info'> <p><button onclick="closeLoginInfo();" class='b b-info i i-left' title='Back to Sign In'></button><h3>掃碼登入</h3> </p> <p class='line-wh'></p> <p style="position: relative;"> <input type="hidden" id="qid" value=""> <p id="qrcode-exp">二維條碼已失效<br>點選重新獲取</p> <img id="qrcode" src="" /> </p> </p> </p> <script src='./public/js/jquery.min.js'></script> <script src='./public/js/modernizr.min.js'></script> <script id="rendered-js"> $(document).ready(function () { restQRCode(); openLoginInfo(); $('#qrcode-exp').click(function () { restQRCode(); $(this).hide(); }); }); /** * 開啟二維條碼 */ function openLoginInfo() { $(document).ready(function () { $('.b-form').css("opacity", "0.01"); $('.box-form').css("left", "-100px"); $('.box-info').css("right", "-100px"); }); } /** * 關閉二維條碼 */ function closeLoginInfo() { $(document).ready(function () { $('.b-form').css("opacity", "1"); $('.box-form').css("left", "0px"); $('.box-info').css("right", "-5px"); }); } /** * 重新整理二維條碼 */ var ws, wsTid = null; function restQRCode() { $.ajax({ url: 'http://localhost/qrcode/code.php', type:'post', dataType: "json", async: false, success:function (result) { $('#qrcode').attr('src', result.img); $('#qid').val(result.qid); } }); if ("WebSocket" in window) { if (typeof ws != 'undefined'){ ws.close(); null != wsTid && window.clearInterval(wsTid); } ws = new WebSocket("ws://loc.websocket.net?qid=" + $('#qid').val()); ws.onopen = function() { console.log('websocket 已連線上!'); }; ws.onmessage = function(e) { // todo: 本函數做登入處理,登入判斷,建立快取資訊! console.log(e.data); var result = JSON.parse(e.data); console.log(result); alert('登入成功:' + result.name); }; ws.onclose = function() { console.log('websocket 連線已關閉!'); $('#qrcode-exp').show(); null != wsTid && window.clearInterval(wsTid); }; // 傳送心跳 wsTid = window.setInterval( function () { if (typeof ws != 'undefined') ws.send('1'); }, 50000 ); } else { // todo: 不支援 WebSocket 的,可以使用 js 輪詢處理,這裡不作該功能實現! alert('您的瀏覽器不支援 WebSocket!'); } }</script> </body> </html>
測試使用,模擬登入處理,未做安全認證!!
<?php require_once dirname(__FILE__) . '/lib/RedisUtile.php'; require_once dirname(__FILE__) . '/lib/Common.php';/** * ------- 登入邏輯模擬 -------- * 請根據實際編寫登入邏輯並處理安全驗證 */$qid = $_GET['qid']; $uid = $_GET['uid']; $data = array();switch ($uid) { case '1': $data['uid'] = 1; $data['name'] = '張三'; break; case '2': $data['uid'] = 2; $data['name'] = '李四'; break; } $data = json_encode($data); $redis = libRedisUtile::getInstance(); $redis->setex(libCommon::getQidLoginKey($qid), 1800, $data);
更多相關知識,請存取PHP中文網!
以上就是分享PHP掃碼登入原理及實現方法的詳細內容,更多請關注TW511.COM其它相關文章!