Java 語言實現簡易版掃碼登錄

来源:https://www.cnblogs.com/johnlearning/archive/2022/04/30/16205875.html
-Advertisement-
Play Games

相信大家對二維碼都不陌生,生活中到處充斥著掃碼登錄的場景,如登錄網頁版微信、支付寶等。最近學習了一下掃碼登錄的原理,感覺蠻有趣的,於是自己實現了一個簡易版掃碼登錄的 Demo,以此記錄一下學習過程。 ...


基本介紹

相信大家對二維碼都不陌生,生活中到處充斥著掃碼登錄的場景,如登錄網頁版微信、支付寶等。最近學習了一下掃碼登錄的原理,感覺蠻有趣的,於是自己實現了一個簡易版掃碼登錄的 Demo,以此記錄一下學習過程。

實際上是面試的時候被問到了  ̄△ ̄!

原理解析

1. 身份認證機制

在介紹掃碼登錄的原理之前,我們先聊一聊服務端的身份認證機制。以普通的 賬號 + 密碼 登錄方式為例,服務端收到用戶的登錄請求後,首先驗證賬號、密碼的合法性。如果驗證通過,那麼服務端會為用戶分配一個 token,該 token 與用戶的身份信息相關聯,可作為用戶的登錄憑證。之後 PC 端再次發送請求時,需要在請求的 Header 或者 Query 參數中攜帶 token,服務端根據 token 便可識別出當前用戶。token 的優點是更加方便、安全,它降低了賬號密碼被劫持的風險,而且用戶不需要重覆地輸入賬號和密碼。PC 端通過賬號和密碼登錄的過程如下:

掃碼登錄本質上也是一種身份認證方式,賬號 + 密碼 登錄與掃碼登錄的區別在於,前者是利用 PC 端的賬號和密碼為 PC 端申請一個 token,後者是利用 手機端的 token + 設備信息 為 PC 端申請一個 token。這兩種登錄方式的目的相同,都是為了使 PC 端獲得服務端的 "授權",在為 PC 端申請 token 之前,二者都需要向服務端證明自己的身份,也就是必須讓服務端知道當前用戶是誰,這樣服務端才能為其生成 PC 端 token。由於掃碼前手機端一定是處於已登錄狀態的,因此手機端本身已經保存了一個 token,該 token 可用於服務端的身份識別。那麼為什麼手機端在驗證身份時還需要設備信息呢?實際上,手機端的身份認證和 PC 端略有不同:

  1. 手機端在登錄前也需要輸入賬號和密碼,但登錄請求中除了賬號密碼外還包含著設備信息,例如設備類型、設備 id 等。

  2. 接收到登錄請求後,服務端會驗證賬號和密碼,驗證通過後,將用戶信息與設備信息關聯起來,也就是將它們存儲在一個數據結構 structure 中。

  3. 服務端為手機端生成一個 token,並將 token 與用戶信息、設備信息關聯起來,即以 token 為 key,structure 為 value,將該鍵值對持久化保存到本地,之後將 token 返回給手機端。

  4. 手機端發送請求,攜帶 token 和設備信息,服務端根據 token 查詢出 structure,並驗證 structure 中的設備信息和手機端的設備信息是否相同,以此判斷用戶的有效性。

我們在 PC 端登錄成功後,可以短時間內正常瀏覽網頁,但之後訪問網站時就要重新登陸了,這是因為 token 是有過期時間的,較長的有效時間會增大 token 被劫持的風險。但是,手機端好像很少有這種問題,例如微信登錄成功後可以一直使用,即使關閉微信或重啟手機。這是因為設備信息具有唯一性,即使 token 被劫持了,由於設備信息不同,攻擊者也無法向服務端證明自己的身份,這樣大大提高了安全繫數,因此 token 可以長久使用。手機端通過賬號密碼登錄的過程如下:

2. 流程概述

瞭解了服務端的身份認證機制後,我們再聊一聊掃碼登錄的整個流程。以網頁版微信為例,我們在 PC 端點擊二維碼登錄後,瀏覽器頁面會彈出二維碼圖片,此時打開手機微信掃描二維碼,PC 端隨即顯示 "正在掃碼",手機端點擊確認登錄後,PC 端就會顯示 "登陸成功" 了。

