导语:之前做过一个工具箱,其中登录模块中,有一个扫码登录的功能,模拟了客户端授权,web端自动登录的过程,今天就这个做一个总结归纳。
# 目录
- 概念
- 原理
- 实现
# 概念
扫码登录,就是指当我们在PC端网站上面登录账号的时候,省去了输入用户名和密码的麻烦,只需要打开手机上的对应网站的APP,调用扫码功能,扫描网站上的二维码,确认登录,即可完成网页登录。
# 原理
我这里简单的画了一个示意图,是简化的扫码登录流程。
- 游览器向服务端发起扫码请求;
- 服务端返回唯一ID和二维码;
- 游览器不断轮询查看扫码状态;
- 服务端向APP端发起确认请求;
- APP端弹出请求页面进行操作;
- APP端确认/取消登录后发送服务端;
- 服务端收到扫码结果后处理;
- 确认登录返回用户token,否则返回提示;
- 游览器收到状态后用户信息渲染页面;
# 实现
扫码登录涉及到游览器端,服务端和APP端,由于实际原因,APP端的确认/取消功能由游览器端代理模拟。
# 新建项目
新建一个名为scan
的express项目。
express --view=ejs scan
cd scan
npm i
npm start
1
2
3
4
2
3
4
# 游览器端
这部分放在/public/web/
文件夹下面。
- 首页
<div class="scan">
<div class="scan-inner">
<div class="scan-qrcode">
<div id="qrcode"></div>
<span id="qrcode-text">请使用APP扫码登录</span>
</div>
<div id="scan-welcome">
欢迎光临!
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
body {
margin: 0;
padding: 0;
background-color: #f8f8f8;
}
.scan {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.scan-inner {
padding: 75px 0;
max-width: 320px;
width: 100%;
text-align: center;
box-shadow: 0 0 5px 3px #ddd;
background-color: #fff;
}
.scan #qrcode {
margin: 0 auto 20px auto;
display: block;
width: 150px;
height: 150px;
background-color: #f9f9f9;
box-shadow: 0 0 5px #ccc;
}
.scan #qrcode.active {
border: 3px solid #f00;
}
.scan span {
font-size: 14px;
}
.scan span.active {
color: #f00;
font-weight: bold;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
看下页面效果。
- 变量元素
let timer = null,
qrcode = document.getElementById('qrcode'),
qrcodeText = document.getElementById('qrcode-text'),
scanQrcode = document.querySelector('.scan-qrcode'),
scanWelcome = document.getElementById('scan-welcome');
scanWelcome.style.display = 'none';
scanQrcode.style.display = 'block';
checkLogin();
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 检测登录
// 检测登录
function checkLogin () {
let userInfo = localStorage.getItem('userInfo');
if (!userInfo) {
getQrcode();
} else {
scanWelcome.style.display = 'block';
scanQrcode.style.display = 'none';
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 点击刷新二维码
// 点击获取二维码
qrcode.onclick = function () {
getQrcode();
}
1
2
3
4
2
3
4
- 获取二维码
// 获取授权二维码地址
function getQrcode () {
qrcode.className = '';
qrcodeText.innerText = '请使用APP扫码登录';
qrcodeText.className = '';
const fpPromise = FingerprintJS.load();
fpPromise
.then((fp) => fp.get())
.then(async (result) => {
let bfp = result.visitorId;
let data = await axios.post('/users/scan', {
type: 'qrcode',
uuid: bfp,
});
if (data.data.code === 200) {
showQrcode(data.data.data.url);
queryStatus(data.data.data.code);
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 显示二维码
// 显示二维码
async function showQrcode (url) {
let data = await axios.get('/users/qrcode?url='+url);
let qrcode = document.getElementById('qrcode');
if (data.status === 200) {
qrcode.innerHTML = data.data;
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 轮询查询
function queryStatus (code) {
timer = setInterval(() => {
getStatus(code);
}, 1000);
}
1
2
3
4
5
2
3
4
5
- 获取状态
async function getStatus (code) {
let data = await axios.post('/users/scan', {
type: 'status',
code,
})
if (data.data.code === 200) {
console.log(data.data.data);
if (data.data.data.status === 'scaned') {
qrcodeText.innerText = '用户已扫码!';
}
if (data.data.data.status === 'cancel') {
qrcode.className = 'active';
qrcodeText.innerText = '用户已取消!';
qrcodeText.className = 'active';
}
if (data.data.data.status === 'confirm') {
qrcode.className = '';
qrcodeText.innerText = '用户扫码成功!';
qrcodeText.className = '';
localStorage.setItem('userInfo', JSON.stringify(data.data.data.user));
clearInterval(timer);
setTimeout(() => {
scanWelcome.style.display = 'block';
scanQrcode.style.display = 'none';
}, 3000);
}
} else {
if (data.data.data.status == 'invalid') {
clearInterval(timer);
qrcode.className = 'active';
qrcodeText.innerText = data.data.data.info;
qrcodeText.className = 'active';
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# APP端
这部分放在/public/app/
文件夹下面。
- 公用样式
body {
margin: 0;
padding: 0;
background-color: #f8f8f8;
}
p {
margin: 0;
}
.login {
display: flex;
justify-content: center;
align-items: flex-start;
height: 100vh;
}
.login-inner {
max-width: 768px;
width: 100%;
}
header {
width: 100%;
height: 55px;
line-height: 55px;
color: #fff;
text-align: center;
background-color: #333;
}
form,
.scan-box {
box-sizing: border-box;
margin: 20px auto;
padding: 20px 15px;
width: 90%;
background-color: #fff;
text-align: center;
}
p {
height: 50px;
line-height: 50px;
}
form input {
width: 80%;
height: 25px;
line-height: 25px;
outline: none;
border: 1px solid #ccc;
font-size: 12px;
}
form input::placeholder {
font-size: 12px;
}
form label {
width: 20%;
}
form input[type="submit"] {
width: 100%;
height: 30px;
background-color: #f00;
color: #fff;
border: none;
}
.scan-box {
display: flex;
flex-direction: column;
padding: 30px 15px;
}
.scan-box i {
margin-bottom: 35px;
font-size: 100px;
}
.scan-box button {
margin: 0 auto;
margin-bottom: 15px;
width: 95%;
height: 40px;
outline: none;
border: none;
font-size: 14px;
color: #fff;
border-radius: 4px;
}
.confirm {
background-color: #2d952d;
}
.confirm:hover {
box-shadow: 0 0 3px #2d952d;
}
.cancel {
background-color: #f11515;
}
.cancel:hover {
box-shadow: 0 0 3px #f11515;
}
main {
padding-left: 15px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
- 登录页面
<div class="login">
<div class="login-inner">
<header>APP登录</header>
<form id="user">
<p>
<label for="username">用户:</label>
<input type="text" placeholder="请输入用户名" name="username" id="username" required>
</p>
<p>
<label for="password">密码:</label>
<input type="password" placeholder="请输入密码" name="password" id="password" required>
</p>
<p>
<input type="submit" value="登录">
</p>
</form>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 登录方法
let user = document.getElementById('user'),
username = document.getElementById('username'),
password = document.getElementById('password');
let code = getParams().code;
user.onsubmit = function () {
userLogin();
return false;
}
// 用户登录
async function userLogin () {
let data = await axios.post('/users/login', {
username: username.value,
password: password.value,
code,
});
if (data.data.code === 200) {
localStorage.setItem('userInfo', JSON.stringify(data.data.data.data));
alert(data.data.data.info);
setTimeout(() => {
location.href = '/app/scan.html?code='+code;
}, 1000);
} else {
alert(data.data.data.info);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- 扫码授权页面
<div class="login">
<div class="login-inner">
<header>扫码授权</header>
<div class="scan-box">
<i class="fa fa-solid fa-desktop"></i>
<button class="confirm">确认登录</button>
<button class="cancel">取消授权</button>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 检测登录
let params = getParams();
checkLogin();
// 检测登录
function checkLogin () {
let userInfo = localStorage.getItem('userInfo');
if (userInfo) {
userInfo = JSON.parse(userInfo);
if (userInfo && userInfo.token && params.code) {
sendScanStatus('scaned');
}
} else {
location.href = './login.html?code='+params.code;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 发送状态
// 发送状态
async function sendScanStatus (status) {
let userInfo = localStorage.getItem('userInfo');
if (userInfo) {
userInfo = JSON.parse(userInfo);
}
let data = await axios.post('/users/scan', {
type: 'status',
code: params.code,
token: userInfo.token,
data: {
status,
}
});
if (data.data.code === 101) {
alert(data.data.data.info);
setTimeout(() => {
location.href = './';
}, 1000);
} else {
let status = data.data.data.status;
if (status === 'confirm' ||
status === 'cancel') {
setTimeout(() => {
location.href = './';
}, 1000);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- 确认取消
// 确认取消
let confirmBtn = document.querySelector('.confirm'),
cancelBtn = document.querySelector('.cancel');
confirmBtn.onclick = function () {
sendScanStatus('confirm');
}
cancelBtn.onclick = function () {
sendScanStatus('cancel');
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 首页
<div class="login">
<div class="login-inner">
<header>APP首页</header>
<main>
<h2>Hello,Friend!</h2>
<p>Welcome to website.</p>
</main>
</div>
</div>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 服务端
这部分放在/routes/users.js
文件中。
页面基本上写好了,接下来是服务端。
- 获取二维码图片
npm i qr-image
1
// 引入二维码依赖包
var qrCode = require('qr-image');
// 生成二维码接口
router.get('/qrcode', function (req, res) {
let url = req.query.url;
if (url) {
let result = qrCode.imageSync(url, {
type: 'svg'
});
res.send(result);
} else {
res.send('url is musted.');
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 令牌生成和验证
npm i jsonwebtoken
1
// config/api.js
var jwt = require('jsonwebtoken');
var secret = '123456';
function getToken (data, time = 60) {
let token = jwt.sign(
{
data,
},
secret,
{
expiresIn: time,
}
);
return token;
}
function verifyToken (token) {
return jwt.verify(token, secret, function (err, data) {
if (err) {
return {
code: 101,
data: null,
}
} else {
return {
code: 200,
data,
}
}
});
}
module.exports = {
getToken,
verifyToken,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- 用户登录
// routes/users.js
var apis = require('../config/api');
router.post('/login', function (req, res) {
let params = req.body;
if (params.username && params.password &&
params.username === 'demo' &&
params.password === '123456') {
let userToken = apis.getToken({
name: params.username,
code: params.code,
});
return res.json({
code: 200,
msg: 'get_succ',
data: {
info: '用户登录成功!',
data: {
token: userToken,
}
}
});
} else {
return res.json({
code: 101,
msg: 'get_fail',
data: {
info: '用户名或密码错误!',
}
});
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- 扫码接口
npm i xquuid
1
var xquuid = require('xquuid');
var scanStatus = ''; // 扫码状态
var userToken = ''; // 用户令牌
router.post('/scan', function (req, res) {
let params = req.body;
// 获取二维码地址和ID
if (params.type === 'qrcode') {
scanStatus = 'scaning';
userToken = '';
let code = xquuid.Guid();
let scanToken = apis.getToken({
uuid: params.uuid,
code,
});
let url = 'http://127.0.0.1:3000/app/scan.html?code='+scanToken;
return res.json({
code: 200,
msg: 'get_succ',
data: {
code: scanToken,
url,
}
})
}
// 响应轮询状态
if (params.type === 'status') {
let scanToken = apis.verifyToken(params.code);
// 已过期
if (scanToken.code == 101) {
scanStatus = 'scaning';
userToken = '';
return res.json({
code: 101,
msg: 'get_fail',
data: {
info: '授权二维码已失效!',
status: 'invalid',
}
})
} else {
// 已扫描
if (params.data && params.data.status == 'scaned') {
scanStatus = 'scaned';
}
// 已确认
if (params.data && params.data.status == 'confirm') {
scanStatus = 'confirm';
userToken = params.token;
}
// 已取消
if (params.data && params.data.status == 'cancel') {
scanStatus = 'cancel';
}
if (scanStatus == 'confirm') {
return res.json({
code: 200,
msg: 'get_succ',
data: {
status: scanStatus,
user: userToken,
}
})
} else {
return res.json({
code: 200,
msg: 'get_succ',
data: {
status: scanStatus,
}
})
}
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 写在最后
以上就是一个简单的扫码登录操作开发流程和步骤,有兴趣的可以自己根据文章实现体验一下。
当然可能会有一些疏漏,比如校验授权码等细节就没有顾得上仔细说,总体来说就是这样。