Làm chức năng đăng nhập bằng QR Code giống Zalo
Bài đăng này đã không được cập nhật trong 2 năm
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.metrình duyệt gửi một request đếnhttps://id.zalo.me/account/authen?a=qr&t=1và 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=2vớicodegử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ó codetương ứng vớitokensẽ 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=3kè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=2nhậ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=3thà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
 
  
  
 