Làm chức năng đăng nhập bằng QR Code giống Zalo
Mở đầu
- Một chức năng khá tiện cho các trường hợp quên mật khẩu nhưng vẫn còn session trên điện thoại.
- Với đức tính thích tìm tòi, nghịp ngợm chàng sinh viên tên Hoàng đã mở F12 và sau đó ....... Có bài viết này 😂
1. Luồng hoạt động
- Đầu tiên khi vào trang chủ zalo phiên bản web
id.zalo.me
trình duyệt gửi một request đếnhttps://id.zalo.me/account/authen?a=qr&t=1
và response trả về có dạng
{
"code": "A", // Dùng để nhận dạng trình duyệt đang đăng nhập
"image: "B", // Ảnh qrcode dưới dạng base64
"token": "C" // Nội dung trong QRCode
}
Vấn đề đầu tiên: Làm sao nhận dạng được trình duyệt để khi điện thoại quét QR Code thành công mình còn thực hiện các thao tác như xác nhận thông tin, cấp token và chuyển hướng nó đến trang chủ.
- Tiếp theo trình duyệt thực hiện request thứ 2 đến
https://id.zalo.me/account/authen?a=qr&t=2
vớicode
gửi kèm trong body.
Điểm khá thú vị request này sẽ mãi ở trạng thái pending
. Nó chỉ success khi có người quét mã QR Code bằng ứng dụng zalo hoặc request timeout.
Đến đây mọi chuyện đã sáng tỏ, zalo dựa vào "code" để cấp mã nhận diện cho trình duyệt.
- Khi người dùng quét QR Code trong ứng dụng, app sẽ gửi request lên zalo để xác nhận. Nếu thành công thì request ở bước 2 nào có
code
tương ứng vớitoken
sẽ trả về dữ liệu là tên người dùng.
- Đến bước này có thể cho người dùng đăng nhập. Nhưng zalo vẫn bắt thêm 1 bước xác nhận trên điện thoại nữa. Trình duyệt sẽ hiển thị avatar và tên người dùng lên màn hình, và request tiếp đến
https://id.zalo.me/account/authen?a=qr&t=3
kèm vớicode
Vẫn giống như bước 2 request này vẫn sẽ pending cho đến khi người dùng xác nhận, từ chối trên điện thoại hoặc timeout. Nếu thành công người dùng sẽ được cấp token và chuyển hướng đến trang chủ
- Nếu quá lâu không được quét, request ở bước 2, 3 sẽ timeout và yêu cầu người dùng tạo lại mã mới.
2. Thử triển khai demo nho nhỏ
Em sẽ dùng Expressjs để demo nhỏ nhé
- Đầu tiên viết API sinh
code, qrcode, token
app.post("/account/authen", (req,res) => {
const { t } = req.query;
if(t == "1"){
// Sinh code và token random rồi lưu vào database
// Tạo QRCode với token vừa tạo dạng base64
return res.send({ code, qrcode, token })
}
})
-
Viết giao diện và call API
/account/authen?t=1
để render ảnh QR và lưu lạicode
(các bác tự viết nhé). -
Tiếp tục viết API thứ 2 phía backend:
app.post("/account/authen", (req,res) => {
const { t } = req.query;
if(t == "1"){
// Sinh code và token random rồi lưu vào database
// Tao QRCode với token vừa tạo dạng base64
return res.send({ code, qrcode, token })
}
if(t == "2"){
// api này sẽ mãi pending cho đến khi nhận được event confirm của socket (như ảnh dưới)
socket.on(`confirm-info-${req.body.code}`, ({ avatar, name}) => {
return res.send({ avatar, name });
});
}
})
Sau đó request đến /account/authen?t=2
kèm theo code
.
Việc giao tiếp giữa các controller có nhiều cách, ở đây em dùng socket cho nhanh.
- Thay vì viết 1 con app để đọc QR rồi gửi request để confirm thì ta viết 1 api cho mobile rồi giả lập điện thoại quét bằng postman.
app.post("/mobile/authen", (req, res) => {
const { t } = req.query
if(t == 2){
const { token, jwt } = req.body
// Check jwt xem có hợp lệ không ?
// Nếu có ? query database tìm code tương ứng với token
// Hợp lệ emit socket
io.emit(`comfirm-info-${code}`, { name: "Bùi Việt Hoàng" })
res.send("OK")
}
})
- Request
/account/authen?t=2
nhận được event của socket lập tức trả về tên của người dùng. Và tiếp tục request đến/account/authen?t=3
để chờ người dùng xác nhận
Cập nhật code backend
app.post("/account/authen", (req,res) => {
const { t } = req.query;
if(t == "1"){
// Sinh code và token random rồi lưu vào database
// Tao QRCode với token vừa tạo dạng base64
return res.send({ code, qrcode, token })
}
if(t == "2"){
// api này sẽ mãi pending cho đến khi nhận được event confirm của socket (như ảnh dưới)
socket.on(`comfirm-info-${req.body.code}`, ({ avatar, name}) => {
return res.send({ avatar, name });
});
}
if(t == "3"){
// api này sẽ mãi pending cho đến khi nhận được event confirm của socket
socket.on(`comfirm-auth-${req.body.code}`, () => {
// sinh token jwt và trả về cho trình duyệt web có thể đăng nhập
return res.send({ jwt });
});
}
})
app.post("/mobile/authen", (req, res) => {
const { t } = req.query
if(t == 2){
const { token, jwt } = req.body
// Check jwt xem có hợp lệ không ?
// Nếu có ? Query database tìm code tương ứng với token
// Hợp lệ emit socket với code vừa query
io.emit(`comfirm-info-${code}`, { name: "Bùi Việt Hoàng" })
res.send("OK")
}
if(t == "3"){
// Kiểm tra xác thực trên đt
io.emit(`comfirm-auth-${code}`, {})
res.send("OK")
}
})
- Nếu request
/account/authen?t=3
thành công chuyển hướng người dùng đến trang chủ - Xử lý timeout ta viết 1 middleware để xử lý.
app.use(function(req, res, next){
res.setTimeout(30000, function(){
console.log('Mã qr hết hạn, vui lòng tạo mới.');
res.status(408).send();
});
next();
});
3. Kết
Nếu có gì sai sót mong nhận được góp ý từ các bác 😘😘
All rights reserved