分享PHP扫码登录原理及实现方法
由于扫码登录比账号密码登录更方便、快捷、灵活,在实际使用中更受到用户的欢迎。
本文主要介绍了扫码登录的原理及整体流程,包含了二维码的生成/获取、过期失效的处理、登录状态的监听。
扫码登录的原理
整体流程
为方便理解,我简单画了一个 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
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 服务器
使用 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 = \lib\RedisUtile::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(\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); } } /** * 构建响应 * @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; } /** * 编译发送数据 * @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(\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();
登录页面
<!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 = \lib\RedisUtile::getInstance(); $redis->setex(\lib\Common::getQidLoginKey($qid), 1800, $data);
更多相关知识,请访问PHP中文网!
Atas ialah kandungan terperinci 分享PHP扫码登录原理及实现方法. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

Video Face Swap
Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas





PHP 8.4 membawa beberapa ciri baharu, peningkatan keselamatan dan peningkatan prestasi dengan jumlah penamatan dan penyingkiran ciri yang sihat. Panduan ini menerangkan cara memasang PHP 8.4 atau naik taraf kepada PHP 8.4 pada Ubuntu, Debian, atau terbitan mereka

Jika anda seorang pembangun PHP yang berpengalaman, anda mungkin merasakan bahawa anda telah berada di sana dan telah melakukannya. Anda telah membangunkan sejumlah besar aplikasi, menyahpenyahpepijat berjuta-juta baris kod dan mengubah suai sekumpulan skrip untuk mencapai op

Kod Visual Studio, juga dikenali sebagai Kod VS, ialah editor kod sumber percuma — atau persekitaran pembangunan bersepadu (IDE) — tersedia untuk semua sistem pengendalian utama. Dengan koleksi sambungan yang besar untuk banyak bahasa pengaturcaraan, Kod VS boleh menjadi c

JWT adalah standard terbuka berdasarkan JSON, yang digunakan untuk menghantar maklumat secara selamat antara pihak, terutamanya untuk pengesahan identiti dan pertukaran maklumat. 1. JWT terdiri daripada tiga bahagian: header, muatan dan tandatangan. 2. Prinsip kerja JWT termasuk tiga langkah: menjana JWT, mengesahkan JWT dan muatan parsing. 3. Apabila menggunakan JWT untuk pengesahan di PHP, JWT boleh dijana dan disahkan, dan peranan pengguna dan maklumat kebenaran boleh dimasukkan dalam penggunaan lanjutan. 4. Kesilapan umum termasuk kegagalan pengesahan tandatangan, tamat tempoh, dan muatan besar. Kemahiran penyahpepijatan termasuk menggunakan alat debugging dan pembalakan. 5. Pengoptimuman prestasi dan amalan terbaik termasuk menggunakan algoritma tandatangan yang sesuai, menetapkan tempoh kesahihan dengan munasabah,

Tutorial ini menunjukkan cara memproses dokumen XML dengan cekap menggunakan PHP. XML (bahasa markup extensible) adalah bahasa markup berasaskan teks yang serba boleh yang direka untuk pembacaan manusia dan parsing mesin. Ia biasanya digunakan untuk penyimpanan data

Rentetan adalah urutan aksara, termasuk huruf, nombor, dan simbol. Tutorial ini akan mempelajari cara mengira bilangan vokal dalam rentetan yang diberikan dalam PHP menggunakan kaedah yang berbeza. Vokal dalam bahasa Inggeris adalah a, e, i, o, u, dan mereka boleh menjadi huruf besar atau huruf kecil. Apa itu vokal? Vokal adalah watak abjad yang mewakili sebutan tertentu. Terdapat lima vokal dalam bahasa Inggeris, termasuk huruf besar dan huruf kecil: a, e, i, o, u Contoh 1 Input: String = "TutorialSpoint" Output: 6 menjelaskan Vokal dalam rentetan "TutorialSpoint" adalah u, o, i, a, o, i. Terdapat 6 yuan sebanyak 6

Mengikat statik (statik: :) Melaksanakan pengikatan statik lewat (LSB) dalam PHP, yang membolehkan kelas panggilan dirujuk dalam konteks statik dan bukannya menentukan kelas. 1) Proses parsing dilakukan pada masa runtime, 2) Cari kelas panggilan dalam hubungan warisan, 3) ia boleh membawa overhead prestasi.

Apakah kaedah sihir PHP? Kaedah sihir PHP termasuk: 1. \ _ \ _ Membina, digunakan untuk memulakan objek; 2. \ _ \ _ Destruct, digunakan untuk membersihkan sumber; 3. \ _ \ _ Call, mengendalikan panggilan kaedah yang tidak wujud; 4. \ _ \ _ Mendapatkan, melaksanakan akses atribut dinamik; 5. \ _ \ _ Set, melaksanakan tetapan atribut dinamik. Kaedah ini secara automatik dipanggil dalam situasi tertentu, meningkatkan fleksibiliti dan kecekapan kod.
