溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

PHP掃碼登錄原理及實(shí)現(xiàn)方法有哪些

發(fā)布時(shí)間:2020-07-22 13:52:34 來源:億速云 閱讀:427 作者:Leah 欄目:編程語(yǔ)言

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)PHP掃碼登錄原理及實(shí)現(xiàn)方法有哪些,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

掃碼登錄的原理

整體流程

為方便理解,我簡(jiǎn)單畫了一個(gè) UML 時(shí)序圖,用以描述掃碼登錄的大致流程!

總結(jié)下核心流程:

  1. 請(qǐng)求業(yè)務(wù)服務(wù)器獲取用以登錄的二維碼和 UUID。

  2. 通過 websocket 連接 socket 服務(wù)器,并定時(shí)(時(shí)間間隔依據(jù)服務(wù)器配置時(shí)間調(diào)整)發(fā)送心跳保持連接。

  3. 用戶通過 APP 掃描二維碼,發(fā)送請(qǐng)求到業(yè)務(wù)服務(wù)器處理登錄。根據(jù) UUID 設(shè)置登錄結(jié)果。

  4. socket 服務(wù)器通過監(jiān)聽獲取登錄結(jié)果,建立 session 數(shù)據(jù),根據(jù) UUID 推送登錄數(shù)據(jù)到用戶瀏覽器。

  5. 用戶登錄成功,服務(wù)器主動(dòng)將該 socker 連接從連接池中剔除,該二維碼失效。

關(guān)于客戶端標(biāo)識(shí)

也就是 UUID,這是貫穿整個(gè)流程的紐帶,一個(gè)閉環(huán)登錄過程,每一步業(yè)務(wù)處理都是圍繞該次的 UUD 進(jìn)行處理的。UUID 的生成有根據(jù) session_id 的也有根據(jù)客戶端 ip 地址的。個(gè)人還是建議每個(gè)二維碼都有單獨(dú)的 UUID,適用場(chǎng)景更廣一些!

關(guān)于前端和服務(wù)器通訊

前端肯定是要和服務(wù)器保持一直通訊的,用以獲取登錄結(jié)果和二維碼狀態(tài)??戳讼戮W(wǎng)上的一些實(shí)現(xiàn)方案,基本各個(gè)方案都有用的:輪詢、長(zhǎng)輪詢、長(zhǎng)鏈接、websocket。也不能肯定的說哪個(gè)方案好哪個(gè)方案不好,只能說哪個(gè)方案更適用于當(dāng)前應(yīng)用場(chǎng)景。個(gè)人比較建議使用長(zhǎng)輪詢、websocket 這種比較節(jié)省服務(wù)器性能的方案。

關(guān)于安全性

掃碼登錄的好處顯而易見,一是人性化,再就是防止密碼泄漏。但是新方式的接入,往往也伴隨著新的風(fēng)險(xiǎn)。所以,很有必要再整體過程中加入適當(dāng)?shù)陌踩珯C(jī)制。例如:

  • 強(qiáng)制 HTTPS 協(xié)議
  • 短期令牌
  • 數(shù)據(jù)簽名
  • 數(shù)據(jù)加密

掃碼登錄的過程演示

代碼實(shí)現(xiàn)和源碼后面會(huì)給出。

開啟 Socket 服務(wù)器

訪問登錄頁(yè)面

可以看到用戶請(qǐng)求的二維碼資源,并獲取到了 qid 。

獲取二維碼時(shí)候會(huì)建立相應(yīng)緩存,并設(shè)置過期時(shí)間:

之后會(huì)連接 socket 服務(wù)器,定時(shí)發(fā)送心跳。

此時(shí) socket 服務(wù)器會(huì)有相應(yīng)連接日志輸出:

用戶使用 APP 掃碼并授權(quán)

服務(wù)器驗(yàn)證并處理登錄,創(chuàng)建 session,建立對(duì)應(yīng)的緩存:

Socket 服務(wù)器讀取到緩存,開始推送信息,并關(guān)閉剔除連接:

前端獲取信息,處理登錄:

掃碼登錄的實(shí)現(xiàn)

注意:本 Demo 只是個(gè)人學(xué)習(xí)測(cè)試,所以并未做太多安全機(jī)制!

Socket 代理服務(wù)器

使用 Nginx 作為代理 socke 服務(wù)器。可使用域名,方便做負(fù)載均衡。本次測(cè)試域名: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;
    }
}

Socket 服務(wù)器

使用 PHP 構(gòu)建的 socket 服務(wù)器。實(shí)際項(xiàng)目中大家可以考慮使用第三方應(yīng)用,穩(wěn)定性更好一些!

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';/**
 * 掃碼登陸服務(wù)端
 * Class QRServer
 * @author BNDong */class QRServer {    private $_sock;    private $_redis;    private $_clients = array();    /**
     * socketServer constructor.     */
    public function __construct()
    {        // 設(shè)置 timeout
        set_time_limit(0);        // 創(chuàng)建一個(gè)套接字(通訊節(jié)點(diǎn))
        $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);        // 監(jiān)聽套接字上的連接
        socket_listen($this->_sock, 4) or die("Could not set up socket listener" . PHP_EOL);

        $this->_redis  = \lib\RedisUtile::getInstance();
    }    /**
     * 啟動(dòng)服務(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)); // 讀取請(qǐng)求
                    $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) { // 驗(yàn)證是否存在 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();

登錄頁(yè)面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>掃碼登錄 - 測(cè)試頁(yè)面</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><h3>登錄</h3>
        </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="請(qǐng)輸入用戶賬戶/郵箱地址" />
                </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><h4>掃碼登錄</h4>
        </p>
        <p class='line-wh'></p>
        <p style="position: relative;">
            <input type="hidden" id="qid" value="">
            <p id="qrcode-exp">二維碼已失效<br>點(diǎn)擊重新獲取</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");
        });
    }    /**
     * 關(guān)閉二維碼     */
    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: 本函數(shù)做登錄處理,登錄判斷,創(chuàng)建緩存信息!                console.log(e.data);                var result = JSON.parse(e.data);
                console.log(result);
                alert('登錄成功:' + result.name);
            };

            ws.onclose = function() {
                console.log('websocket 連接已關(guān)閉!');
                $('#qrcode-exp').show();                null != wsTid && window.clearInterval(wsTid);
            };            // 發(fā)送心跳
            wsTid = window.setInterval( function () {                if (typeof ws != 'undefined') ws.send('1');
            }, 50000 );

        } else {            // todo: 不支持 WebSocket 的,可以使用 js 輪詢處理,這里不作該功能實(shí)現(xiàn)!
            alert('您的瀏覽器不支持 WebSocket!');
        }
    }</script>
</body>
</html>

登錄處理

測(cè)試使用,模擬登錄處理,未做安全認(rèn)證??!

<?php

require_once dirname(__FILE__) . '/lib/RedisUtile.php';
require_once dirname(__FILE__) . '/lib/Common.php';/**
 * -------  登錄邏輯模擬 --------
 * 請(qǐng)根據(jù)實(shí)際編寫登錄邏輯并處理安全驗(yàn)證 */$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 = \lib\RedisUtile::getInstance();
$redis->setex(\lib\Common::getQidLoginKey($qid), 1800, $data);

上述就是小編為大家分享的PHP掃碼登錄原理及實(shí)現(xiàn)方法有哪些了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

php
AI