前言
本文將介紹基于SpringBoot + Vue + Android實作的掃碼登錄demo的總體思路,完整代碼已上傳到GitHub,Web端體驗地址:http://47.116.72.33/(只剩一個月有效期),apk下載地址:https://github.com/zhangjiwei1221/qrscan/releases/tag/0.0.1,用戶名:非空即可,密碼:123456,效果見文末,整體實作如有不妥之處,歡迎交流討論,實作部分參考二維碼掃碼登錄是什么原理,
專案簡介
后端:SpringBoot,Redis,
前端:Vue,Vue Router、VueX、Axios、vue-qr、ElemntUI,
安卓:ZXing、XUI、YHttp,
實作思路
總體的掃碼登錄和OAuth2.0的驗證邏輯相似,如下所示:

用戶選擇掃碼登錄可以看作是A:前端發授權請求,等待app掃碼,
用戶使用app進行掃碼可以看作是B:掃碼進行授權,回傳一個臨時Token供二次認證,
用戶在app進行確認登錄可以看作是C:進行登錄確認,授權用戶在Web端登錄,
后端在用戶確認登錄后回傳一個正式Token即可看作是步驟D,
后續前端根據正式Token訪問后臺介面,正式在Web端進行操作即可看作是E和F,
二次認證的原因
之所以在用戶掃碼之后還需要進行再一次的確認登錄,而不是直接就登錄的原因,則是為了用戶安全考慮,避免用戶掃了其他人需要登錄的二維碼,在未經確認就直接登錄了,導致他人可能會在我們不知道的情況下訪問我們的資訊,
實作步驟
-
用戶訪問網頁端,選擇掃碼登錄
用戶在選擇掃碼登錄時,會向后端發送一個二維碼的生成請求,后端生成
UUID,并保存到Redis(固定有效時間),狀態設定為UNUSED(未使用)狀態,如果Redis快取過期,則為EXPIRE(過期)狀態,前端根據后端回傳的內容生成二維碼,并設定一個定時器,每隔一段時間根據二維碼的內容中的UUID,向后端發送請求,獲取二維碼的狀態,更新界面展示的內容,生成二維碼后端介面:
/** * 生成二維碼內容 * * @return 結果 */ @GetMapping("/generate") public BaseResult generate() { String code = IdUtil.simpleUUID(); redisCache.setCacheObject(code, CodeUtils.getUnusedCodeInfo(), DEFAULT_QR_EXPIRE_SECONDS, TimeUnit.SECONDS); return BaseResult.success(GENERATE_SUCCESS, code); }前端獲取內容,生成二維碼:
getToken() { this.codeStatus = 'EMPTY' this.tip = '正在獲取登錄碼,請稍等' // 有效時間 60 秒 this.effectiveSeconds = 60 clearInterval(this.timer) request({ method: 'get', url: '/code/generate' }).then((response) => { // 請求成功, 設定二維碼內容, 并更新相關資訊 this.code = `${HOST}/code/scan?code=${response.data}` this.codeStatus = 'UNUSED' this.tip = '請使用手機掃碼登錄' this.timer = setInterval(this.getTokenInfo, 2000) }).catch(() => { this.getToken() }) }后端回傳二維碼狀態資訊的介面:
/** * 獲取二維碼狀態資訊 * * @param code 二維碼 * @return 結果 */ @GetMapping("/info") public BaseResult info(String code) { CodeVO codeVO = redisCache.getCacheObject(code); if (codeVO == null) { return BaseResult.success(INVALID_CODE, StringUtils.EMPTY); } return BaseResult.success(GET_SUCCESS, codeVO); }前端輪詢獲取二維碼狀態:
getTokenInfo() { this.effectiveSeconds-- // 二維碼過期 if (this.effectiveSeconds <= 0) { this.codeStatus = 'EXPIRE' this.tip = '二維碼已過期,請重繪' return } // 輪詢查詢二維碼狀態 request({ method: 'get', url: '/code/info', params: { code: this.code.substr(this.code.indexOf('=') + 1) } }).then(response => { const codeVO = response.data // 二維碼過期 if (!codeVO || !codeVO.codeStatus) { this.codeStatus = 'EXPIRE' this.tip = '二維碼已過期,請重繪' return } // 二維碼狀態為為正在登錄 if (codeVO.codeStatus === 'CONFIRMING') { this.username = codeVO.username this.avatar = codeVO.avatar this.codeStatus = 'CONFIRMING' this.tip = '掃碼成功,請在手機上確認' return } // 二維碼狀態為確認登錄 if (codeVO.codeStatus === 'CONFIRMED') { clearInterval(this.timer) const token = codeVO.token store.commit('setToken', token) this.$router.push('/home') Message.success('登錄成功') return } }) } -
使用手機掃碼,二維碼狀態改變
當用戶使用手機掃碼時(已登錄并且為正確的
app,否則掃碼會跳轉到自定義的宣傳頁),會更新二維碼的狀態為CONFIRMING(待確認)狀態,并在Redis快取中新增用戶名及頭像資訊的保存供前端使用展示,此外還會回傳用戶的登錄資訊(登錄地址、瀏覽器、作業系統)給app展示,同時生成一個臨時Token給app(固定有效時間),用戶掃碼時的后臺處理:
/** * 處理未使用狀態的二維碼 * * @param code 二維碼 * @param token token * @return 結果 */ private BaseResult handleUnusedQr(String code, String token) { // 校驗 app 端訪問傳遞的 token boolean isLegal = JwtUtils.verify(token); if (!isLegal) { return BaseResult.error(AUTHENTICATION_FAILED); } // 保存用戶名、頭像資訊, 供前端展示 String username = JwtUtils.getUsername(token); CodeVO codeVO = CodeUtils.getConfirmingCodeInfo(username, DEFAULT_AVATAR_URL); redisCache.setCacheObject(code, codeVO, DEFAULT_QR_EXPIRE_SECONDS, TimeUnit.SECONDS); // 回傳登錄地址、瀏覽器、作業系統以及一個臨時 token 給 app String address = HttpUtils.getRealAddressByIp(); String browser = HttpUtils.getBrowserName(); String os = HttpUtils.getOsName(); String tmpToken = JwtUtils.sign(username); // 將臨時 token 作為鍵, 用戶名為內容存盤在 redis 中 redisCache.setCacheObject(tmpToken, username, DEFAULT_TEMP_TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES); LoginInfoVO loginInfoVO = new LoginInfoVO(address, browser, os, tmpToken); return BaseResult.success(SCAN_SUCCESS, loginInfoVO); } -
手機確認登錄
當用戶在
app中點擊確認登錄時,就會攜帶生成的臨時Token發送更新狀態的請求,二維碼的狀態會被更新為CONFIRMED(已確認登錄)狀態,同時后端會生成一個正式Token保存在Redis中,前端在輪詢更新狀態時獲取這個Token,然后使用這個Token進行登錄,后端處理確認登錄的代碼:
/** * 處理未待確認狀態的二維碼 * * @param code 二維碼 * @param token token * @return 結果 */ private BaseResult handleConfirmingQr(String code, String token) { // 使用臨時 token 獲取用戶名, 并從 redis 中洗掉臨時 token String username = redisCache.getCacheObject(token); if (StringUtils.isBlank(username)) { return BaseResult.error(AUTHENTICATION_FAILED); } redisCache.deleteObject(token); // 根據用戶名生成正式 token并保存在 redis 中供前端使用 String formalToken = JwtUtils.sign(username); CodeVO codeVO = CodeUtils.getConfirmedCodeInfo(username, DEFAULT_AVATAR_URL, formalToken); redisCache.setCacheObject(code, codeVO, DEFAULT_QR_EXPIRE_SECONDS, TimeUnit.SECONDS); return BaseResult.success(CONFIRM_SUCCESS); }
效果演示


轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/302229.html
標籤:java
