欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

基本介紹

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

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

原理解析

1. 身份認(rèn)證機(jī)制

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

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

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

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

2. 流程概述

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

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

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

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

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

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

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

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

代碼實(shí)現(xiàn)

1. 環(huán)境準(zhǔn)備

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

2. 主要依賴

  • SpringBoot:項(xiàng)目基本環(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 端訪問 "登錄" 請求時(shí),服務(wù)端調(diào)用 createQrImg 方法,生成一個(gè) uuid 和一個(gè) LoginTicket 對象,LoginTicket 對象中封裝了用戶的 userId 和二維碼的狀態(tài)。然后服務(wù)端將 uuid 作為 key,LoginTicket 對象作為 value 存入到 Redis 服務(wù)器中,并設(shè)置有效時(shí)間為 5 分鐘(二維碼的有效時(shí)間),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é)中提到,手機(jī)端的操作主要影響二維碼的狀態(tài),PC 端輪詢時(shí)也是查看二維碼的狀態(tài),那么為什么還要在 LoginTicket 對象中封裝 userId 呢?這樣做是為了將二維碼與用戶進(jìn)行關(guān)聯(lián),想象一下我們登錄網(wǎng)頁版微信的場景,手機(jī)端掃碼后,PC 端就會(huì)顯示用戶的頭像,雖然手機(jī)端并未確認(rèn)登錄,但 PC 端輪詢時(shí)已經(jīng)獲取到了當(dāng)前掃碼的用戶(僅頭像信息)。因此手機(jī)端掃碼后,需要將二維碼與用戶綁定在一起,使用 LoginTicket 對象只是一種實(shí)現(xiàn)方式。二維碼生成后,我們將其狀態(tài)置為 "待掃描" 狀態(tài),userId 不做處理,默認(rèn)為 null。

4. 掃描二維碼

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

@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) {
   // 避免多個(gè)移動(dòng)端同時(shí)掃描同一個(gè)二維碼
   lock.lock();
   JSONObject data = new JSONObject();
   try {
      String ticketKey = CommonUtil.buildTicketKey(uuid);
      LoginTicket loginTicket = (LoginTicket) cacheStore.get(ticketKey);

      // redis 中 key 過期后也可能不會(huì)立即刪除
      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);
            }
            // 將二維碼與用戶進(jìn)行關(guān)聯(lián)
            loginTicket.setUserId(user.getUserId());
            cacheStore.put(ticketKey, loginTicket, expired, TimeUnit.SECONDS);

            // 生成一次性 token, 用于之后的確認(rèn)請求
            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 中存儲(chǔ)的 LoginTicket 對象,然后檢查二維碼的狀態(tài)是否為 "待掃描" 狀態(tài),如果是,那么將二維碼的狀態(tài)改為 "待確認(rèn)" 狀態(tài)。如果不是,那么該二維碼已被掃描過,服務(wù)端提示用戶 "二維碼已失效"。我們規(guī)定,只允許第一個(gè)手機(jī)端能夠掃描成功,加鎖的目的是為了保證 查詢 + 修改 操作的原子性,避免兩個(gè)手機(jī)端同時(shí)掃碼,且同時(shí)檢測到二維碼的狀態(tài)為 "待掃描"。

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

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

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

5. 確認(rèn)登錄

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

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

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

6. PC 端輪詢

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

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

由于長輪詢相比短輪詢能夠得到實(shí)時(shí)的響應(yīng),且更加節(jié)約資源,因此項(xiàng)目中我們考慮使用 ReentrantLock 來實(shí)現(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());
      }

      // 用戶確認(rèn)后為 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();
   }
}

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

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

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

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

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

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

7. 攔截器配置

項(xiàng)目中配置了兩個(gè)攔截器,一個(gè)用于確認(rèn)用戶的身份,即驗(yàn)證 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 獲取用戶的信息,并將用戶信息存儲(chǔ)到 ThreadLocal 容器。手機(jī)端和 PC 端的請求都由該攔截器處理,如 PC 端的 "查詢用戶信息" 請求,手機(jī)端的 "掃碼" 請求。由于我們忽略了手機(jī)端驗(yàn)證時(shí)所需要的的設(shè)備信息,因此 PC 端和手機(jī)端 token 可以使用同一套驗(yàn)證邏輯。

另一個(gè)攔截器用于攔截 "確認(rèn)登錄" 請求,即驗(yàn)證一次性 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;
    }
}

該攔截器主要攔截 "確認(rèn)登錄" 請求,需要注意的是,一次性 token 驗(yàn)證通過后要立即將其刪除。

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

效果演示

1. 工具準(zhǔn)備

  • 瀏覽器:PC 端操作
  • Postman:模仿手機(jī)端操作。

2. 數(shù)據(jù)準(zhǔn)備

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

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

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

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

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

3. 掃碼登錄流程展示

啟動(dòng)項(xiàng)目,訪問 localhost:8080/index

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

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

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

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

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

結(jié)語

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

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

相關(guān)文章

  • 詳解Java如何使用責(zé)任鏈默認(rèn)優(yōu)雅地進(jìn)行參數(shù)校驗(yàn)

    詳解Java如何使用責(zé)任鏈默認(rèn)優(yōu)雅地進(jìn)行參數(shù)校驗(yàn)

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

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

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

    springCloud中的Sidecar多語言支持詳解

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

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

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

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

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

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

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

    Spring?Bean名稱不會(huì)被代理的命名技巧

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

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

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

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

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

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

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

最新評論