這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)PHP掃碼登錄原理及實現(xiàn)方法有哪些,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
成都創(chuàng)新互聯(lián)公司是一家專注網(wǎng)站建設(shè)、網(wǎng)絡(luò)營銷策劃、微信小程序開發(fā)、電子商務(wù)建設(shè)、網(wǎng)絡(luò)推廣、移動互聯(lián)開發(fā)、研究、服務(wù)為一體的技術(shù)型公司。公司成立十余年以來,已經(jīng)為近千家宣傳片制作各業(yè)的企業(yè)公司提供互聯(lián)網(wǎng)服務(wù)?,F(xiàn)在,服務(wù)的近千家客戶與我們一路同行,見證我們的成長;未來,我們一起分享成功的喜悅。
為方便理解,我簡單畫了一個 UML 時序圖,用以描述掃碼登錄的大致流程!
總結(jié)下核心流程:
請求業(yè)務(wù)服務(wù)器獲取用以登錄的二維碼和 UUID。
通過 websocket 連接 socket 服務(wù)器,并定時(時間間隔依據(jù)服務(wù)器配置時間調(diào)整)發(fā)送心跳保持連接。
用戶通過 APP 掃描二維碼,發(fā)送請求到業(yè)務(wù)服務(wù)器處理登錄。根據(jù) UUID 設(shè)置登錄結(jié)果。
socket 服務(wù)器通過監(jiān)聽獲取登錄結(jié)果,建立 session 數(shù)據(jù),根據(jù) UUID 推送登錄數(shù)據(jù)到用戶瀏覽器。
用戶登錄成功,服務(wù)器主動將該 socker 連接從連接池中剔除,該二維碼失效。
也就是 UUID,這是貫穿整個流程的紐帶,一個閉環(huán)登錄過程,每一步業(yè)務(wù)處理都是圍繞該次的 UUD 進(jìn)行處理的。UUID 的生成有根據(jù) session_id 的也有根據(jù)客戶端 ip 地址的。個人還是建議每個二維碼都有單獨(dú)的 UUID,適用場景更廣一些!
前端肯定是要和服務(wù)器保持一直通訊的,用以獲取登錄結(jié)果和二維碼狀態(tài)??戳讼戮W(wǎng)上的一些實現(xiàn)方案,基本各個方案都有用的:輪詢、長輪詢、長鏈接、websocket。也不能肯定的說哪個方案好哪個方案不好,只能說哪個方案更適用于當(dāng)前應(yīng)用場景。個人比較建議使用長輪詢、websocket 這種比較節(jié)省服務(wù)器性能的方案。
掃碼登錄的好處顯而易見,一是人性化,再就是防止密碼泄漏。但是新方式的接入,往往也伴隨著新的風(fēng)險。所以,很有必要再整體過程中加入適當(dāng)?shù)陌踩珯C(jī)制。例如:
代碼實現(xiàn)和源碼后面會給出。
可以看到用戶請求的二維碼資源,并獲取到了 qid
。
獲取二維碼時候會建立相應(yīng)緩存,并設(shè)置過期時間:
之后會連接 socket 服務(wù)器,定時發(fā)送心跳。
此時 socket 服務(wù)器會有相應(yīng)連接日志輸出:
服務(wù)器驗證并處理登錄,創(chuàng)建 session,建立對應(yīng)的緩存:
Socket 服務(wù)器讀取到緩存,開始推送信息,并關(guān)閉剔除連接:
前端獲取信息,處理登錄:
注意:本 Demo 只是個人學(xué)習(xí)測試,所以并未做太多安全機(jī)制!
使用 Nginx 作為代理 socke 服務(wù)器??墒褂糜蛎?,方便做負(fù)載均衡。本次測試域名: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 構(gòu)建的 socket 服務(wù)器。實際項目中大家可以考慮使用第三方應(yīng)用,穩(wěn)定性更好一些!
QRServer.php
_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); // 監(jiān)聽套接字上的連接 socket_listen($this->_sock, 4) or die("Could not set up socket listener" . PHP_EOL); $this->_redis = \lib\RedisUtile::getInstance(); } /** * 啟動服務(wù) */ 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)); // 發(fā)送響應(yīng) 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(\lib\Common::getQidKey($key))) { $loginKey = \lib\Common::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); } } /** * 構(gòu)建響應(yīng) * @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,"\r\n"))); $newKey = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true)); $newMessage = "HTTP/1.1 101 Switching Protocols\r\n"; $newMessage .= "Upgrade: websocket\r\n"; $newMessage .= "Sec-WebSocket-Version: 13\r\n"; $newMessage .= "Connection: Upgrade\r\n"; $newMessage .= "Sec-WebSocket-Accept: " . $newKey . "\r\n\r\n"; return $newMessage; } /** * 獲取 qid * @param string $buf * @return mixed|string */ private function getHandQid($buf) { preg_match("/^[\s\n]?GET\s+\/\?qid\=([a-z0-9]+)\s+HTTP.*/", $buf, $matches); $qid = isset($matches[1]) ? $matches[1] : ''; return $qid; } /** * 編譯發(fā)送數(shù)據(jù) * @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; } /** * 解析接收數(shù)據(jù) * @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; } /** * 發(fā)送消息 * @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); } /** * 關(guān)閉 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(\lib\Common::getQidKey($qid)); $this->_redis->del(\lib\Common::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();
掃碼登錄 - 測試頁面
登錄
掃碼登錄
二維碼已失效
點擊重新獲取
測試使用,模擬登錄處理,未做安全認(rèn)證!!
setex(\lib\Common::getQidLoginKey($qid), 1800, $data);
上述就是小編為大家分享的PHP掃碼登錄原理及實現(xiàn)方法有哪些了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。