springboot掃碼登錄的簡(jiǎn)單實(shí)現(xiàn)
前言
本文將介紹基于SpringBoot + Vue + Android
實(shí)現(xiàn)的掃碼登錄demo
的總體思路,完整代碼已上傳到GitHub。Web
端體驗(yàn)地址:http://47.116.72.33/(只剩一個(gè)月有效期),apk
下載地址:https://github.com/zhangjiwei1221/qrscan/releases/tag/0.0.1。用戶名:非空即可,密碼:123456,效果見文末,整體實(shí)現(xiàn)如有不妥之處,歡迎交流討論,實(shí)現(xiàn)部分參考二維碼掃碼登錄是什么原理。
項(xiàng)目簡(jiǎn)介
后端:SpringBoot
,Redis
。
前端:Vue
,Vue Router
、VueX
、Axios
、vue-qr
、ElemntUI
。
安卓:ZXing
、XUI
、YHttp
。
實(shí)現(xiàn)思路
總體的掃碼登錄和OAuth2.0
的驗(yàn)證邏輯相似,如下所示:
用戶選擇掃碼登錄可以看作是A
:前端發(fā)授權(quán)請(qǐng)求,等待app
掃碼。
用戶使用app
進(jìn)行掃碼可以看作是B
:掃碼進(jìn)行授權(quán),返回一個(gè)臨時(shí)Token
供二次認(rèn)證。
用戶在app
進(jìn)行確認(rèn)登錄可以看作是C
:進(jìn)行登錄確認(rèn),授權(quán)用戶在Web
端登錄。
后端在用戶確認(rèn)登錄后返回一個(gè)正式Token
即可看作是步驟D
。
后續(xù)前端根據(jù)正式Token
訪問(wèn)后臺(tái)接口,正式在Web
端進(jìn)行操作即可看作是E
和F
。
二次認(rèn)證的原因
之所以在用戶掃碼之后還需要進(jìn)行再一次的確認(rèn)登錄,而不是直接就登錄的原因,則是為了用戶安全考慮,避免用戶掃了其他人需要登錄的二維碼,在未經(jīng)確認(rèn)就直接登錄了,導(dǎo)致他人可能會(huì)在我們不知道的情況下訪問(wèn)我們的信息。
實(shí)現(xiàn)步驟
用戶訪問(wèn)網(wǎng)頁(yè)端,選擇掃碼登錄
用戶在選擇掃碼登錄時(shí),會(huì)向后端發(fā)送一個(gè)二維碼的生成請(qǐng)求,后端生成UUID
,并保存到Redis
(固定有效時(shí)間),狀態(tài)設(shè)置為UNUSED
(未使用)狀態(tài),如果Redis
緩存過(guò)期,則為EXPIRE
(過(guò)期)狀態(tài),前端根據(jù)后端返回的內(nèi)容生成二維碼,并設(shè)置一個(gè)定時(shí)器,每隔一段時(shí)間根據(jù)二維碼的內(nèi)容中的UUID
,向后端發(fā)送請(qǐng)求,獲取二維碼的狀態(tài),更新界面展示的內(nèi)容。
生成二維碼后端接口:
/** * 生成二維碼內(nèi)容 * * @return 結(jié)果 */ @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); }
前端獲取內(nèi)容,生成二維碼:
getToken() { this.codeStatus = 'EMPTY' this.tip = '正在獲取登錄碼,請(qǐng)稍等' // 有效時(shí)間 60 秒 this.effectiveSeconds = 60 clearInterval(this.timer) request({ method: 'get', url: '/code/generate' }).then((response) => { // 請(qǐng)求成功, 設(shè)置二維碼內(nèi)容, 并更新相關(guān)信息 this.code = `${HOST}/code/scan?code=${response.data}` this.codeStatus = 'UNUSED' this.tip = '請(qǐng)使用手機(jī)掃碼登錄' this.timer = setInterval(this.getTokenInfo, 2000) }).catch(() => { this.getToken() }) }
后端返回二維碼狀態(tài)信息的接口:
/** * 獲取二維碼狀態(tài)信息 * * @param code 二維碼 * @return 結(jié)果 */ @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); }
前端輪詢獲取二維碼狀態(tài):
getTokenInfo() { this.effectiveSeconds-- // 二維碼過(guò)期 if (this.effectiveSeconds <= 0) { this.codeStatus = 'EXPIRE' this.tip = '二維碼已過(guò)期,請(qǐng)刷新' return } // 輪詢查詢二維碼狀態(tài) request({ method: 'get', url: '/code/info', params: { code: this.code.substr(this.code.indexOf('=') + 1) } }).then(response => { const codeVO = response.data // 二維碼過(guò)期 if (!codeVO || !codeVO.codeStatus) { this.codeStatus = 'EXPIRE' this.tip = '二維碼已過(guò)期,請(qǐng)刷新' return } // 二維碼狀態(tài)為為正在登錄 if (codeVO.codeStatus === 'CONFIRMING') { this.username = codeVO.username this.avatar = codeVO.avatar this.codeStatus = 'CONFIRMING' this.tip = '掃碼成功,請(qǐng)?jiān)谑謾C(jī)上確認(rèn)' return } // 二維碼狀態(tài)為確認(rèn)登錄 if (codeVO.codeStatus === 'CONFIRMED') { clearInterval(this.timer) const token = codeVO.token store.commit('setToken', token) this.$router.push('/home') Message.success('登錄成功') return } }) }
使用手機(jī)掃碼,二維碼狀態(tài)改變
當(dāng)用戶使用手機(jī)掃碼時(shí)(已登錄并且為正確的app
,否則掃碼會(huì)跳轉(zhuǎn)到自定義的宣傳頁(yè)),會(huì)更新二維碼的狀態(tài)為CONFIRMING
(待確認(rèn))狀態(tài),并在Redis
緩存中新增用戶名及頭像信息的保存供前端使用展示,此外還會(huì)返回用戶的登錄信息(登錄地址、瀏覽器、操作系統(tǒng))給app
展示,同時(shí)生成一個(gè)臨時(shí)Token
給app
(固定有效時(shí)間)。
用戶掃碼時(shí)的后臺(tái)處理:
/** * 處理未使用狀態(tài)的二維碼 * * @param code 二維碼 * @param token token * @return 結(jié)果 */ private BaseResult handleUnusedQr(String code, String token) { // 校驗(yàn) app 端訪問(wèn)傳遞的 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); // 返回登錄地址、瀏覽器、操作系統(tǒng)以及一個(gè)臨時(shí) token 給 app String address = HttpUtils.getRealAddressByIp(); String browser = HttpUtils.getBrowserName(); String os = HttpUtils.getOsName(); String tmpToken = JwtUtils.sign(username); // 將臨時(shí) token 作為鍵, 用戶名為內(nèi)容存儲(chǔ)在 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); }
手機(jī)確認(rèn)登錄
當(dāng)用戶在app
中點(diǎn)擊確認(rèn)登錄時(shí),就會(huì)攜帶生成的臨時(shí)Token
發(fā)送更新狀態(tài)的請(qǐng)求,二維碼的狀態(tài)會(huì)被更新為CONFIRMED
(已確認(rèn)登錄)狀態(tài),同時(shí)后端會(huì)生成一個(gè)正式Token
保存在Redis
中,前端在輪詢更新狀態(tài)時(shí)獲取這個(gè)Token
,然后使用這個(gè)Token
進(jìn)行登錄。
后端處理確認(rèn)登錄的代碼:
/** * 處理未待確認(rèn)狀態(tài)的二維碼 * * @param code 二維碼 * @param token token * @return 結(jié)果 */ private BaseResult handleConfirmingQr(String code, String token) { // 使用臨時(shí) token 獲取用戶名, 并從 redis 中刪除臨時(shí) token String username = redisCache.getCacheObject(token); if (StringUtils.isBlank(username)) { return BaseResult.error(AUTHENTICATION_FAILED); } redisCache.deleteObject(token); // 根據(jù)用戶名生成正式 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); }
效果演示
到此這篇關(guān)于springboot掃碼登錄的簡(jiǎn)單實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)springboot掃碼登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel)
這篇文章主要介紹了詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Springboot配置過(guò)濾器實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Springboot配置過(guò)濾器實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08springcloud 熔斷監(jiān)控Hystrix Dashboard和Turbine
這篇文章主要介紹了springcloud 熔斷監(jiān)控Hystrix Dashboard和Turbine,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Java中instanceof關(guān)鍵字實(shí)例講解
大家好,本篇文章主要講的是Java中instanceof關(guān)鍵字實(shí)例講解,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01Java安全框架——Shiro的使用詳解(附springboot整合Shiro的demo)
這篇文章主要介紹了Java安全框架——Shiro的使用詳解,幫助大家更好的理解和學(xué)習(xí)使用Shiro,感興趣的朋友可以了解下2021-04-04Java中字符數(shù)組、String類、StringBuffer三者之間相互轉(zhuǎn)換
這篇文章主要介紹了Java中字符數(shù)組、String類、StringBuffer三者之間相互轉(zhuǎn)換,需要的朋友可以參考下2018-05-05Mybatis攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限詳解
這篇文章主要介紹了Mybatis攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限詳解, 通過(guò)Mybatis攔截器我們可以攔截某些方法的調(diào)用,我們可以選擇在這些被攔截的方法執(zhí)行前后加上某些邏輯,需要的朋友可以參考下2023-11-11關(guān)于Java中try finally return語(yǔ)句的執(zhí)行順序淺析
這篇文章主要介紹了關(guān)于Java中try finally return語(yǔ)句的執(zhí)行順序淺析,需要的朋友可以參考下2017-08-08