上述過程中,服務端可以根據手機端的操作來響應 PC 端,那麼服務端是如何將二者關聯起來的呢?答案就是通過 "二維碼",嚴格來說是通過二維碼中的內容。使用二維碼解碼器掃描網頁版微信的二維碼,可以得到如下內容:

由上圖我們得知,二維碼中包含的其實是一個網址,手機掃描二維碼後,會根據該網址向服務端發送請求。接著,我們打開 PC 端瀏覽器的開發者工具:

可見,在顯示出二維碼之後,PC 端一直都沒有 "閑著",它通過輪詢的方式不斷向服務端發送請求,以獲知手機端操作的結果。這裡我們註意到,PC 端發送的 URL 中有一個參數 uuid,值為 "Adv-NP1FYw==",該 uuid 也存在於二維碼包含的網址中。由此我們可以推斷,服務端在生成二維碼之前會先生成一個二維碼 id,二維碼 id 與二維碼的狀態、過期時間等信息綁定在一起,一同存儲在服務端。手機端可以根據二維碼 id 操作服務端二維碼的狀態,PC 端可以根據二維碼 id 向服務端詢問二維碼的狀態。

二維碼最初為 "待掃描" 狀態,手機端掃碼後服務端將其狀態改為 "待確認" 狀態,此時 PC 端的輪詢請求到達,服務端向其返回 "待確認" 的響應。手機端確認登錄後,二維碼變成 "已確認" 狀態,服務端為 PC 端生成用於身份認證的 token,PC 端再次詢問時,就可以得到這個 token。整個掃碼登錄的流程如下圖所示:

  1. PC 端發送 "掃碼登錄" 請求,服務端生成二維碼 id,並存儲二維碼的過期時間、狀態等信息。

  2. PC 端獲取二維碼並顯示。

  3. PC 端開始輪詢檢查二維碼的狀態,二維碼最初為 "待掃描" 狀態。

  4. 手機端掃描二維碼,獲取二維碼 id。

  5. 手機端向服務端發送 "掃碼" 請求,請求中攜帶二維碼 id、手機端 token 以及設備信息。

  6. 服務端驗證手機端用戶的合法性,驗證通過後將二維碼狀態置為 "待確認",並將用戶信息與二維碼關聯在一起,之後為手機端生成一個一次性 token,該 token 用作確認登錄的憑證。

  7. PC 端輪詢時檢測到二維碼狀態為 "待確認"。

  8. 手機端向服務端發送 "確認登錄" 請求,請求中攜帶著二維碼 id、一次性 token 以及設備信息。

  9. 服務端驗證一次性 token,驗證通過後將二維碼狀態置為 "已確認",併為 PC 端生成 PC 端 token。

  10. PC 端輪詢時檢測到二維碼狀態為 "已確認",並獲取到了 PC 端 token,之後 PC 端不再輪詢。

  11. PC 端通過 PC 端 token 訪問服務端。

上述過程中,我們註意到,手機端掃碼後服務端會返回一個一次性 token,該 token 也是一種身份憑證,但它只能使用一次。一次性 token 的作用是確保 "掃碼請求" 與 "確認登錄" 請求由同一個手機端發出,也就是說,手機端用戶不能 "幫其他用戶確認登錄"。

關於一次性 token 的知識本人也不是很瞭解,但可以推測,在服務端的緩存中,一次性 token 映射的 value 應該包含 "掃碼" 請求傳入的二維碼信息、設備信息以及用戶信息。

代碼實現

1. 環境準備

  • JDK 1.8:項目使用 Java 語言編寫。

  • Maven:依賴管理。

  • Redis:Redis 既作為資料庫存儲用戶的身份信息(為了簡化操作未使用 MySQL),也作為緩存存儲二維碼信息、token 信息等。

2. 主要依賴

  • SpringBoot:項目基本環境。

  • Hutool:開源工具類,其中的 QrCodeUtil 可用於生成二維碼圖片。

  • Thymeleaf:模板引擎,用於頁面渲染。

