+33

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 😂

image.png

1. Luồng hoạt động

image.png

  1. Đầ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 đến https://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ủ.

  1. 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ới code gửi kèm trong body.

image.png

Đ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.

  1. 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ới token sẽ trả về dữ liệu là tên người dùng.

image.png

  1. Đế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ới code

image.png

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ủ

  1. 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é

  1. Đầ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 })
  }
})
  1. Viết giao diện và call API /account/authen?t=1 để render ảnh QR và lưu lại code (các bác tự viết nhé).

  2. 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.

image.png

  1. 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")
  }
})

image.png

  1. 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

image.png

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")
  }
})
  1. Nếu request /account/authen?t=3thành công chuyển hướng người dùng đến trang chủ
  2. 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

Viblo
Let's register a Viblo Account to get more interesting posts.