本文目录导读:

我来详细介绍二维码扫码登录的实现方案,包含完整的Java实现代码。
扫码登录基本原理
核心流程
sequenceDiagram
participant Web端
participant 服务器
participant 手机端
Web端->>服务器: 1. 请求生成二维码
服务器->>服务器: 2. 生成唯一码(如UUID)
服务器->>Web端: 3. 返回二维码图片
Web端->>Web端: 4. 显示二维码
手机端->>服务器: 5. 扫描二维码
手机端->>服务器: 6. 确认登录
服务器->>Web端: 7. 轮询/推送登录状态
Web端->>Web端: 8. 登录成功,跳转
状态流转
待扫描 -> 已扫描 -> 已确认 -> 登录成功
↓ ↓ ↓
未登录 等待确认 登录成功
完整实现代码
Maven依赖
<!-- 二维码生成 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
核心实体类
@Data
public class QRCodeLoginDTO {
private String qrCodeId; // 二维码唯一标识
private String ticket; // 临时凭证
private Integer status; // 状态:0-待扫描,1-已扫描,2-已确认
private String userId; // 用户ID
private Long expireTime; // 过期时间
}
public enum QRCodeStatus {
WAITING(0, "待扫描"),
SCANNED(1, "已扫描"),
CONFIRMED(2, "已确认"),
EXPIRED(3, "已过期");
private int code;
private String desc;
// getter方法...
}
二维码生成服务
@Service
@Slf4j
public class QRCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${qr.code.expire:300}")
private long expireSeconds; // 默认5分钟
@Value("${qr.code.base-url}")
private String baseUrl;
/**
* 生成二维码
*/
public String generateQRCode() throws Exception {
// 1. 生成唯一ID
String qrCodeId = UUID.randomUUID().toString().replace("-", "");
// 2. 生成临时凭证(防篡改)
String ticket = generateTicket(qrCodeId);
// 3. 构建二维码内容(URL格式:包含ID和凭证)
String content = String.format("%s/qr/login?qrCodeId=%s&ticket=%s",
baseUrl, qrCodeId, ticket);
// 4. 生成二维码图片
String image = generateQRCodeImage(content);
// 5. 保存登录信息到Redis
QRCodeLoginDTO loginDTO = new QRCodeLoginDTO();
loginDTO.setQrCodeId(qrCodeId);
loginDTO.setTicket(ticket);
loginDTO.setStatus(QRCodeStatus.WAITING.getCode());
loginDTO.setExpireTime(System.currentTimeMillis() + expireSeconds * 1000);
redisTemplate.opsForValue().set(
"qr:login:" + qrCodeId,
loginDTO,
expireSeconds,
TimeUnit.SECONDS
);
return image;
}
/**
* 生成二维码图片(Base64)
*/
private String generateQRCodeImage(String content) throws Exception {
int width = 300;
int height = 300;
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new QRCodeWriter().encode(
content,
BarcodeFormat.QR_CODE,
width,
height,
hints
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", out);
return Base64.getEncoder().encodeToString(out.toByteArray());
}
/**
* 生成临时凭证
*/
private String generateTicket(String qrCodeId) {
String raw = qrCodeId + System.currentTimeMillis() + "secret_key";
return DigestUtils.md5DigestAsHex(raw.getBytes());
}
}
扫码登录控制器
@RestController
@RequestMapping("/api/qr")
@Slf4j
public class QRCodeController {
@Autowired
private QRCodeService qrCodeService;
@Autowired
private LoginService loginService;
@Autowired
private WebSocketService webSocketService;
/**
* 获取二维码
*/
@GetMapping("/get")
public Result<Map<String, Object>> getQRCode() {
try {
String base64Image = qrCodeService.generateQRCode();
Map<String, Object> result = new HashMap<>();
result.put("qrImage", "data:image/png;base64," + base64Image);
result.put("expireTime", 300); // 过期时间(秒)
return Result.success(result);
} catch (Exception e) {
log.error("生成二维码失败", e);
return Result.error("生成二维码失败");
}
}
/**
* 手机端扫码
*/
@PostMapping("/scan")
public Result<Boolean> scanQRCode(@RequestBody QRCodeScanDTO scanDTO) {
// 1. 验证二维码是否有效
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(scanDTO.getQrCodeId());
if (loginInfo == null) {
return Result.error("二维码已过期");
}
// 2. 验证凭证
if (!loginInfo.getTicket().equals(scanDTO.getTicket())) {
return Result.error("非法请求");
}
// 3. 更新状态为已扫描
loginInfo.setStatus(QRCodeStatus.SCANNED.getCode());
qrCodeService.updateLoginInfo(loginInfo);
// 4. 通过WebSocket通知网页端
webSocketService.sendMessage(scanDTO.getQrCodeId(),
new QRCodeMessage(QRCodeStatus.SCANNED.getCode(), "已扫描"));
return Result.success(true);
}
/**
* 手机端确认登录
*/
@PostMapping("/confirm")
public Result<String> confirmLogin(@RequestBody QRCodeConfirmDTO confirmDTO) {
// 1. 验证二维码信息
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(confirmDTO.getQrCodeId());
if (loginInfo == null || loginInfo.getStatus() != QRCodeStatus.SCANNED.getCode()) {
return Result.error("请先扫码");
}
// 2. 用户认证
String userId = loginService.authenticate(confirmDTO.getToken());
if (userId == null) {
return Result.error("用户认证失败");
}
// 3. 生成临时登录凭证
String tempToken = UUID.randomUUID().toString();
loginInfo.setUserId(userId);
loginInfo.setStatus(QRCodeStatus.CONFIRMED.getCode());
qrCodeService.updateLoginInfo(loginInfo);
// 4. 保存临时token到Redis(供网页端使用)
redisTemplate.opsForValue().set(
"qr:token:" + confirmDTO.getQrCodeId(),
tempToken,
60, // 1分钟有效
TimeUnit.SECONDS
);
// 5. 通知网页端登录成功
webSocketService.sendMessage(confirmDTO.getQrCodeId(),
new QRCodeMessage(QRCodeStatus.CONFIRMED.getCode(), "登录成功"));
return Result.success(tempToken);
}
/**
* 网页端轮询检查登录状态
*/
@GetMapping("/check/{qrCodeId}")
public Result<QRCodeCheckResult> checkLoginStatus(@PathVariable String qrCodeId) {
// 1. 检查临时token
String tempToken = (String) redisTemplate.opsForValue().get("qr:token:" + qrCodeId);
if (tempToken != null) {
// 2. 获取登录信息
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(qrCodeId);
if (loginInfo != null && loginInfo.getStatus() == QRCodeStatus.CONFIRMED.getCode()) {
// 3. 生成正式token
String accessToken = loginService.generateToken(loginInfo.getUserId());
// 4. 清理临时数据
redisTemplate.delete("qr:token:" + qrCodeId);
redisTemplate.delete("qr:login:" + qrCodeId);
return Result.success(QRCodeCheckResult.builder()
.status(QRCodeStatus.CONFIRMED.getCode())
.accessToken(accessToken)
.build());
}
}
// 5. 返回当前状态
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(qrCodeId);
if (loginInfo == null) {
return Result.success(QRCodeCheckResult.builder()
.status(QRCodeStatus.EXPIRED.getCode())
.build());
}
return Result.success(QRCodeCheckResult.builder()
.status(loginInfo.getStatus())
.build());
}
}
WebSocket推送服务
@Component
@Slf4j
public class WebSocketService {
private final Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
/**
* 发送消息
*/
public void sendMessage(String qrCodeId, QRCodeMessage message) {
WebSocketSession session = sessionMap.get(qrCodeId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(JSON.toJSONString(message)));
} catch (IOException e) {
log.error("WebSocket发送消息失败", e);
}
}
}
/**
* 注册session
*/
public void register(String qrCodeId, WebSocketSession session) {
sessionMap.put(qrCodeId, session);
}
/**
* 移除session
*/
public void remove(String qrCodeId) {
sessionMap.remove(qrCodeId);
}
}
WebSocket处理器
@Component
public class QRCodeWebSocketHandler extends TextWebSocketHandler {
@Autowired
private WebSocketService webSocketService;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String qrCodeId = getQRCodeId(session);
if (qrCodeId != null) {
webSocketService.register(qrCodeId, session);
log.info("WebSocket连接建立: {}", qrCodeId);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String qrCodeId = getQRCodeId(session);
if (qrCodeId != null) {
webSocketService.remove(qrCodeId);
log.info("WebSocket连接关闭: {}", qrCodeId);
}
}
private String getQRCodeId(WebSocketSession session) {
// 从URL参数中获取qrCodeId
URI uri = session.getUri();
if (uri != null) {
String query = uri.getQuery();
if (query != null) {
String[] params = query.split("&");
for (String param : params) {
String[] kv = param.split("=");
if (kv.length == 2 && "qrCodeId".equals(kv[0])) {
return kv[1];
}
}
}
}
return null;
}
}
WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private QRCodeWebSocketHandler qrCodeWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(qrCodeWebSocketHandler, "/ws/qr")
.setAllowedOrigins("*");
}
}
前端HTML实现
<!DOCTYPE html>
<html>
<head>扫码登录</title>
</head>
<body>
<div id="qr-container">
<img id="qr-image" />
<div id="qr-status">等待扫描</div>
</div>
<script>
let qrCodeId;
let websocket;
// 1. 获取二维码
async function getQRCode() {
const response = await fetch('/api/qr/get');
const result = await response.json();
if (result.success) {
// 显示二维码
document.getElementById('qr-image').src = result.data.qrImage;
qrCodeId = result.data.qrCodeId;
// 建立WebSocket连接
connectWebSocket(qrCodeId);
// 开始轮询
startPolling(qrCodeId);
}
}
// 2. 建立WebSocket连接
function connectWebSocket(qrCodeId) {
const ws = new WebSocket(`ws://localhost:8080/ws/qr?qrCodeId=${qrCodeId}`);
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
handleQRCodeStatus(data);
};
ws.onclose = function() {
console.log('WebSocket连接关闭');
};
}
// 3. 处理状态变化
function handleQRCodeStatus(data) {
const statusMap = {
0: '等待扫描',
1: '已扫描,请在手机上确认',
2: '登录成功,正在跳转...'
};
document.getElementById('qr-status').textContent = statusMap[data.status] || '未知状态';
if (data.status === 2) {
// 登录成功,跳转首页
setTimeout(() => {
window.location.href = '/index';
}, 1000);
}
}
// 4. 轮询检查状态(作为WebSocket的补充)
function startPolling(qrCodeId) {
setInterval(async () => {
const response = await fetch(`/api/qr/check/${qrCodeId}`);
const result = await response.json();
if (result.success && result.data.status === 2) {
// 登录成功
localStorage.setItem('access_token', result.data.accessToken);
window.location.href = '/index';
}
}, 3000); // 每3秒轮询一次
}
// 初始化
getQRCode();
</script>
</body>
</html>
关键优化建议
安全性优化
// 添加防重复扫码
public synchronized boolean processScan(String qrCodeId, String userId) {
// 加锁防止并发
boolean locked = redisTemplate.opsForValue()
.setIfAbsent("qr:lock:" + qrCodeId, userId, 5, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("二维码正在被扫描");
}
try {
// 业务处理
} finally {
redisTemplate.delete("qr:lock:" + qrCodeId);
}
}
性能优化
// 使用连接池
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
// 配置连接池
return factory;
}
// 异步处理
@Async
public CompletableFuture<Boolean> asyncProcessLogin(QRCodeLoginDTO loginDTO) {
// 异步处理登录
return CompletableFuture.completedFuture(true);
}
异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(QRCodeExpiredException.class)
public Result handleQRCodeExpired() {
return Result.error("二维码已过期,请刷新");
}
@ExceptionHandler(QRCodeInvalidException.class)
public Result handleQRCodeInvalid() {
return Result.error("非法二维码");
}
}
部署配置
application.yml
qr:
code:
expire: 300 # 二维码过期时间(秒)
base-url: http://your-domain.com
secret-key: your-secret-key
redis:
host: localhost
port: 6379
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1
这个实现包含了完整的扫码登录流程,支持WebSocket实时推送和HTTP轮询两种模式,并考虑了安全性和性能优化。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播