3. 生成二維碼

二維碼的生成以及二維碼狀態的保存邏輯如下:

@RequestMapping(path = "/getQrCodeImg", method = RequestMethod.GET)
public String createQrCodeImg(Model model) {

   String uuid = loginService.createQrImg();
   String qrCode = Base64.encodeBase64String(QrCodeUtil.generatePng("http://127.0.0.1:8080/login/uuid=" + uuid, 300, 300));

   model.addAttribute("uuid", uuid);
   model.addAttribute("QrCode", qrCode);

   return "login";
}

PC 端訪問 "登錄" 請求時,服務端調用 createQrImg 方法,生成一個 uuid 和一個 LoginTicket 對象,LoginTicket 對象中封裝了用戶的 userId 和二維碼的狀態。然後服務端將 uuid 作為 key,LoginTicket 對象作為 value 存入到 Redis 伺服器中,並設置有效時間為 5 分鐘(二維碼的有效時間),createQrImg 方法的邏輯如下:

public String createQrImg() {
   // uuid
   String uuid = CommonUtil.generateUUID();
   LoginTicket loginTicket = new LoginTicket();
   // 二維碼最初為 WAITING 狀態
   loginTicket.setStatus(QrCodeStatusEnum.WAITING.getStatus());

   // 存入 redis
   String ticketKey = CommonUtil.buildTicketKey(uuid);
   cacheStore.put(ticketKey, loginTicket, LoginConstant.WAIT_EXPIRED_SECONDS, TimeUnit.SECONDS);

   return uuid;
}

我們在前一節中提到,手機端的操作主要影響二維碼的狀態,PC 端輪詢時也是查看二維碼的狀態,那麼為什麼還要在 LoginTicket 對象中封裝 userId 呢?這樣做是為了將二維碼與用戶進行關聯,想象一下我們登錄網頁版微信的場景,手機端掃碼後,PC 端就會顯示用戶的頭像,雖然手機端並未確認登錄,但 PC 端輪詢時已經獲取到了當前掃碼的用戶(僅頭像信息)。因此手機端掃碼後,需要將二維碼與用戶綁定在一起,使用 LoginTicket 對象只是一種實現方式。二維碼生成後,我們將其狀態置為 "待掃描" 狀態,userId 不做處理,預設為 null。

4. 掃描二維碼

手機端發送 "掃碼" 請求時,Query 參數中攜帶著 uuid,服務端接收到請求後,調用 scanQrCodeImg 方法,根據 uuid 查詢出二維碼並將其狀態置為 "待確認" 狀態,操作完成後服務端向手機端返回 "掃碼成功" 或 "二維碼已失效" 的信息:

@RequestMapping(path = "/scan", method = RequestMethod.POST)
@ResponseBody
public Response scanQrCodeImg(@RequestParam String uuid) {
   JSONObject data = loginService.scanQrCodeImg(uuid);
   if (data.getBoolean("valid")) {
      return Response.createResponse("掃碼成功", data);
   }
   return Response.createErrorResponse("二維碼已失效");
}

scanQrCodeImg 方法的主要邏輯如下:

