首先先給大家道個歉,由于上次寫東西不認真,自己也沒有測試,不僅沒能幫到大家,還害的不少人走了彎路,是以原文章代碼我删掉了
掃碼登陸的原理 我上面的圖上已經說的很清楚 實際上就是手機端的token傳遞到web端的過程 關于token我不多說,這個大家應該都懂
修改過後的代碼可以在下面的連結中下載下傳到 代碼注釋非常清楚 可以直接運作 請先看reedme!
首先是編寫服務端
需要的node moudle如下
winston 日志子產品 可以用log4替代 做日志記錄用 需安裝 npm installwinston 下同
express web伺服器兼架構 主要利用裡面一些現成的東西
socket.io 長連結的服務子產品
request 網絡請求 如果你的node server需要與後端web server進行通信 需要這個 選裝
定義日志記錄
var winston = require('winston');
var logger = new (winston.Logger)({
transports: [
new (winston.transports.Console)({level: "info", timestamp: true}),//設定日志級别
new (winston.transports.File)({ filename: 'access.log',json: false})//設定日志記錄的檔案及各式
]
});
編寫http服務
由于手機端沒有必要做長連結 一般是以短連結的形式 當然你使用長連結也可以 可以省掉這一段
var http = require('http');
//加載url子產品 解析get參數
var url = require('url');
//加載query子產品 解析post參數
var query = require("querystring");
//開啟http服務
var server = http.createServer(function (request,response) {
//擷取url通路參數
var pathUrl = url.parse(request.url).pathname;
//我這裡不實用rount子產品來實作路由 直接使用switch來做一個簡單的路由
switch (pathUrl){
//手機端連結成功
case '/connection':
var postdata = '';
//當監測到post資料後 将參數追加到postdata中 如果資料量大 這裡可以使用buffer
request.on("data",function(postchunk){
postdata += postchunk;
})
//接受完post資料後
request.on("end",function(){
var data = query.parse(postdata.toString('utf-8'));
if(!isNull(data.uuid)){
errorHead(response,'沒傳遞uuid參數');
response.end();
}
replayToDisplayer(data.uuid,{"status":0,"message":"手機端已連結成功"},'/appconnect')
successHead(response,'連結成功');
})
break;
//手機端确認登陸
case '/confrim':
var postdata = '';
request.on("data",function(postchunk){
postdata += postchunk;
})
request.on("end",function(){
var data = query.parse(postdata.toString('utf-8'));
if(!isNull(data.token)){
errorHead(response,'沒傳遞token參數');
response.end();
}
if(!isNull(data.uuid)){
errorHead(response,'沒傳遞uuid參數');
response.end();
}
replayToDisplayer(data.uuid,{"status":0,"data":{"token":data.token},"message":"手機端已确認登陸"},'/appconfirm')
successHead(response,"手機端已确定登陸",data.token);
})
break;
default:
errorHead(response);
break;
}
}).listen(8889, "127.0.0.1");
//http服務狀态報告
server.once('listening', function() {
logger.info('tcp服務開啟 監聽端口 8889');
});
然後是socket的服務
//定義一個list存放uuid與socket.id的對應關系
//同一時間會有多個用戶端連結在伺服器上 是以要知道手機端到底要将token給哪一個用戶端
//當socket連結建立成功後 每一個連結都有一個獨一無二的socket.id 服務端會根據socket.id來決定要給哪個用戶端發送消息
//而用戶端連結的時候會提供一個唯一參數uuid
//而手機端掃碼完成後 就可以解析道這個參數 進而得知到底要響應那一個用戶端
//我們要做的就是将uuid與socket.id進行綁定 這裡比較難懂一點 多看幾遍
//可以了解成坐飛機 首先你得有張飛機票(socket.id) 而能上飛機的人都有飛機票 但你總得知道你坐在哪裡
//這時候你的座位号就有用了(uuid) 通過你的座位号你才知道 你究竟做哪 而根據座位号 可以反着計算出你的票是那張 這裡要實作的就是票和座位号的綁定關系
<span style="color:#FF0000;">var UUIDMap = {}</span>;//後續請主要關注這個集合的變化
/**
* 開啟socket.io服務
*/
var request = require('request');
var io = require('socket.io').listen(8888);
logger.info('socket服務開啟,監聽端口:', 8888);
/**
* socket.io事件 連接配接成功
* @param {string} event名稱
* @param {function} 連接配接成功的回調函數
*/
io.sockets.on('connection', function (socket) {
logger.info('web端連結成功,socket_id為:', socket.id);
var UUID;
//用戶端進行uuid與socket.id綁定
socket.on('/register', function(data){
UUID = data['uuid'];
UUIDMap[UUID] = socket.id;
logger.info('web端注冊,uuid為', UUID);
});
//用戶端斷開連結
socket.on('/disconnect', function () {
if (UUID != null) {
logger.info('用戶端斷開連結,從連接配接池中删除', "uuid 為"+UUID+",socket.id為"+socket.id);
delete UUIDMap[UUID];
}
});
});
公共函數
/**
* http請求成功應答
*/
function successHead(response,notice,token){
response.writeHead(200,{"Content-Type":"text/plain","Content-Type":"text/html; charset=utf-8"});
var message = {"status":"0","data": isNull(token) ? token : "","message" :isNull(notice) ? notice : "請求成功"};
response.write(JSON.stringify(message));
response.end();
}
/**
* http請求失敗應答
*/
function errorHead(response,notice){
response.writeHead(200,{"Content-Type":"text/plain","Content-Type":"text/html; charset=utf-8"});
var message = {"status":"1","data":"","message": isNull(notice) ? notice : "請求位址不存在"};
response.write(JSON.stringify(message));
response.end();
}
/**
* 向指定web端發送資訊
*
* @param {json} data 要傳回的資料
* @param {string} event 要回調用戶端的監聽事件
* @returns {undefined}
*/
function replayToDisplayer(uuid, data, event) {
var submitUUID = uuid;
var displayerSocket = findSocketByUUID(submitUUID);
if (displayerSocket != null) {
logger.info('根據uuid:'+uuid+"找到socket.id:");
displayerSocket.emit(event, data);
}
}
/**
* 通過uuid查找socket connection id
* @param {uuid} data 要傳回的資料
*/
function findSocketByUUID(UUID) {
var targetSocketID = UUIDMap[UUID];
if (targetSocketID != null) {
var targetSocket = io.sockets.connected[targetSocketID];
if (targetSocket != null) {
return targetSocket;
}else{
logger.info('不能根據uuid找到socketid,uuid為', UUID);
}
}
return null;
}
/**
* 判斷是否null
* @param {string} data
* @return bool
*/
function isNull(data){
return (data == "" || data == undefined || data == null) ? false : true;
}
php後端
php後端主要用來生成二維碼和校驗token 校驗部分請根據自身業務編寫 在node server中使用request子產品做一個網絡請求發送到php端來驗證token
include 'qrcode/phpqrcode.php';
//uuid 唯一的标示符 用于指定用戶端收發資訊
$uuid = 'abc123';
//生成二維碼檔案
$filename = 'qrcode'.time().mt_rand(1000,9999).'.png';
//二維碼中包含的資料
$data = [
"ip"=>'127.0.0.1',
"port"=>'8888',
'exprise'=>time()+60,
'uuid'=>$uuid
];
try{
QRcode::png($data,'temp/'.$filename,'L',15);
echo json_encode(['code'=>1,'message'=>'temp/'.$filename,'uuid'=>$uuid]);
}catch(\Exception $e) {
echo json_encode(['code'=>0,'message'=>$e->getMessage()]);
}
用戶端(web)
web端主要加載二維碼 然後即等待伺服器響應 代碼如下
<!Doctype html>
<html>
<head>
<title>掃碼登入demo</title>
<meta charset="utf-8"></meta>
</head>
<body>
<div style='margin:100px auto;width:80%;text-align:center'>
<img src="" class="qrcode" style="display:none;margin:0 auto;"/><br />
<p></p><br />
<button class='button' οnclick="getQrcode()" style='font-size:16px'>擷取二維碼登陸</button>
</div>
<script type="text/javascript" src="js/socket.io.js"></script>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript">
var timeLimit = 60;
var uuid;
/**
* 擷取二維碼
*/
function getQrcode(){
$.ajax({
type: 'POST',
url: '../php/index.php',
data: {},
dataType: 'json',
success: function(data){
if(data.code === 1){
$('.qrcode').attr('src','../php/'+data.message);
$('.qrcode').show();
$('.button').attr('disabled',true);
uuid = data.uuid;
countDown();
init(data.uuid);
console.log('生成二維碼成功,正在建立連結...');
}else{
console.log(data.message);
}
}
});
}
/**
* 重新整理計時器
*/
function countDown(){
var id = setInterval(function (){
var str = '二維碼有效期剩餘:'+timeLimit+'秒';
$('.button').html(str);
if(timeLimit >0){
timeLimit--;
}else{
timeLimit = 60;
clearInterval(id);
$('.button').html('擷取二維碼登陸');
$('.button').attr('disabled',false);
}
},1000);
}
/*
* 初始化連結
*/
function init(uuid) {
var socket = io.connect('http://127.0.0.1:8888');
//向伺服器發送uuid綁定socket.id
socket.emit('/register',{uuid:uuid});
console.log("連結成功");
//手機端掃碼成功
socket.on('/appconnect',function(data){
console.log(data);
//後續操作 頁面顯示掃碼成功啦等等
});
//手機端确認登陸
socket.on('/appconfirm',function(data){
//實際上就是要手機的token 掃碼登陸實際上就是把手機的token傳遞到web端上
console.log(data);
//後續操作 比如跳轉頁面
});
}
</script>
</body>
</html>
手機端(ios swift)
掃碼解析
import UIKit
import AVFoundation
class WKQrCodeViewController: UIViewController,AVCaptureMetadataOutputObjectsDelegate {
fileprivate let sWidth = UIScreen.main.bounds.size.width
fileprivate let sHeight = UIScreen.main.bounds.size.height
fileprivate let maskViewColor = UIColor.black
fileprivate let maskViewAlpha : CGFloat = 0.3
deinit {
print("二維碼界面被銷毀了")
}
//比例
let scaleWidth : CGFloat = 0.6
var session:AVCaptureSession?
var lineView:UIImageView? = UIImageView.init(imageName: "qrscan_line")
var timer = Timer()
fileprivate var isSent: Bool = false
override func viewWillAppear(_ animated: Bool) {
//即将進入時對狀态條進行隐藏
UIApplication.shared.setStatusBarHidden(true, with: UIStatusBarAnimation.none)
}
override func viewWillDisappear(_ animated: Bool) {
self.timer.invalidate()
}
override func viewDidLoad() {
super.viewDidLoad()
//二維碼框上的動畫計時器
self.timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(configLine), userInfo: nil, repeats: true)
//擷取攝像裝置,注意是Video而不是Audio
let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
//初始化AV Session來協調和處理AV的輸入和輸出流
let session = AVCaptureSession()
//建立輸入流
let input:AVCaptureDeviceInput? = try! AVCaptureDeviceInput(device: device)
if session.canAddInput(input){
session.addInput(input)
}
//建立輸出流
let output:AVCaptureMetadataOutput = AVCaptureMetadataOutput()
if session.canAddOutput(output){
session.addOutput(output)
//設定輸出流代理,從接收端收到的所有中繼資料都會被傳送到delegate方法,所有delegate方法均在queue中執行
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
//設定中繼資料的類型,這裡是二維碼QRCode
output.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
// //固定寬度
// let gdWidth : CGFloat = 200
// let scaleWidth : CGFloat = 200 / sWidth
//比例
// let scaleWidth : CGFloat = 0.6
/*!
這個是手機橫着的時候的 x,y,w,h
*/
output.rectOfInterest = CGRect(x : (1 - scaleWidth * sHeight / sWidth) / 2, y : (1 - scaleWidth) / 2,width : scaleWidth * sHeight / sWidth, height : scaleWidth)
print(output.rectOfInterest)
}
//建立視訊裝置拍攝視訊區域
let layer:AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer.init(session: session)
layer.videoGravity = AVLayerVideoGravityResizeAspectFill
layer.frame = CGRect(x : 0, y : 0, width : UIScreen.main.bounds.size.height,height : UIScreen.main.bounds.size.width);
self.view.layer.addSublayer(layer)
//上
let topView = UIView()
print((sHeight - sWidth * scaleWidth) / 2)
print(sHeight)//320
print(sWidth)//640
topView.frame = CGRect(x:0, y:0, width:sHeight, height:(sWidth - sHeight * scaleWidth) / 2)
topView.backgroundColor = maskViewColor
topView.alpha = maskViewAlpha
//下
let downView = UIView()
downView.frame = CGRect(x:0, y:sWidth - topView.frame.size.height, width:topView.frame.size.width, height:topView.frame.size.height)
downView.backgroundColor = maskViewColor
downView.alpha = maskViewAlpha
//左
let leftView = UIView()
leftView.frame = CGRect(x:0,y:topView.frame.size.height,width:(sHeight - (sWidth - 2*topView.frame.size.height)) / 2, height:sWidth - 2*topView.frame.size.height)
leftView.backgroundColor = maskViewColor
leftView.alpha = maskViewAlpha
//右
let rightView = UIView()
rightView.frame = CGRect(x:sWidth - 2*topView.frame.size.height + leftView.frame.size.width, y:topView.frame.size.height, width:(sHeight - (sWidth - 2*topView.frame.size.height)) / 2, height:sWidth - 2*topView.frame.size.height)
rightView.backgroundColor = maskViewColor
rightView.alpha = maskViewAlpha
//溫馨提示(上)
var tmpview = UIView()
tmpview = tmpview.configOnPrompt(center: rightView.center)
view.addSubview(tmpview)
//溫馨提示(下)
var lab = UILabel()
lab = lab.configDownPrompt(frame: leftView.frame)
view.addSubview(lab)
self.view.layer.addSublayer(topView.layer)
self.view.layer.addSublayer(downView.layer)
self.view.layer.addSublayer(leftView.layer)
self.view.layer.addSublayer(rightView.layer)
//線
configLine()
//框
configborder()
//取消
configBack()
//開始采集視訊資料
session.startRunning()
}
func configBack() -> Void {
let backButton = UIButton()
backButton.setTitle("取消", for: .normal)
backButton.sizeToFit()
backButton.frame = CGRect(x:sHeight - 37.5, y:15, width:40, height:20)
backButton.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2));
backButton.backgroundColor = UIColor.clear
backButton.addTarget(self, action: #selector(backEvent), for: UIControlEvents.touchUpInside)
view.addSubview(backButton)
}
func backEvent() -> Void {
print("二維碼界面的傳回被點選")
guard (self.presentingViewController? .isKind(of: WKQrConfirmViewController.classForCoder()))! else {
self.presentingViewController?.dismiss(animated: true, completion: nil)
return
}
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
//線(存在問題是圖檔不能夠放在UIImageView上)
func configLine() -> Void {
/*
imageView.contentScaleFactor = [[UIScreen mainScreen] scale];
5
imageView.contentMode = UIViewContentModeScaleAspectFill;
6
imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
7
imageView.clipsToBounds = YES;
*/
// lineView?.contentScaleFactor = UIScreen.main.scale
// lineView?.autoresizingMask = .flexibleHeight
// lineView?.contentMode = .scaleAspectFill
lineView!.frame = CGRect(x: (sHeight - (sWidth - 2*(sWidth - sHeight * scaleWidth) / 2)) / 2 + self.sWidth - 2*(self.sWidth - self.sHeight * self.scaleWidth) / 2,y: (sWidth - sHeight * scaleWidth) / 2, width: 2, height: (sWidth - sHeight * scaleWidth) / 2 + 2)
UIView.animate(withDuration: 2) {
self.lineView!.frame = CGRect(x: (self.sHeight - (self.sWidth - 2*(self.sWidth - self.sHeight * self.scaleWidth) / 2)) / 2,y: (self.sWidth - self.sHeight * self.scaleWidth) / 2,width: 2, height: (self.sWidth - self.sHeight * self.scaleWidth) / 2 + 2)
self.view.addSubview(self.lineView!)
}
}
func configborder() -> Void {
let qrCodeFrameView = UIImageView(image: UIImage(named: "qrscan_frame"))
qrCodeFrameView.frame = CGRect(x:(sHeight - (sWidth - (sWidth - sHeight * scaleWidth))) / 2, y:(sWidth - sHeight * scaleWidth) / 2, width:sWidth - (sWidth - sHeight * scaleWidth), height:sWidth - (sWidth - sHeight * scaleWidth))
view.addSubview(qrCodeFrameView)
self.view.layer.addSublayer(qrCodeFrameView.layer);
}
//實作AVCaptureMetadataOutputObjectsDelegate的成員方法來處理二維碼資訊
@objc(captureOutput:didOutputMetadataObjects:fromConnection:) func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from: AVCaptureConnection!) {
session?.stopRunning()
//擷取二維碼資訊中繼資料
guard let metadataObject = metadataObjects.first else {
return
}
//讓掃描隻執行一次
if isSent == true {
return
}
isSent = true
let readableObject = metadataObject as! AVMetadataMachineReadableCodeObject
//添加震動
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
// MARK: - 拿到加密串base64 解碼 && 反序列化
let decodedData = NSData(base64Encoded
: readableObject.stringValue!, options:.ignoreUnknownCharacters )
let decodedString = String(data: decodedData! as Data, encoding: String.Encoding.utf8)
let UTF8Data = decodedString?.data(using: String.Encoding.utf8)
let oResult = try! JSONSerialization.jsonObject(with: UTF8Data!, options: [JSONSerialization.ReadingOptions.mutableContainers, JSONSerialization.ReadingOptions.mutableLeaves])
print(oResult)
guard let result = oResult as? [String: AnyObject] else {
print("result沒解析出來!!")
return
}
let host = result["host"] as! String
let port = String(describing: result["port"])
let uuid = result["uuid"] as! String
let viewModel = WKQrCodeViewModel()
viewModel.qrCodeUpData(host: host, port : port, UUID: uuid, success: {
let userToken = UserAccountViewModel.sharedUserAccount
let para = ["uuid" : uuid, "token" : userToken.accessToken!] as [String : Any]
print(para)
viewModel.qrCodeUpDataAgain(para: para as! Dictionary<String, String>, success: {
//第二次網絡請求成功,觸發去除二維碼界面,傳回跳進重新整理界面
//完成跳轉後對二維碼界面進行銷毀
self.dismiss(animated: false, completion: nil)
let confirmVC = WKQrConfirmViewController()
self.presentingViewController?.present(confirmVC, animated: true, completion: {
})
}, failure: { (errMsg) in
print("列印第二次失敗資訊:\(errMsg)")
let alert = UIAlertController(title: "溫馨提示", message: "伺服器故障,請取消掃碼", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "知道了", style: .default){(action)->() in
alert.view.isHidden = true
})
alert.view.isHidden = true
self.present(alert, animated: true, completion: {() -> Void in
alert.view.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
alert.view.isHidden = false
})
})
}) { (errMsg) in
print("列印失敗資訊\(errMsg)");
let alert = UIAlertController(title: "溫馨提示", message: "伺服器故障,請取消掃碼", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "知道了", style: .default){(action)->() in
alert.view.isHidden = true
})
alert.view.isHidden = true
self.present(alert, animated: true, completion: {() -> Void in
alert.view.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
alert.view.isHidden = false
})
}
}
override var shouldAutorotate : Bool {
return false
}
override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
//MARK: 二維碼界面提示
extension UIView {
func configOnPrompt(center:CGPoint) -> UIView {
let promptLab1 = UILabel()
let promptLab2 = UILabel()
let backboard = UIView()
backboard.backgroundColor = UIColor.clear
backboard.bounds = CGRect(x: 0,y: 0,width: 300,height: 40)
backboard.center = center
promptLab1.text = "請使用電腦登陸"
promptLab1.textColor = UIColor.white
promptLab1.font = UIFont.boldSystemFont(ofSize: 15)
promptLab1.textAlignment = .center
promptLab1.backgroundColor = UIColor.clear
promptLab1.numberOfLines = 1
promptLab1.frame = CGRect(x: 0,y: 0,width: 300,height: 15)
promptLab2.text = "www.yiqiweikeshangchuan.com"
promptLab2.textColor = UIColor.white
promptLab2.font = UIFont.boldSystemFont(ofSize: 14)
promptLab2.textAlignment = .center
promptLab2.backgroundColor = UIColor.clear
promptLab2.numberOfLines = 1
promptLab2.frame = CGRect(x: 0,y: 20,width: 300,height: 15)
backboard.addSubview(promptLab1)
backboard.addSubview(promptLab2)
backboard.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
return backboard
}
}
extension UILabel {
//溫馨提示lable(下)
func configDownPrompt(frame:CGRect) -> UILabel {
let promptLab = UILabel()
promptLab.text = "掃碼登陸後進行上傳"
promptLab.textColor = UIColor.white
promptLab.font = UIFont.boldSystemFont(ofSize: 15)
promptLab.textAlignment = .center
promptLab.backgroundColor = UIColor.clear
promptLab.numberOfLines = 1
promptLab.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
promptLab.frame = frame
return promptLab
}
}
網絡請求
import Foundation
class WKQrCodeViewModel {
var url = "" //不含路徑
private let netTool = NetworkTools.sharedTools;
func qrCodeUpData(host : String, port : String, UUID : String, success : @escaping ()->(), failure : @escaping (_ errMsg : String) -> ()) {
//協定
let url_protocol = "http://"
//路徑
let url_host = host;
//端口号(暫用8889)
let url_port = ":8889"
//字首
let path = "/connection"
url = url_protocol + url_host + url_port
print("第一次目前的url是\(url)")
//url
let urlStr: String = url + path
//參數
let param = ["uuid":UUID]
print("拼接後的網址是\(urlStr),parameterDic是\(param)")
//請求
netTool.request(.POST, URLString: urlStr, parameters: param as [String : AnyObject]?) { (result, error) in
if error == nil {
guard let result = result as? [String: AnyObject] else {
return
}
guard let status = result["status"] as? String else {
return
}
print(status,result)
if Int(status) == 0 {
success()
}else {
guard let message = result["message"] as? String else {
return
}
failure(message)
}
} else {
failure("網絡異常")
}
}
}
//第二次請求
func qrCodeUpDataAgain(para : Dictionary<String, String>, success:@escaping ()->(), failure:@escaping (_ errMsg : String)->()) -> Void {
print(url)
netTool.request(.POST, URLString: url+"/confrim", parameters: para as [String : AnyObject]?) { (result, error) in
if error == nil {
guard let result = result as? [String: AnyObject] else {
return
}
guard let status = result["status"] as? String else {
return
}
print(status,result)
if Int(status) == 0 {
success()
}else {
guard let message = result["message"] as? String else {
return
}
//伺服器傳回失敗消息
failure(message)
}
}else {
failure("網絡異常")
}
}
}
}
流程圖
運作結果
用戶端
模拟手機端的請求
手機端一般是2次請求
第一次需要告訴web端自己已經成功掃描二維碼并解析
第二次是登陸确認
解析的步驟由手機端實作 這裡我隻是模拟 是以可以看到我的uuid和token是随便寫的
node伺服器端收到的資訊
node伺服器的搭建非常簡單 http://blog.csdn.net/zhangsheng_1992/article/details/51322707
所有代碼可以在這裡找到 https://code.csdn.net/zhangsheng_1992/socket-io/tree/master