java編程之基于SpringBoot框架實(shí)現(xiàn)掃碼登錄
完整代碼已上傳到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)目簡介
后端:SpringBoot,Redis。
前端:Vue,Vue Router、VueX、Axios、vue-qr、ElemntUI。
安卓:ZXing、XUI、YHttp。
實(shí)現(xiàn)思路
總體的掃碼登錄和OAuth2.0的驗(yàn)證邏輯相似,如下所示:

用戶選擇掃碼登錄可以看作是A:前端發(fā)授權(quán)請求,等待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訪問后臺接口,正式在Web端進(jìn)行操作即可看作是E和F。
二次認(rèn)證的原因
之所以在用戶掃碼之后還需要進(jìn)行再一次的確認(rèn)登錄,而不是直接就登錄的原因,則是為了用戶安全考慮,避免用戶掃了其他人需要登錄的二維碼,在未經(jīng)確認(rèn)就直接登錄了,導(dǎo)致他人可能會在我們不知道的情況下訪問我們的信息。
實(shí)現(xiàn)步驟
用戶訪問網(wǎng)頁端,選擇掃碼登錄
用戶在選擇掃碼登錄時(shí),會向后端發(fā)送一個(gè)二維碼的生成請求,后端生成UUID,并保存到Redis(固定有效時(shí)間),狀態(tài)設(shè)置為UNUSED(未使用)狀態(tài),如果Redis緩存過期,則為EXPIRE(過期)狀態(tài),前端根據(jù)后端返回的內(nèi)容生成二維碼,并設(shè)置一個(gè)定時(shí)器,每隔一段時(shí)間根據(jù)二維碼的內(nèi)容中的UUID,向后端發(fā)送請求,獲取二維碼的狀態(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 = '正在獲取登錄碼,請稍等'
// 有效時(shí)間 60 秒
this.effectiveSeconds = 60
clearInterval(this.timer)
request({
method: 'get',
url: '/code/generate'
}).then((response) => {
// 請求成功, 設(shè)置二維碼內(nèi)容, 并更新相關(guān)信息
this.code = `${HOST}/code/scan?code=${response.data}`
this.codeStatus = 'UNUSED'
this.tip = '請使用手機(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--
// 二維碼過期
if (this.effectiveSeconds <= 0) {
this.codeStatus = 'EXPIRE'
this.tip = '二維碼已過期,請刷新'
return
}
// 輪詢查詢二維碼狀態(tài)
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
}
// 二維碼狀態(tài)為為正在登錄
if (codeVO.codeStatus === 'CONFIRMING') {
this.username = codeVO.username
this.avatar = codeVO.avatar
this.codeStatus = 'CONFIRMING'
this.tip = '掃碼成功,請?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,否則掃碼會跳轉(zhuǎn)到自定義的宣傳頁),會更新二維碼的狀態(tài)為CONFIRMING(待確認(rèn))狀態(tài),并在Redis緩存中新增用戶名及頭像信息的保存供前端使用展示,此外還會返回用戶的登錄信息(登錄地址、瀏覽器、操作系統(tǒng))給app展示,同時(shí)生成一個(gè)臨時(shí)Token給app(固定有效時(shí)間)。
用戶掃碼時(shí)的后臺處理:
/**
* 處理未使用狀態(tài)的二維碼
*
* @param code 二維碼
* @param token token
* @return 結(jié)果
*/
private BaseResult handleUnusedQr(String code, String token) {
// 校驗(yàn) 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);
// 返回登錄地址、瀏覽器、操作系統(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)容存儲在 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í),就會攜帶生成的臨時(shí)Token發(fā)送更新狀態(tài)的請求,二維碼的狀態(tài)會被更新為CONFIRMED(已確認(rèn)登錄)狀態(tài),同時(shí)后端會生成一個(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);
}
效果演示


以上就是java編程基于SpringBoot框架實(shí)現(xiàn)掃碼登錄的詳細(xì)內(nèi)容,更多關(guān)于java編程SpringBoot框架實(shí)現(xiàn)掃碼登錄的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于Java中@SuppressWarnings的正確使用方法
這篇文章主要介紹了關(guān)于Java中@SuppressWarnings的正確使用方法,@SuppressWarnings注解主要用在取消一些編譯器產(chǎn)生的警告對代碼左側(cè)行列的遮擋,有時(shí)候這會擋住我們斷點(diǎn)調(diào)試時(shí)打的斷點(diǎn),需要的朋友可以參考下2023-05-05
java應(yīng)用占用內(nèi)存過高排查的解決方案
這篇文章主要介紹了java應(yīng)用占用內(nèi)存過高排查的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-03-03
Spring-IOC容器-Bean管理-基于XML方式超詳解
這篇文章主要介紹了Spring為IOC容器Bean的管理,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2021-08-08
Java 實(shí)現(xiàn)攔截器Interceptor的攔截功能方式
這篇文章主要介紹了Java 實(shí)現(xiàn)攔截器Interceptor的攔截功能方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10