public JSONObject scanQrCodeImg(String uuid) {
   // 避免多個移動端同時掃描同一個二維碼
   lock.lock();
   JSONObject data = new JSONObject();
   try {
      String ticketKey = CommonUtil.buildTicketKey(uuid);
      LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey);

      // redis 中 key 過期後也可能不會立即刪除
      Long expired = cacheStore.getExpireForSeconds(ticketKey);
      boolean valid = loginTicket != null &&
               QrCodeStatusEnum.parse(loginTicket.getStatus()) == QrCodeStatusEnum.WAITING &&
               expired != null &&
               expired >= 0;
      if (valid) {
            User user = hostHolder.getUser();
            if (user == null) {
               throw new RuntimeException("用戶未登錄");
            }
            // 修改掃碼狀態
            loginTicket.setStatus(QrCodeStatusEnum.SCANNED.getStatus());
            Condition condition = CONDITION_CONTAINER.get(uuid);
            if (condition != null) {
               condition.signal();
               CONDITION_CONTAINER.remove(uuid);
            }
            // 將二維碼與用戶進行關聯
            loginTicket.setUserId(user.getUserId());
            cacheStore.put(ticketKey, loginTicket, expired, TimeUnit.SECONDS);

            // 生成一次性 token, 用於之後的確認請求
            String onceToken = CommonUtil.generateUUID();

            cacheStore.put(CommonUtil.buildOnceTokenKey(onceToken), uuid, LoginConstant.ONCE_TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);

            data.put("once_token", onceToken);
      }
      data.put("valid", valid);
      return data;
   } finally {
      lock.unlock();
   }
}
  1. 首先根據 uuid 查詢 Redis 中存儲的 LoginTicket 對象,然後檢查二維碼的狀態是否為 "待掃描" 狀態,如果是,那麼將二維碼的狀態改為 "待確認" 狀態。如果不是,那麼該二維碼已被掃描過,服務端提示用戶 "二維碼已失效"。我們規定,只允許第一個手機端能夠掃描成功,加鎖的目的是為了保證 查詢 + 修改 操作的原子性,避免兩個手機端同時掃碼,且同時檢測到二維碼的狀態為 "待掃描"。

  2. 上一步操作成功後,服務端將 LoginTicket 對象中的 userId 置為當前用戶(掃碼用戶)的 userId,也就是將二維碼與用戶信息綁定在一起。由於掃碼請求是由手機端發送的,因此該請求一定來自於一個有效的用戶,我們在項目中配置一個攔截器(也可以是過濾器),當攔截到 "掃碼" 請求後,根據請求中的 token(手機端發送請求時一定會攜帶 token)查詢出用戶信息,並將其存儲到 ThreadLocal 容器(hostHolder)中,之後綁定信息時就可以從 ThreadLocal 容器將用戶信息提取出來。註意,這裡的 token 指的手機端 token,實際中應該還有設備信息,但為了簡化操作,我們忽略掉設備信息。

  3. 用戶信息與二維碼信息關聯在一起後,服務端為手機端生成一個一次性 token,並存儲到 Redis 伺服器,其中 key 為一次性 token 的值,value 為 uuid。一次性 token 會返回給手機端,作為 "確認登錄" 請求的憑證。

上述代碼中,當二維碼的狀態被修改後,我們喚醒了在 condition 中阻塞的線程,這一步的目的是為了實現長輪詢操作,下文中會介紹長輪詢的設計思路。

5. 確認登錄

手機端發送 "確認登錄" 請求時,Query 參數中攜帶著 uuid,且 Header 中攜帶著一次性 token,服務端接收到請求後,首先驗證一次性 token 的有效性,即檢查一次性 token 對應的 uuid 與 Query 參數中的 uuid 是否相同,以確保掃碼操作和確認操作來自於同一個手機端,該驗證過程可在攔截器中配置。驗證通過後,服務端調用 confirmLogin 方法,將二維碼的狀態置為 "已確認":

@RequestMapping(path = "/confirm", method = RequestMethod.POST)
@ResponseBody
public Response confirmLogin(@RequestParam String uuid) {
   boolean logged = loginService.confirmLogin(uuid);
   String msg = logged ? "登錄成功!" : "二維碼已失效!";
   return Response.createResponse(msg, logged);
}

confirmLogin 方法的主要邏輯如下:

public boolean confirmLogin(String uuid) {
   String ticketKey = CommonUtil.buildTicketKey(uuid);
   LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey);
   boolean logged = true;
   Long expired = cacheStore.getExpireForSeconds(ticketKey);
   if (loginTicket == null || expired == null || expired == 0) {
      logged = false;
   } else {
      lock.lock();
      try {
            loginTicket.setStatus(QrCodeStatusEnum.CONFIRMED.getStatus());
            Condition condition = CONDITION_CONTAINER.get(uuid);
            if (condition != null) {
               condition.signal();
               CONDITION_CONTAINER.remove(uuid);
            }
            cacheStore.put(ticketKey, loginTicket, expired, TimeUnit.SECONDS);
      } finally {
            lock.unlock();
      }
   }
   return logged;
}

該方法會根據 uuid 查詢二維碼是否已經過期,如果未過期,那麼就修改二維碼的狀態。

6. PC 端輪詢

輪詢操作指的是前端重覆多次向後端發送相同的請求,以獲知數據的變化。輪詢分為長輪詢和短輪詢:

  • 長輪詢:服務端收到請求後,如果有數據,那麼就立即返回,否則線程進入等待狀態,直到有數據到達或超時,瀏覽器收到響應後立即重新發送相同的請求。

  • 短輪詢:服務端收到請求後無論是否有數據都立即返回,瀏覽器收到響應後間隔一段時間後重新發送相同的請求。

由於長輪詢相比短輪詢能夠得到實時的響應,且更加節約資源,因此項目中我們考慮使用 ReentrantLock 來實現長輪詢。輪詢的目的是為了查看二維碼狀態的變化:

@RequestMapping(path = "/getQrCodeStatus", method = RequestMethod.GET)
@ResponseBody
public Response getQrCodeStatus(@RequestParam String uuid, @RequestParam int currentStatus) throws InterruptedException {
   JSONObject data = loginService.getQrCodeStatus(uuid, currentStatus);
   return Response.createResponse(null, data);
}

getQrCodeStatus 方法的主要邏輯如下:

public JSONObject getQrCodeStatus(String uuid, int currentStatus) throws InterruptedException {
   lock.lock();
   try {
      JSONObject data = new JSONObject();
      String ticketKey = CommonUtil.buildTicketKey(uuid);
      LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey);

      QrCodeStatusEnum statusEnum = loginTicket == null || QrCodeStatusEnum.parse(loginTicket.getStatus()) == QrCodeStatusEnum.INVALID ?
               QrCodeStatusEnum.INVALID : QrCodeStatusEnum.parse(loginTicket.getStatus());

      if (currentStatus == statusEnum.getStatus()) {
            Condition condition = CONDITION_CONTAINER.get(uuid);
            if (condition == null) {
               condition = lock.newCondition();
               CONDITION_CONTAINER.put(uuid, condition);
            }
            condition.await(LoginConstant.POLL_WAIT_TIME, TimeUnit.SECONDS);
      }
      // 用戶掃碼後向 PC 端返回頭像信息
      if (statusEnum == QrCodeStatusEnum.SCANNED) {
            User user = userService.getCurrentUser(loginTicket.getUserId());
            data.put("avatar", user.getAvatar());
      }

      // 用戶確認後為 PC 端生成 access_token
      if (statusEnum == QrCodeStatusEnum.CONFIRMED) {
            String accessToken = CommonUtil.generateUUID();
            cacheStore.put(CommonUtil.buildAccessTokenKey(accessToken), loginTicket.getUserId(), LoginConstant.ACCESS_TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
            data.put("access_token", accessToken);
      }

      data.put("status", statusEnum.getStatus());
      data.put("message", statusEnum.getMessage());
      return data;
   } finally {
      lock.unlock();
   }
}

該方法接收兩個參數,即 uuid 和 currentStatus,其中 uuid 用於查詢二維碼,currentStatus 用於確認二維碼狀態是否發生了變化,如果是,那麼需要立即向 PC 端反饋。我們規定 PC 端在輪詢時,請求的參數中需要攜帶二維碼當前的狀態。

  1. 首先根據 uuid 查詢出二維碼的最新狀態,並比較其是否與 currentStatus 相同。如果相同,那麼當前線程進入阻塞狀態,直到被喚醒或者超時。

  2. 如果二維碼狀態為 "待確認",那麼服務端向 PC 端返回掃碼用戶的頭像信息(處於 "待確認" 狀態時,二維碼已與用戶信息綁定在一起,因此可以查詢出用戶的頭像)。

  3. 如果二維碼狀態為 "已確認",那麼服務端為 PC 端生成一個 token,在之後的請求中,PC 端可通過該 token 表明自己的身份。

上述代碼中的加鎖操作是為了能夠令當前處理請求的線程進入阻塞狀態,當二維碼的狀態發生變化時,我們再將其喚醒,因此上文中的掃碼操作和確認登錄操作完成後,還會有一個喚醒線程的過程。

實際上,加鎖操作設計得不太合理,因為我們只設置了一把鎖。因此對不同二維碼的查詢或修改操作都會搶占同一把鎖。按理來說,不同二維碼的操作之間應該是相互獨立的,即使加鎖,也應該是為每個二維碼均配一把鎖,但這樣做代碼會更加複雜,或許有其它更好的實現長輪詢的方式?或者乾脆直接短輪詢。當然,也可以使用 WebSocket 實現長連接。

7. 攔截器配置

項目中配置了兩個攔截器,一個用於確認用戶的身份,即驗證 token 是否有效:

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private CacheStore cacheStore;

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String accessToken = request.getHeader("access_token");
        // access_token 存在
        if (StringUtils.isNotEmpty(accessToken)) {
            String userId = (String) cacheStore.get(CommonUtil.buildAccessTokenKey(accessToken));
            User user = userService.getCurrentUser(userId);
            hostHolder.setUser(user);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        hostHolder.clear();
    }
}

如果 token 有效,那麼服務端根據 token 獲取用戶的信息,並將用戶信息存儲到 ThreadLocal 容器。手機端和 PC 端的請求都由該攔截器處理,如 PC 端的 "查詢用戶信息" 請求,手機端的 "掃碼" 請求。由於我們忽略了手機端驗證時所需要的的設備信息,因此 PC 端和手機端 token 可以使用同一套驗證邏輯。

另一個攔截器用於攔截 "確認登錄" 請求,即驗證一次性 token 是否有效:

@Component
public class ConfirmInterceptor implements HandlerInterceptor {

    @Autowired
    private CacheStore cacheStore;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        String onceToken = request.getHeader("once_token");
        if (StringUtils.isEmpty(onceToken)) {
            return false;
        }
        if (StringUtils.isNoneEmpty(onceToken)) {
            String onceTokenKey = CommonUtil.buildOnceTokenKey(onceToken);
            String uuidFromCache = (String) cacheStore.get(onceTokenKey);
            String uuidFromRequest = request.getParameter("uuid");
            if (!StringUtils.equals(uuidFromCache, uuidFromRequest)) {
                throw new RuntimeException("非法的一次性 token");
            }
            // 一次性 token 檢查完成後將其刪除
            cacheStore.delete(onceTokenKey);
        }
        return true;
    }
}

該攔截器主要攔截 "確認登錄" 請求,需要註意的是,一次性 token 驗證通過後要立即將其刪除。

編碼過程中,我們簡化了許多操作,例如:1. 忽略掉了手機端的設備信息;2. 手機端確認登錄後並沒有直接為用戶生成 PC 端 token,而是在輪詢時生成。

效果演示

1. 工具準備

  • 瀏覽器:PC 端操作

  • Postman:模仿手機端操作。

2. 數據準備

由於我們沒有實現真實的手機端掃碼的功能,因此使用 Postman 模仿手機端向服務端發送請求。首先我們需要確保服務端存儲著用戶的信息,即在 Test 類中執行如下代碼:

@Test
void insertUser() {
   User user = new User();
   user.setUserId("1");
   user.setUserName("John同學");
   user.setAvatar("/avatar.jpg");
   cacheStore.put("user:1", user);
}

手機端發送請求時需要攜帶手機端 token,這裡我們為 useId 為 "1" 的用戶生成一個 token(手機端 token):

@Test
void loginByPhone() {
   String accessToken = CommonUtil.generateUUID();
   System.out.println(accessToken);
   cacheStore.put(CommonUtil.buildAccessTokenKey(accessToken), "1");
}

手機端 token(accessToken)為 "aae466837d0246d486f644a3bcfaa9e1"(隨機值),之後發送 "掃碼" 請求時需要攜帶這個 token。

3. 掃碼登錄流程展示

啟動項目,訪問 localhost:8080/index

點擊登錄,併在開發者工具中找到二維碼 id(uuid):

