当前位置:首页 > PHP教程 > php应用 > 列表

分享PHP扫码登录原理及实现方法

发布:smiling 来源: PHP粉丝网  添加日期:2022-07-05 07:40:42 浏览: 评论:0 

由于扫码登录比账号密码登录更方便、快捷、灵活,在实际使用中更受到用户的欢迎。

本文主要介绍了扫码登录的原理及整体流程,包含了二维码的生成/获取、过期失效的处理、登录状态的监听。

扫码登录的原理

整体流程

为方便理解,我简单画了一个 UML 时序图,用以描述扫码登录的大致流程!

总结下核心流程:

请求业务服务器获取用以登录的二维码和 UUID。

通过 websocket 连接 socket 服务器,并定时(时间间隔依据服务器配置时间调整)发送心跳保持连接。

用户通过 APP 扫描二维码,发送请求到业务服务器处理登录。根据 UUID 设置登录结果。

socket 服务器通过监听获取登录结果,建立 session 数据,根据 UUID 推送登录数据到用户浏览器。

用户登录成功,服务器主动将该 socker 连接从连接池中剔除,该二维码失效。

关于客户端标识

也就是 UUID,这是贯穿整个流程的纽带,一个闭环登录过程,每一步业务处理都是围绕该次的 UUD 进行处理的。UUID 的生成有根据 session_id 的也有根据客户端 ip 地址的。个人还是建议每个二维码都有单独的 UUID,适用场景更广一些!

关于前端和服务器通讯

前端肯定是要和服务器保持一直通讯的,用以获取登录结果和二维码状态。看了下网上的一些实现方案,基本各个方案都有用的:轮询、长轮询、长链接、websocket。也不能肯定的说哪个方案好哪个方案不好,只能说哪个方案更适用于当前应用场景。个人比较建议使用长轮询、websocket 这种比较节省服务器性能的方案。

关于安全性

扫码登录的好处显而易见,一是人性化,再就是防止密码泄漏。但是新方式的接入,往往也伴随着新的风险。所以,很有必要再整体过程中加入适当的安全机制。例如:

强制 HTTPS 协议

短期令牌

数据签名

数据加密

扫码登录的过程演示

代码实现和源码后面会给出。

开启 Socket 服务器

访问登录页面

可以看到用户请求的二维码资源,并获取到了 qid 。

获取二维码时候会建立相应缓存,并设置过期时间:

之后会连接 socket 服务器,定时发送心跳。

此时 socket 服务器会有相应连接日志输出:

用户使用 APP 扫码并授权

服务器验证并处理登录,创建 session,建立对应的缓存:

Socket 服务器读取到缓存,开始推送信息,并关闭剔除连接:

前端获取信息,处理登录:

扫码登录的实现

注意:本 Demo 只是个人学习测试,所以并未做太多安全机制!

Socket 代理服务器

使用 Nginx 作为代理 socke 服务器。可使用域名,方便做负载均衡。本次测试域名:loc.websocket.net

