扫码登录
随着移动互联网的快速发展,扫码登录已经成为许多应用中的常见功能。它为用户提供了一种便捷、安全的登录方式,尤其是在PC端和移动端之间进行无缝切换时,扫码登录显得尤为重要。本文将介绍如何使用Java实现一个简单的移动端扫码登录功能。
1.原理
实现扫码登录一般有三种实现方式:可以看一下文末的《三种方式实现扫码登录》,本文介绍的是短轮循方式。
扫码登录的基本流程如下:
- 生成二维码:用户在PC端访问登录页面时,服务器生成一个唯一的二维码,并将其展示在页面上。
- 扫描二维码:用户使用移动端应用扫描PC端展示的二维码。
- 确认登录:移动端应用将扫描到的二维码信息发送到服务器,服务器验证后完成登录。
- 登录成功:PC端页面刷新,用户登录成功。
时序图
PC 端 服务器 移动端
| | |
| 1. 请求生成二维码 | |
| ----------------------->| |
| | |
|2.生成token和二维码(Base64)| |
|<----------------------- | |
| | |
| 3. 展示二维码 | |
| | |
| 4. 轮询登录状态 | |
| ----------------------->| |
| | 5. 返回登录状态 |
| <-----------------------| |
| | |
| | 6. 扫描二维码 |
| | <-----------------------|
| | 7. 确认登录 |
| | ----------------------->|
| | 8. 更新登录状态 |
| | <-----------------------|
| 9. 轮询登录状态 | |
| ----------------------->| |
| | 10. 登录状态修改返回登录信息|
| <-----------------------| |
| 11. 跳转到主页 | |
2.实现
这个图片前端和后端都可以生成,这里使用的是后端zxing生成Base64结果返回给前端进行展示
一、生成唯一的token
生成token使用的是sha521,这个传入的值是前端的浏览器指纹。通过这个东西来进行生成唯一的token。
@Slf4j
public class SHA512Util {
public static String generateSHA512Hash(String input) {
try {
// 创建MessageDigest实例并指定SHA-512算法
MessageDigest digest = MessageDigest.getInstance("SHA-512");
// 将输入字符串转换为字节数组并生成哈希值
byte[] hashBytes = digest.digest(input.getBytes());
// 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString().toUpperCase();
} catch (NoSuchAlgorithmException e) {
log.info("生成sha521失败!{}", e.getMessage());
return null;
}
}
}
二、生成图片
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
/**
* @description: 根据文字生成图片/Base64编码
* @author: Leon
* @date: 2025/1/10 18:48
*/
@Slf4j
public class TextPictureConverse {
public static String generateQRCodeBase64(String text, int width, int height) {
try {
// 设置二维码参数
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 设置字符编码
// 生成二维码矩阵
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
// 将二维码写入字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
// 将字节数组转换为 Base64 字符串
byte[] qrCodeBytes = outputStream.toByteArray();
return Base64.getEncoder().encodeToString(qrCodeBytes);
} catch (WriterException | IOException e) {
log.info("生成登录二维码失败!{}", e.getMessage());
throw new GenericException(ResultCode.SCRIPT_EXECUTE_ERROR, "生成登录二维码失败!");
}
}
public static void main(String[] args) {
// 要编码的文本,
String data = "token=63F5DEA1E90CCB2C8A48A72149824812B5A0E26BFA806F03300332D4CE74B47C5993D8DF9C523D0A32B2418EEA19A67CAB4B2158107AD8D62EA5A25FCDB9E5C3";
// 生成的二维码图片文件路径
int width = 600; // 二维码宽度
int height = 600; // 二维码高度
String base64QRCode = generateQRCodeBase64(data, width, height);
System.out.println(base64QRCode);
}
}
三、接入redis
具体代码可以看我接入redis的文章
四、其他代码
public enum CodeLogin {
NOT_SCANNED(0, "未扫描"),
SUCCESS(1, "已成功");
@EnumValue
private final Integer code;
private final String name;
@Generated
private CodeLogin(Integer code, String name) {
this.code = code;
this.name = name;
}
@Generated
public Integer getCode() {
return this.code;
}
@Generated
public String getName() {
return this.name;
}
}
四、代码实现
/**
* @description: 二维码扫码登录
* @author: Leon
* @date: 2025/1/22 14:42
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/login")
public class CodeLoginController {
private final RedisUtils redisUtils;
//接口负载均衡
@Operation(summary = "获取登录二维码")
@GetMapping("/getLoginCode/{t}")
@IgnoreInterceptor(dataPermission = true, tenant = true)
public HashMap<String, String> getAll(@PathVariable("t") @NotEmpty String t) {
//校验token时间是否超过5秒
//根据秒数生成临时token
String token = SHA512Util.generateSHA512Hash(t);
Integer status = (Integer) redisUtils.get(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token);
if (Objects.equals(CodeLogin.NOT_SCANNED.getCode(), status)) {
//直接返回Base64
}
//存到redis
redisUtils.set(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token, CodeLogin.NOT_SCANNED.getCode(), 5);
//返回token和携带token的图片base64
HashMap<String, String> res = new HashMap<>();
res.put("token", token);
res.put("codeBase64", TextPictureConverse.generateQRCodeBase64(token, 300, 300));
return res;
}
@PostMapping("/confirm")
public void confirmLogin(@RequestParam String token) {
//当前登录人Id
Long currentUserId = ContextUtil.getCurrentUserId();
//判断token存在,userId正确
Integer status = (Integer) redisUtils.get(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token);
if (!Objects.isNull(status)) {
if (Objects.equals(CodeLogin.NOT_SCANNED.getCode(), status)) {
//删除
redisUtils.del(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token);
//设置新的redis进行登录携带userId
redisUtils.set(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token, CodeLogin.SUCCESS.getCode() + "-" + currentUserId, 5);
}
}
}
// 检查登录状态
@GetMapping("/status")
@IgnoreInterceptor(dataPermission = true, tenant = true)
@Transactional(rollbackFor = Exception.class)
public String checkLoginStatus(@RequestParam String token) {
//查询redis状态
String redisRes = (String) redisUtils.get(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token);
String[] split = redisRes.split("-");
String status = split[0];
String userId = split[1];
if (!Objects.isNull(status)) {
if (Objects.equals(CodeLogin.SUCCESS.getCode(), Integer.parseInt(status))) {
//根据userId获取登录token,走正常的登录逻辑返回给PC
//清除缓存
redisUtils.del(RedisKeyPrefix.CODE_LOGIN_PREFIX + ":" + token);
return token;
}
}
return null;
}
}
3.总结
1.实现一个功能有很多方法,找到适合自己项目的方法最重要。
2.其中还是有很堵哦技术细节需要完善,比如未授权就可以访问的获取验证码接口需要加一些校验提高接口的安全系数
3.流程中一定有哪里还有缺陷需要仔细考虑
4.有想法的同学可以一起讨论一下
5.点赞加关注
参考文档: