亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

基于Java實現(xiàn)掃碼登錄的示例代碼

 更新時間:2022年04月30日 08:41:12   作者:John同學  
相信大家對二維碼都不陌生,生活中到處充斥著掃碼登錄的場景,如登錄網(wǎng)頁版微信、支付寶等。本文將利用Java實現(xiàn)一個簡易版掃碼登錄的 Demo,需要的可以參考一下

基本介紹

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

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

原理解析

1. 身份認證機制

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

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

  • 手機端在登錄前也需要輸入賬號和密碼,但登錄請求中除了賬號密碼外還包含著設(shè)備信息,例如設(shè)備類型、設(shè)備 id 等。
  • 接收到登錄請求后,服務(wù)端會驗證賬號和密碼,驗證通過后,將用戶信息與設(shè)備信息關(guān)聯(lián)起來,也就是將它們存儲在一個數(shù)據(jù)結(jié)構(gòu) structure 中。
  • 服務(wù)端為手機端生成一個 token,并將 token 與用戶信息、設(shè)備信息關(guān)聯(lián)起來,即以 token 為 key,structure 為 value,將該鍵值對持久化保存到本地,之后將 token 返回給手機端。
  • 手機端發(fā)送請求,攜帶 token 和設(shè)備信息,服務(wù)端根據(jù) token 查詢出 structure,并驗證 structure 中的設(shè)備信息和手機端的設(shè)備信息是否相同,以此判斷用戶的有效性。

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

2. 流程概述

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

上述過程中,服務(wù)端可以根據(jù)手機端的操作來響應(yīng) PC 端,那么服務(wù)端是如何將二者關(guān)聯(lián)起來的呢?答案就是通過 "二維碼",嚴格來說是通過二維碼中的內(nèi)容。使用二維碼解碼器掃描網(wǎng)頁版微信的二維碼,可以得到如下內(nèi)容:

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

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

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

  1. PC 端發(fā)送 "掃碼登錄" 請求,服務(wù)端生成二維碼 id,并存儲二維碼的過期時間、狀態(tài)等信息。
  2. PC 端獲取二維碼并顯示。
  3. PC 端開始輪詢檢查二維碼的狀態(tài),二維碼最初為 "待掃描" 狀態(tài)。
  4. 手機端掃描二維碼,獲取二維碼 id。
  5. 手機端向服務(wù)端發(fā)送 "掃碼" 請求,請求中攜帶二維碼 id、手機端 token 以及設(shè)備信息。
  6. 服務(wù)端驗證手機端用戶的合法性,驗證通過后將二維碼狀態(tài)置為 "待確認",并將用戶信息與二維碼關(guān)聯(lián)在一起,之后為手機端生成一個一次性 token,該 token 用作確認登錄的憑證。
  7. PC 端輪詢時檢測到二維碼狀態(tài)為 "待確認"。
  8. 手機端向服務(wù)端發(fā)送 "確認登錄" 請求,請求中攜帶著二維碼 id、一次性 token 以及設(shè)備信息。
  9. 服務(wù)端驗證一次性 token,驗證通過后將二維碼狀態(tài)置為 "已確認",并為 PC 端生成 PC 端 token。
  10. PC 端輪詢時檢測到二維碼狀態(tài)為 "已確認",并獲取到了 PC 端 token,之后 PC 端不再輪詢。
  11. PC 端通過 PC 端 token 訪問服務(wù)端。

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

關(guān)于一次性 token 的知識本人也不是很了解,但可以推測,在服務(wù)端的緩存中,一次性 token 映射的 value 應(yīng)該包含 "掃碼" 請求傳入的二維碼信息、設(shè)備信息以及用戶信息。

代碼實現(xiàn)

1. 環(huán)境準備

  • JDK 1.8:項目使用 Java 語言編寫。
  • Maven:依賴管理。
  • Redis:Redis 既作為數(shù)據(jù)庫存儲用戶的身份信息(為了簡化操作未使用 MySQL),也作為緩存存儲二維碼信息、token 信息等。

2. 主要依賴

  • SpringBoot:項目基本環(huán)境。
  • Hutool:開源工具類,其中的 QrCodeUtil 可用于生成二維碼圖片。
  • Thymeleaf:模板引擎,用于頁面渲染。

3. 生成二維碼

二維碼的生成以及二維碼狀態(tài)的保存邏輯如下:

@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 端訪問 "登錄" 請求時,服務(wù)端調(diào)用 createQrImg 方法,生成一個 uuid 和一個 LoginTicket 對象,LoginTicket 對象中封裝了用戶的 userId 和二維碼的狀態(tài)。然后服務(wù)端將 uuid 作為 key,LoginTicket 對象作為 value 存入到 Redis 服務(wù)器中,并設(shè)置有效時間為 5 分鐘(二維碼的有效時間),createQrImg 方法的邏輯如下:

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

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

   return uuid;
}

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

4. 掃描二維碼

手機端發(fā)送 "掃碼" 請求時,Query 參數(shù)中攜帶著 uuid,服務(wù)端接收到請求后,調(diào)用 scanQrCodeImg 方法,根據(jù) uuid 查詢出二維碼并將其狀態(tài)置為 "待確認" 狀態(tài),操作完成后服務(wù)端向手機端返回 "掃碼成功" 或 "二維碼已失效" 的信息:

@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("用戶未登錄");
            }
            // 修改掃碼狀態(tài)
            loginTicket.setStatus(QrCodeStatusEnum.SCANNED.getStatus());
            Condition condition = CONDITION_CONTAINER.get(uuid);
            if (condition != null) {
               condition.signal();
               CONDITION_CONTAINER.remove(uuid);
            }
            // 將二維碼與用戶進行關(guān)聯(lián)
            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.首先根據(jù) uuid 查詢 Redis 中存儲的 LoginTicket 對象,然后檢查二維碼的狀態(tài)是否為 "待掃描" 狀態(tài),如果是,那么將二維碼的狀態(tài)改為 "待確認" 狀態(tài)。如果不是,那么該二維碼已被掃描過,服務(wù)端提示用戶 "二維碼已失效"。我們規(guī)定,只允許第一個手機端能夠掃描成功,加鎖的目的是為了保證 查詢 + 修改 操作的原子性,避免兩個手機端同時掃碼,且同時檢測到二維碼的狀態(tài)為 "待掃描"。

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

3.用戶信息與二維碼信息關(guān)聯(lián)在一起后,服務(wù)端為手機端生成一個一次性 token,并存儲到 Redis 服務(wù)器,其中 key 為一次性 token 的值,value 為 uuid。一次性 token 會返回給手機端,作為 "確認登錄" 請求的憑證。

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

5. 確認登錄

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

@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;
}

該方法會根據(jù) uuid 查詢二維碼是否已經(jīng)過期,如果未過期,那么就修改二維碼的狀態(tài)。

6. PC 端輪詢

輪詢操作指的是前端重復多次向后端發(fā)送相同的請求,以獲知數(shù)據(jù)的變化。輪詢分為長輪詢和短輪詢:

  • 長輪詢:服務(wù)端收到請求后,如果有數(shù)據(jù),那么就立即返回,否則線程進入等待狀態(tài),直到有數(shù)據(jù)到達或超時,瀏覽器收到響應(yīng)后立即重新發(fā)送相同的請求。
  • 短輪詢:服務(wù)端收到請求后無論是否有數(shù)據(jù)都立即返回,瀏覽器收到響應(yīng)后間隔一段時間后重新發(fā)送相同的請求。

由于長輪詢相比短輪詢能夠得到實時的響應(yīng),且更加節(jié)約資源,因此項目中我們考慮使用 ReentrantLock 來實現(xiàn)長輪詢。輪詢的目的是為了查看二維碼狀態(tài)的變化:

@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();
   }
}

該方法接收兩個參數(shù),即 uuid 和 currentStatus,其中 uuid 用于查詢二維碼,currentStatus 用于確認二維碼狀態(tài)是否發(fā)生了變化,如果是,那么需要立即向 PC 端反饋。我們規(guī)定 PC 端在輪詢時,請求的參數(shù)中需要攜帶二維碼當前的狀態(tài)。

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

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

3.如果二維碼狀態(tài)為 "已確認",那么服務(wù)端為 PC 端生成一個 token,在之后的請求中,PC 端可通過該 token 表明自己的身份。

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

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

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 有效,那么服務(wù)端根據(jù) token 獲取用戶的信息,并將用戶信息存儲到 ThreadLocal 容器。手機端和 PC 端的請求都由該攔截器處理,如 PC 端的 "查詢用戶信息" 請求,手機端的 "掃碼" 請求。由于我們忽略了手機端驗證時所需要的的設(shè)備信息,因此 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. 忽略掉了手機端的設(shè)備信息;2. 手機端確認登錄后并沒有直接為用戶生成 PC 端 token,而是在輪詢時生成。

效果演示