websocker.conf

  1. server { 
  2.  
  3.     listen       80
  4.  
  5.     server_name  loc.websocket.net; 
  6.  
  7.     root   /www/websocket; 
  8.  
  9.     index  index.php index.html index.htm; 
  10.  
  11.     #charset koi8-r; 
  12.  
  13.  
  14.  
  15.     access_log /dev/null; 
  16.  
  17.     #access_log  /var/log/nginx/nginx.localhost.access.log  main; 
  18.  
  19.     error_log  /var/log/nginx/nginx.websocket.error.log  warn; 
  20.  
  21.  
  22.  
  23.     #error_page  404              /404.html; 
  24.  
  25.  
  26.  
  27.     # redirect server error pages to the static page /50x.html 
  28.  
  29.     # 
  30.  
  31.     error_page   500 502 503 504  /50x.html; 
  32.  
  33.     location = /50x.html { 
  34.  
  35.         root   /usr/share/nginx/html; 
  36.  
  37.     } 
  38.  
  39.  
  40.  
  41.     location / { 
  42.  
  43.         proxy_pass http://php-cli:8095/; 
  44.  
  45.         proxy_http_version 1.1
  46.  
  47.         proxy_connect_timeout 4s
  48.  
  49.         proxy_read_timeout 60s
  50.  
  51.         proxy_send_timeout 12s
  52.  
  53.         proxy_set_header Upgrade $http_upgrade; 
  54.  
  55.         proxy_set_header Connection $connection_upgrade; 
  56.  
  57.     } 
  58.  

Socket 服务器

使用 PHP 构建的 socket 服务器。实际项目中大家可以考虑使用第三方应用,稳定性更好一些!

QRServer.php

  1. <?php 
  2.  
  3. require_once dirname(dirname(__FILE__)) . '/Config.php'
  4.  
  5. require_once dirname(dirname(__FILE__)) . '/lib/RedisUtile.php'
  6.  
  7. require_once dirname(dirname(__FILE__)) . '/lib/Common.php';/** 
  8.  
  9.  * 扫码登陆服务端 
  10.  
  11.  * Class QRServer 
  12.  
  13.  * @author BNDong */class QRServer {    private $_sock;    private $_redis;    private $_clients = array();    /** 
  14.  
  15.      * socketServer constructor.     */ 
  16.  
  17.     public function __construct() 
  18.  
  19.     {        // 设置 timeout 
  20.  
  21.         set_time_limit(0);        // 创建一个套接字(通讯节点) 
  22.  
  23.         $this->_sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Could not create socket" . PHP_EOL); 
  24.  
  25.         socket_set_option($this->_sock, SOL_SOCKET, SO_REUSEADDR, 1);        // 绑定地址 
  26.  
  27.         socket_bind($this->_sock, \Config::QRSERVER_HOST, \Config::QRSERVER_PROT) or die("Could not bind to socket" . PHP_EOL);        // 监听套接字上的连接 
  28.  
  29.         socket_listen($this->_sock, 4) or die("Could not set up socket listener" . PHP_EOL); 
  30.  
  31.  
  32.  
  33.         $this->_redis  = \lib\RedisUtile::getInstance(); 
  34.  
  35.     }    /** 
  36.  
  37.      * 启动服务     */ 
  38.  
  39.     public function run() 
  40.  
  41.     { 
  42.  
  43.         $this->_clients = array(); 
  44.  
  45.         $this->_clients[uniqid()] = $this->_sock;        while (true){ 
  46.  
  47.             $changes = $this->_clients; 
  48.  
  49.             $write   = NULL; 
  50.  
  51.             $except  = NULL; 
  52.  
  53.             socket_select($changes,  $write,  $except, NULL);            foreach ($changes as $key => $_sock) {                if($this->_sock == $_sock){ // 判断是不是新接入的 socket 
  54.  
  55.  
  56.  
  57.                     if(($newClient = socket_accept($_sock))  === false){ 
  58.  
  59.                         die('failed to accept socket: '.socket_strerror($_sock)."\n"); 
  60.  
  61.                     } 
  62.  
  63.  
  64.  
  65.                     $buffer   = trim(socket_read($newClient, 1024)); // 读取请求 
  66.  
  67.                     $response = $this->handShake($buffer); 
  68.  
  69.                     socket_write($newClient$responsestrlen($response)); // 发送响应 
  70.  
  71.                     socket_getpeername($newClient$ip); // 获取 ip 地址 
  72.  
  73.                     $qid = $this->getHandQid($buffer); 
  74.  
  75.                     $this->log("new clinet: "$qid);                    if ($qid) { // 验证是否存在 qid 
  76.  
  77.                         if (isset($this->_clients[$qid])) $this->close($qid$this->_clients[$qid]); 
  78.  
  79.                         $this->_clients[$qid] = $newClient
  80.  
  81.                     } else { 
  82.  
  83.                         $this->close($qid$newClient); 
  84.  
  85.                     } 
  86.  
  87.  
  88.  
  89.                 } else {                    // 判断二维码是否过期 
  90.  
  91.                     if ($this->_redis->exists(\lib\Common::getQidKey($key))) { 
  92.  
  93.  
  94.  
  95.                         $loginKey = \lib\Common::getQidLoginKey($key);                        if ($this->_redis->exists($loginKey)) { // 判断用户是否扫码 
  96.  
  97.                             $this->send($key$this->_redis->get($loginKey)); 
  98.  
  99.                             $this->close($key$_sock); 
  100.  
  101.                         } 
  102.  
  103.  
  104.  
  105.                         $res = socket_recv($_sock$buffer,  2048, 0);                        if (false === $res) { 
  106.  
  107.                             $this->close($key$_sock); 
  108.  
  109.                         } else { 
  110.  
  111.                             $res && $this->log("{$key} clinet msg: " . $this->message($buffer)); 
  112.  
  113.                         } 
  114.  
  115.                     } else { 
  116.  
  117.                         $this->close($key$this->_clients[$key]); 
  118.  
  119.                     } 
  120.  
  121.  
  122.  
  123.                 } 
  124.  
  125.             } 
  126.  
  127.             sleep(1); 
  128.  
  129.         } 
  130.  
  131.     }    /** 
  132.  
  133.      * 构建响应 
  134.  
  135.      * @param string $buf 
  136.  
  137.      * @return string     */ 
  138.  
  139.     private function handShake($buf){ 
  140.  
  141.         $buf    = substr($buf,strpos($buf,'Sec-WebSocket-Key:') + 18); 
  142.  
  143.         $key    = trim(substr($buf, 0, strpos($buf,"\r\n"))); 
  144.  
  145.         $newKey = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true)); 
  146.  
  147.         $newMessage = "HTTP/1.1 101 Switching Protocols\r\n"
  148.  
  149.         $newMessage .= "Upgrade: websocket\r\n"
  150.  
  151.         $newMessage .= "Sec-WebSocket-Version: 13\r\n"
  152.  
  153.         $newMessage .= "Connection: Upgrade\r\n"
  154.  
  155.         $newMessage .= "Sec-WebSocket-Accept: " . $newKey . "\r\n\r\n";        return $newMessage
  156.  
  157.     }    /** 
  158.  
  159.      * 获取 qid 
  160.  
  161.      * @param string $buf 
  162.  
  163.      * @return mixed|string     */ 
  164.  
  165.     private function getHandQid($buf) { 
  166.  
  167.         preg_match("/^[\s\n]?GET\s+\/\?qid\=([a-z0-9]+)\s+HTTP.*/"$buf$matches); 
  168.  
  169.         $qid = isset($matches[1]) ? $matches[1] : '';        return $qid
  170.  
  171.     }    /** 
  172.  
  173.      * 编译发送数据 
  174.  
  175.      * @param string $s 
  176.  
  177.      * @return string     */ 
  178.  
  179.     private function frame($s) { 
  180.  
  181.         $a = str_split($s, 125);        if (count($a) == 1) {            return "\x81" . chr(strlen($a[0])) . $a[0]; 
  182.  
  183.         } 
  184.  
  185.         $ns = "";        foreach ($a as $o) { 
  186.  
  187.             $ns .= "\x81" . chr(strlen($o)) . $o
  188.  
  189.         }        return $ns
  190.  
  191.     }    /** 
  192.  
  193.      * 解析接收数据 
  194.  
  195.      * @param resource $buffer 
  196.  
  197.      * @return null|string     */ 
  198.  
  199.     private function message($buffer){ 
  200.  
  201.         $masks = $data = $decoded = null; 
  202.  
  203.         $len = ord($buffer[1]) & 127;        if ($len === 126)  { 
  204.  
  205.             $masks = substr($buffer, 4, 4); 
  206.  
  207.             $data = substr($buffer, 8); 
  208.  
  209.         } else if ($len === 127)  { 
  210.  
  211.             $masks = substr($buffer, 10, 4); 
  212.  
  213.             $data = substr($buffer, 14); 
  214.  
  215.         } else  { 
  216.  
  217.             $masks = substr($buffer, 2, 4); 
  218.  
  219.             $data = substr($buffer, 6); 
  220.  
  221.         }        for ($index = 0; $index < strlen($data); $index++) { 
  222.  
  223.             $decoded .= $data[$index] ^ $masks[$index % 4]; 
  224.  
  225.         }        return $decoded
  226.  
  227.     }    /** 
  228.  
  229.      * 发送消息 
  230.  
  231.      * @param string $qid 
  232.  
  233.      * @param string $msg     */ 
  234.  
  235.     private function send($qid$msg
  236.  
  237.     { 
  238.  
  239.         $frameMsg = $this->frame($msg); 
  240.  
  241.         socket_write($this->_clients[$qid], $frameMsgstrlen($frameMsg)); 
  242.  
  243.         $this->log("{$qid} clinet send: " . $msg); 
  244.  
  245.     }    /** 
  246.  
  247.      * 关闭 socket 
  248.  
  249.      * @param string $qid 
  250.  
  251.      * @param resource $socket     */ 
  252.  
  253.     private function close($qid$socket
  254.  
  255.     { 
  256.  
  257.         socket_close($socket);        if (array_key_exists($qid$this->_clients)) unset($this->_clients[$qid]); 
  258.  
  259.         $this->_redis->del(\lib\Common::getQidKey($qid)); 
  260.  
  261.         $this->_redis->del(\lib\Common::getQidLoginKey($qid)); 
  262.  
  263.         $this->log("{$qid} clinet close"); 
  264.  
  265.     }    /** 
  266.  
  267.      * 日志记录 
  268.  
  269.      * @param string $msg     */ 
  270.  
  271.     private function log($msg
  272.  
  273.     { 
  274.  
  275.         echo '['date('Y-m-d H:i:s') .'] ' . $msg . "\n"
  276.  
  277.     } 
  278.  
  279.  
  280. $server = new QRServer(); 
  281.  
  282. $server->run(); 

登录页面

  1. <!DOCTYPE html> 
  2.  
  3. <html lang="en"
  4.  
  5. <head> 
  6.  
  7.     <meta charset="UTF-8"
  8.  
  9.     <title>扫码登录 - 测试页面</title> 
  10.  
  11.     <meta name="viewport" content="width=device-width, initial-scale=1"
  12.  
  13.     <link rel="stylesheet" type="text/css" href="./public/css/main.css"
  14.  
  15. </head> 
  16.  
  17. <body translate="no"
  18.  
  19.  
  20.  
  21. <p class='box'
  22.  
  23.     <p class='box-form'
  24.  
  25.         <p class='box-login-tab'></p> 
  26.  
  27.         <p class='box-login-title'
  28.  
  29.             <p class='i i-login'></p><h2>登录</h2> 
  30.  
  31.         </p> 
  32.  
  33.         <p class='box-login'
  34.  
  35.             <p class='fieldset-body' id='login_form'
  36.  
  37.                 <button onclick="openLoginInfo();" class='b b-form i i-more' title='Mais Informações'></button> 
  38.  
  39.                 <p class='field'
  40.  
  41.                     <label for='user'>用户账户</label> 
  42.  
  43.                     <input type='text' id='user' name='user' title='Username' placeholder="请输入用户账户/邮箱地址" /> 
  44.  
  45.                 </p> 
  46.  
  47.                 <p class='field'
  48.  
  49.                     <label for='pass'>用户密码</label> 
  50.  
  51.                     <input type='password' id='pass' name='pass' title='Password' placeholder="情输入账户密码" /> 
  52.  
  53.                 </p> 
  54.  
  55.                 <label class='checkbox'
  56.  
  57.                     <input type='checkbox' value='TRUE' title='Keep me Signed in' /> 记住我                </label> 
  58.  
  59.                 <input type='submit' id='do_login' value='登录' title='登录' /> 
  60.  
  61.             </p> 
  62.  
  63.         </p> 
  64.  
  65.     </p> 
  66.  
  67.     <p class='box-info'
  68.  
  69.         <p><button onclick="closeLoginInfo();" class='b b-info i i-left' title='Back to Sign In'></button><h3>扫码登录</h3> 
  70.  
  71.         </p> 
  72.  
  73.         <p class='line-wh'></p> 
  74.  
  75.         <p style="position: relative;"
  76.  
  77.             <input type="hidden" id="qid" value=""
  78.  
  79.             <p id="qrcode-exp">二维码已失效<br>点击重新获取</p> 
  80.  
  81.             <img id="qrcode" src="" /> 
  82.  
  83.         </p> 
  84.  
  85.     </p> 
  86.  
  87. </p> 
  88.  
  89. <script src='./public/js/jquery.min.js'></script> 
  90.  
  91. <script src='./public/js/modernizr.min.js'></script> 
  92.  
  93. <script id="rendered-js"
  94.  
  95.     $(document).ready(function () { 
  96.  
  97.  
  98.  
  99.         restQRCode(); 
  100.  
  101.         openLoginInfo(); 
  102.  
  103.         $('#qrcode-exp').click(function () { 
  104.  
  105.             restQRCode(); 
  106.  
  107.             $(this).hide(); 
  108.  
  109.         }); 
  110.  
  111.     });    /** 
  112.  
  113.      * 打开二维码     */ 
  114.  
  115.     function openLoginInfo() { 
  116.  
  117.         $(document).ready(function () { 
  118.  
  119.             $('.b-form').css("opacity""0.01"); 
  120.  
  121.             $('.box-form').css("left""-100px"); 
  122.  
  123.             $('.box-info').css("right""-100px"); 
  124.  
  125.         }); 
  126.  
  127.     }    /** 
  128.  
  129.      * 关闭二维码     */ 
  130.  
  131.     function closeLoginInfo() { 
  132.  
  133.         $(document).ready(function () { 
  134.  
  135.             $('.b-form').css("opacity""1"); 
  136.  
  137.             $('.box-form').css("left""0px"); 
  138.  
  139.             $('.box-info').css("right""-5px"); 
  140.  
  141.         }); 
  142.  
  143.     }    /** 
  144.  
  145.      * 刷新二维码     */ 
  146.  
  147.     var ws, wsTid = null; 
  148.  
  149.     function restQRCode() { 
  150.  
  151.  
  152.  
  153.         $.ajax({ 
  154.  
  155.             url: 'http://localhost/qrcode/code.php'
  156.  
  157.             type:'post'
  158.  
  159.             dataType: "json",            async: false, 
  160.  
  161.             success:function (result) { 
  162.  
  163.                 $('#qrcode').attr('src', result.img); 
  164.  
  165.                 $('#qid').val(result.qid); 
  166.  
  167.             } 
  168.  
  169.         });        if ("WebSocket" in window) {            if (typeof ws != 'undefined'){ 
  170.  
  171.                 ws.close();                null != wsTid && window.clearInterval(wsTid); 
  172.  
  173.             } 
  174.  
  175.  
  176.  
  177.             ws = new WebSocket("ws://loc.websocket.net?qid=" + $('#qid').val()); 
  178.  
  179.  
  180.  
  181.             ws.onopen = function() { 
  182.  
  183.                 console.log('websocket 已连接上!'); 
  184.  
  185.             }; 
  186.  
  187.  
  188.  
  189.             ws.onmessage = function(e) {                // todo: 本函数做登录处理,登录判断,创建缓存信息!                console.log(e.data);                var result = JSON.parse(e.data); 
  190.  
  191.                 console.log(result); 
  192.  
  193.                 alert('登录成功:' + result.name); 
  194.  
  195.             }; 
  196.  
  197.  
  198.  
  199.             ws.onclose = function() { 
  200.  
  201.                 console.log('websocket 连接已关闭!'); 
  202.  
  203.                 $('#qrcode-exp').show();                null != wsTid && window.clearInterval(wsTid); 
  204.  
  205.             };            // 发送心跳 
  206.  
  207.             wsTid = window.setInterval( function () {                if (typeof ws != 'undefined') ws.send('1'); 
  208.  
  209.             }, 50000 ); 
  210.  
  211.  
  212.  
  213.         } else {            // todo: 不支持 WebSocket 的,可以使用 js 轮询处理,这里不作该功能实现! 
  214.  
  215.             alert('您的浏览器不支持 WebSocket!'); 
  216.  
  217.         } 
  218.  
  219.     }</script> 
  220.  
  221. </body> 
  222.  
  223. </html> 

登录处理

测试使用,模拟登录处理,未做安全认证!!

  1. <?php 
  2.  
  3. require_once dirname(__FILE__) . '/lib/RedisUtile.php'
  4.  
  5. require_once dirname(__FILE__) . '/lib/Common.php';/** 
  6.  
  7.  * -------  登录逻辑模拟 -------- 
  8.  
  9.  * 请根据实际编写登录逻辑并处理安全验证 */$qid = $_GET['qid']; 
  10.  
  11. $uid = $_GET['uid']; 
  12.  
  13.  
  14.  
  15. $data = array();switch ($uid
  16.  
  17. {    case '1'
  18.  
  19.         $data['uid']  = 1; 
  20.  
  21.         $data['name'] = '张三';        break;    case '2'
  22.  
  23.         $data['uid']  = 2; 
  24.  
  25.         $data['name'] = '李四';        break
  26.  
  27.  
  28. $data  = json_encode($data); 
  29.  
  30. $redis = \lib\RedisUtile::getInstance(); 
  31.  
  32. $redis->setex(\lib\Common::getQidLoginKey($qid), 1800, $data);

Tags: PHP扫码登录

分享到: