As the scan code to log more convenient, fast, flexible, more users are welcome in actual use than the account password.
This paper describes the principles and sweep the whole process login code, including a two-dimensional code generation / acquisition, monitoring expired deal, login state.
Principle scan code to log
Overall process
To facilitate understanding, I simply drew a UML sequence diagram to describe the general flow scan code login!
Summary The following core processes:
-
Requests the service server to obtain two-dimensional code and UUID to login.
-
Socket server connected via websocket, and timing (time interval adjusted according to the configuration server) transmits the heartbeat remain connected.
-
APP user scanning two-dimensional code, sends a request to the service server logon. Set to log results based UUID.
-
socket server get by listening to log results, the establishment of session data and push data to log into the user's browser according to UUID.
-
User login is successful, the server actively socker removed from the connection pool, the two-dimensional code fails.
About Client Identity
That is UUID, which is run through the entire process of link, a closed-loop login process, every step of the business process are handled around the times of UUD. UUID is generated according to the session_id have also ip address based on the client. It is recommended that each individual has a separate two-dimensional code UUID, applicable scene is broader!
On front-end and server communication
Front-end server and is sure to keep the communication has to obtain two-dimensional code to log the results and status. Read some implementations under the Internet, various programs have basically used: polling, long polling, long link, websocket. We can not say for sure what a good program which program is not good, can only say which program is more suitable for the current application scenario. Personally recommend the use of long-polling, websocket this comparison save server performance solution.
About Security
The obvious benefit of scan code login, one human, then there are passwords to prevent leakage. But new ways of access, often accompanied by new risks. Therefore, appropriate security mechanism and then the whole process is necessary to add. E.g:
- Forced HTTPS protocol
- Short-lived token
- Data signature
- data encryption
Login procedure demonstrates scan code
And source code implementation will be given later.
Socket server open
Visit the login page
We can see the two-dimensional code resources requested by the user, and to obtain qid
.
When will obtain two-dimensional code to establish the corresponding cache, and set an expiration time:
After connect socket server, regularly send heartbeat?.
In this case there will be a corresponding connector socket server log output:
APP scan code and user authorization
Server authentication and logon, create session, create a corresponding cache:
Socket server reads to the cache, information began to push and close excluding connection:
Front-end access to information, logon:
Login code to achieve sweep
Note: This Demo only individual learning test, so I did not do too much security!
Socket Proxy Server
Socke using Nginx as a proxy server. Domain name can be used to facilitate load balancing. The test domain: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 Server
Socket server using PHP build. The actual project we can consider using third-party applications, better stability of some!
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();
log in page
<!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">
<div class='box'>
<div class='box-form'>
<div class='box-login-tab'></div>
<div class='box-login-title'>
<div class='i i-login'></div><h2>登录</h2>
</div>
<div class='box-login'>
<div 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='登录' />
</div>
</div>
</div>
<div class='box-info'>
<p><button onclick="closeLoginInfo();" class='b b-info i i-left' title='Back to Sign In'></button><h3>扫码登录</h3>
</p>
<div class='line-wh'></div>
<div style="position: relative;">
<input type="hidden" id="qid" value="">
<div id="qrcode-exp">二维码已失效<br>点击重新获取</div>
<img id="qrcode" src="" />
</div>
</div>
</div>
<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>
Login processing
Use test, simulate the login process, without making safety certification! !
<?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);
Kansei Demo Minamoto码: BNDong / Demo / ScanCodeLogin