完整代码已上传到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
,向后端发送请求,获取二维码的状态,更新界面展示的内容。
生成二维码后端接口:
1
2
3
4
5
6
7
8
9
10
11
12
|
/**
* 生成二维码内容
*
* @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);
}
|
前端获取内容,生成二维码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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()
})
}
|
后端返回二维码状态信息的接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* 获取二维码状态信息
*
* @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);
}
|
前端轮询获取二维码状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
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
(固定有效时间)。
用户扫码时的后台处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* 处理未使用状态的二维码
*
* @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
进行登录。
后端处理确认登录的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* 处理未待确认状态的二维码
*
* @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);
}
|
效果演示
以上就是java编程基于SpringBoot框架实现扫码登录的详细内容,更多关于java编程SpringBoot框架实现扫码登录的资料请关注服务器之家其它相关文章!
原文链接:https://blog.csdn.net/qq_41698074/article/details/120405575