打開 Postman,發送 localhost:8080/login/scan 請求,Query 參數中攜帶 uuid,Header 中攜帶手機端 token:

上述請求返回 "掃碼成功" 的響應,同時還返回了一次性 token。此時 PC 端顯示出掃碼用戶的頭像:

在 Postman 中發送 localhost:8080/login/confirm 請求,Query 參數中攜帶 uuid,Header 中攜帶一次性 token:

"確認登錄" 請求發送完成後,PC 端隨即獲取到 PC 端 token,併成功查詢用戶信息:

結語

本文主要介紹了掃碼登錄的原理,並實現了一個簡易版掃碼登錄的 Demo。關於原理部分的理解錯誤以及代碼中的不足之處歡迎大家批評指正(⌒.-),源碼見掃碼登錄,如果覺得有收穫的話給個 Star 吧~。

好文推薦:
[1]. https://juejin.cn/post/6940976355097985032#heading-1
[2]. https://juejin.cn/post/6844904111398191117?utm_source=gold_browser_extension


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • python 學習筆記 變數、運算符與數據類型 點擊標題進行跳轉 容器序列類型 列表 列表是有序集合,無定長,能存儲任意數量和類型的數據,語法為:[元素1, 元素2, ..., 元素n] 創建列表 使用range()創建 使用推導式創建 由於列表中的元素可以任何對象,因此列表中保存的是對象的指針,使 ...
  • 建設目標 平臺介面建設規範旨在為介面開發、測試、使用劃定一個框架邊界,明確技術目標與要求,並要求提供完備的介面文檔說明,為自有平臺與第三方平臺提供數據及服務支持。 建設標準 介面規範 命名規範 在標準的RESTful架構中,每個網址代表一種資源(resource),所以網址中不能有動詞,只能有名詞。 ...
  • 對於從事數據科學和人工智慧領域的人們來說,Python 是大家的首選編程語言。根據最近的一項調查,27% 的程式員開發職位要 求掌握 Python 語言,今年年初這一數字還只是 18.5%。 Python 流行的原因在於其擁有非常直觀的能力:這門語言擁有大量的庫、足夠高的生產效率,還相對易於學習。2 ...
  • 參考資料 The WebSocket Protocol(RFC 6455) Spring Boot 2.6.6 官方文檔 SockJS 什麼是 WebSocket ? WebSocket協議提供了一種標準化的方法,通過單個TCP連接在客戶機和伺服器之間建立全雙工、雙向的通信通道。它是一種不同於HTT ...
  • 前言 數美滑塊的加密及軌跡等應該是入門級別的吧,用他們的教程和話來說 就一個des 然後識別缺口位置可以用cv2或者ddddoc 軌跡 也可以隨便模擬一個,這些簡單的教程 在csdn已經有一大把可以搜到的,但是卻很少人告訴你,它的js好像是一周更新一次,更 新之後post的參數key和des的key ...
  • 今天給大家帶來的這篇文章是關於機器學習的,機器學習有其獨特的數學基礎,我們用微積分來處理變化無限小的函數,並計算 它們的變化;我們使用線性代數來處理計算過程;我們還用概率論與統計學建模不確定性。 在這其中,概率論有其獨特的地位,模型的預測結果、學習過程、學習目標都可以通過概率的角度來理解。 與此同時 ...
  • 除了從文件載入數據,另一個數據源是互聯網,互聯網每天產生各種不同的數據,可以用各種各樣的方式從互聯網載入數據。 一、瞭解 Web API Web 應用編程介面(API)自動請求網站的特定信息,再對這些信息進行可視化。每次運行,都會獲取最新的數據來生成可視化,因此即便網路上的數據瞬息萬變,它呈現的信息 ...
  • ​ 我們現在還是在學習階段因此我們不用配置那麼多的jdk,配置一個jdk8就夠應付日常的學習了。前面的文章我儘量寫詳細一些照顧剛入坑的朋友。後文還有教大家怎麼使用企業版的idea。 一、開發環境的搭建 1)官網下載:官網鏈接 Java Downloads | Oracle ​ 不過官網要註冊ORAC ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...