1. 工具準備

  • 瀏覽器:PC 端操作
  • Postman:模仿手機端操作。

2. 數(shù)據(jù)準備

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

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

手機端發(fā)送請求時需要攜帶手機端 token,這里我們?yōu)?useId 為 "1" 的用戶生成一個 token(手機端 token):

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

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

3. 掃碼登錄流程展示

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

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

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

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

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

"確認登錄" 請求發(fā)送完成后,PC 端隨即獲取到 PC 端 token,并成功查詢用戶信息:

結(jié)語

本文主要介紹了掃碼登錄的原理,并實現(xiàn)了一個簡易版掃碼登錄的 Demo。關(guān)于原理部分的理解錯誤以及代碼中的不足之處歡迎大家批評指正(⌒.-),源碼見掃碼登錄

以上就是基于Java實現(xiàn)掃碼登錄的示例代碼的詳細內(nèi)容,更多關(guān)于Java掃碼登錄的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解Java如何使用責任鏈默認優(yōu)雅地進行參數(shù)校驗

    詳解Java如何使用責任鏈默認優(yōu)雅地進行參數(shù)校驗

    項目中參數(shù)校驗十分重要,它可以保護我們應(yīng)用程序的安全性和合法性。這篇文章主要介紹了如何使用責任鏈默認優(yōu)雅地進行參數(shù)校驗,需要的可以參考一下
    2023-03-03
  • 在IDEA中maven配置MyBatis的流程詳解

    在IDEA中maven配置MyBatis的流程詳解

    剛學完javaweb,對自己的Dao層代碼很不滿意的話,可得來學學MyBatis.學習MyBatis既可以改進JDBC的使用,實現(xiàn)Dao層也會變得很簡便,下面我將介紹IDEA中maven配置MyBatis簡單流程,需要的朋友可以參考下
    2021-06-06
  • springCloud中的Sidecar多語言支持詳解

    springCloud中的Sidecar多語言支持詳解

    這篇文章主要介紹了springCloud中的Sidecar多語言支持詳解,Sidecar是將一組緊密結(jié)合的任務(wù)與主應(yīng)用程序共同放在一臺主機Host中,但會將它們部署在各自的進程或容器中,需要的朋友可以參考下
    2024-01-01
  • MultipartResolver實現(xiàn)文件上傳功能

    MultipartResolver實現(xiàn)文件上傳功能

    這篇文章主要為大家詳細介紹了MultipartResolver實現(xiàn)文件上傳功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-06-06
  • SpringBoot結(jié)合JWT登錄權(quán)限控制的實現(xiàn)

    SpringBoot結(jié)合JWT登錄權(quán)限控制的實現(xiàn)

    本文主要介紹了SpringBoot結(jié)合JWT登錄權(quán)限控制的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-07-07
  • Mybatis中的mapper是如何和XMl關(guān)聯(lián)起來的

    Mybatis中的mapper是如何和XMl關(guān)聯(lián)起來的

    這篇文章主要介紹了Mybatis中的mapper是如何和XMl關(guān)聯(lián)起來的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • Spring?Bean名稱不會被代理的命名技巧

    Spring?Bean名稱不會被代理的命名技巧

    Spring Bean一些使用小細節(jié)就是在不斷的源碼探索中逐步發(fā)現(xiàn)的,今天就來和小伙伴們聊一下通過 beanName 的設(shè)置,可以讓一個 bean 拒絕被代理
    2023-11-11
  • springboot?vue測試平臺接口定義及發(fā)送請求功能實現(xiàn)

    springboot?vue測試平臺接口定義及發(fā)送請求功能實現(xiàn)

    這篇文章主要為大家介紹了springboot+vue測試平臺接口定義及發(fā)送請求功能實現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-05-05
  • MyBatis利用MyCat實現(xiàn)多租戶的簡單思路分享

    MyBatis利用MyCat實現(xiàn)多租戶的簡單思路分享

    這篇文章主要給大家介紹了關(guān)于MyBatis利用MyCat實現(xiàn)多租戶的簡單思路的相關(guān)資料,文中的多租戶是基于多數(shù)據(jù)庫進行實現(xiàn)的,數(shù)據(jù)是通過不同數(shù)據(jù)庫進行隔離,需要的朋友可以參考借鑒,下面來一起看看吧。
    2017-06-06
  • Java開發(fā)常見異常及解決辦法詳解

    Java開發(fā)常見異常及解決辦法詳解

    這篇文章主要介紹了java程序常見異常及處理匯總,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2021-09-09

最新評論