无需微信依赖,纯网页扫码登录实现方案解析及实战
无需微信依赖,纯网页扫码登录实现方案解析及实战
作为一名资深后端开发,你有没有遇到过这样的场景:产品经理跑过来说:"我们网站要支持扫码登录,要像微信一样方便!"但你又不想依赖微信的生态,想自己实现一套完整的扫码登录系统?
今天就来聊聊如何实现一套纯网页的扫码登录系统,不依赖任何第三方平台,让你的用户通过手机扫描网页上的二维码就能快速登录!
一、扫码登录的核心原理
扫码登录的本质是通过二维码作为信息载体,在网页端和手机端之间建立安全的身份验证通道。整个过程可以概括为以下几个步骤:
- 生成二维码:网页端请求服务器生成唯一的二维码
- 展示二维码:网页端展示二维码并轮询登录状态
- 扫描二维码:用户使用手机扫描二维码
- 确认登录:手机端确认登录请求
- 完成登录:网页端获取登录凭证并完成登录
Web端->服务器: 1. 请求生成二维码
服务器->Web端: 2. 返回二维码ID
Web端->Web端: 3. 展示二维码并轮询
手机端->服务器: 4. 扫描二维码
服务器->手机端: 5. 返回确认信息
手机端->服务器: 6. 确认登录
服务器->Web端: 7. 通知登录成功
Web端->Web端: 8. 完成登录跳转
二、技术架构设计
2.1 核心组件
一个完整的扫码登录系统需要以下核心组件:
- 二维码生成服务:负责生成和管理二维码
- 状态轮询服务:网页端轮询二维码状态
- 手机端API:处理手机扫描和确认请求
- 状态存储:存储二维码状态信息
- 安全认证:确保登录过程的安全性
2.2 状态管理设计
二维码在整个登录过程中会经历不同的状态:
public enum QrCodeStatus {
/**
* 未扫描状态
*/
UNSCANNED(0, "未扫描"),
/**
* 已扫描待确认状态
*/
SCANNED(1, "已扫描"),
/**
* 已确认登录状态
*/
CONFIRMED(2, "已确认"),
/**
* 已取消状态
*/
CANCELLED(3, "已取消"),
/**
* 已过期状态
*/
EXPIRED(4, "已过期");
private final int code;
private final String description;
QrCodeStatus(int code, String description) {
this.code = code;
this.description = description;
}
// getter方法...
}
三、后端实现详解
3.1 二维码实体类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class QrCodeInfo {
/**
* 二维码唯一标识
*/
private String qrCodeId;
/**
* 二维码状态
*/
private QrCodeStatus status;
/**
* 用户ID(确认登录后填充)
*/
private Long userId;
/**
* 用户信息(确认登录后填充)
*/
private UserInfo userInfo;
/**
* 创建时间
*/
private Long createTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 二维码内容
*/
private String content;
}
3.2 Redis存储设计
使用Redis来存储二维码状态信息,设置合理的过期时间:
@Service
@Slf4j
public class QrCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserService userService;
/**
* 二维码过期时间(单位:秒)
*/
private static final long QR_CODE_EXPIRE_TIME = 300; // 5分钟
/**
* 生成二维码
*/
public QrCodeInfo generateQrCode() {
// 生成唯一ID
String qrCodeId = UUID.randomUUID().toString().replace("-", "");
// 构造二维码内容(可以是JSON或其他格式)
Map<String, Object> contentMap = new HashMap<>();
contentMap.put("qrCodeId", qrCodeId);
contentMap.put("timestamp", System.currentTimeMillis());
String content = JSON.toJSONString(contentMap);
// 创建二维码信息
QrCodeInfo qrCodeInfo = QrCodeInfo.builder()
.qrCodeId(qrCodeId)
.status(QrCodeStatus.UNSCANNED)
.createTime(System.currentTimeMillis())
.expireTime(System.currentTimeMillis() + QR_CODE_EXPIRE_TIME * 1000)
.content(content)
.build();
// 存储到Redis
String key = "qrcode:" + qrCodeId;
redisTemplate.opsForValue().set(key, qrCodeInfo, QR_CODE_EXPIRE_TIME, TimeUnit.SECONDS);
log.info("生成二维码: {}", qrCodeId);
return qrCodeInfo;
}
/**
* 获取二维码状态
*/
public QrCodeInfo getQrCodeStatus(String qrCodeId) {
String key = "qrcode:" + qrCodeId;
QrCodeInfo qrCodeInfo = (QrCodeInfo) redisTemplate.opsForValue().get(key);
// 检查是否过期
if (qrCodeInfo != null && System.currentTimeMillis() > qrCodeInfo.getExpireTime()) {
qrCodeInfo.setStatus(QrCodeStatus.EXPIRED);
redisTemplate.opsForValue().set(key, qrCodeInfo, 10, TimeUnit.SECONDS); // 短暂存储过期状态
}
return qrCodeInfo;
}
/**
* 处理二维码扫描
*/
public boolean scanQrCode(String qrCodeId, Long userId) {
String key = "qrcode:" + qrCodeId;
QrCodeInfo qrCodeInfo = (QrCodeInfo) redisTemplate.opsForValue().get(key);
if (qrCodeInfo == null) {
return false;
}
// 检查状态是否允许扫描
if (qrCodeInfo.getStatus() != QrCodeStatus.UNSCANNED) {
return false;
}
// 更新状态为已扫描
qrCodeInfo.setStatus(QrCodeStatus.SCANNED);
qrCodeInfo.setUserId(userId);
// 获取用户信息
UserInfo userInfo = userService.getUserById(userId);
qrCodeInfo.setUserInfo(userInfo);
// 更新Redis
redisTemplate.opsForValue().set(key, qrCodeInfo,
(qrCodeInfo.getExpireTime() - System.currentTimeMillis()) / 1000, TimeUnit.SECONDS);
log.info("二维码已扫描: {}, 用户ID: {}", qrCodeId, userId);
return true;
}
/**
* 确认登录
*/
public boolean confirmLogin(String qrCodeId) {
String key = "qrcode:" + qrCodeId;
QrCodeInfo qrCodeInfo = (QrCodeInfo) redisTemplate.opsForValue().get(key);
if (qrCodeInfo == null) {
return false;
}
// 检查状态是否允许确认
if (qrCodeInfo.getStatus() != QrCodeStatus.SCANNED) {
return false;
}
// 更新状态为已确认
qrCodeInfo.setStatus(QrCodeStatus.CONFIRMED);
// 更新Redis
redisTemplate.opsForValue().set(key, qrCodeInfo,
(qrCodeInfo.getExpireTime() - System.currentTimeMillis()) / 1000, TimeUnit.SECONDS);
log.info("二维码登录已确认: {}", qrCodeId);
return true;
}
/**
* 取消登录
*/
public boolean cancelLogin(String qrCodeId) {
String key = "qrcode:" + qrCodeId;
QrCodeInfo qrCodeInfo = (QrCodeInfo) redisTemplate.opsForValue().get(key);
if (qrCodeInfo == null) {
return false;
}
// 更新状态为已取消
qrCodeInfo.setStatus(QrCodeStatus.CANCELLED);
// 更新Redis
redisTemplate.opsForValue().set(key, qrCodeInfo,
(qrCodeInfo.getExpireTime() - System.currentTimeMillis()) / 1000, TimeUnit.SECONDS);
log.info("二维码登录已取消: {}", qrCodeId);
return true;
}
}
3.3 控制器实现
@RestController
@RequestMapping("/api/qrcode")
@Api(tags = "扫码登录")
@Slf4j
public class QrCodeController {
@Autowired
private QrCodeService qrCodeService;
@Autowired
private TokenService tokenService;
/**
* 生成二维码
*/
@GetMapping("/generate")
@ApiOperation("生成登录二维码")
public Result<QrCodeResponse> generateQrCode() {
try {
QrCodeInfo qrCodeInfo = qrCodeService.generateQrCode();
QrCodeResponse response = QrCodeResponse.builder()
.qrCodeId(qrCodeInfo.getQrCodeId())
.content(qrCodeInfo.getContent())
.expireTime(qrCodeInfo.getExpireTime())
.build();
return Result.success(response);
} catch (Exception e) {
log.error("生成二维码失败", e);
return Result.error("生成二维码失败");
}
}
/**
* 轮询二维码状态
*/
@GetMapping("/status/{qrCodeId}")
@ApiOperation("获取二维码状态")
public Result<QrCodeStatusResponse> getQrCodeStatus(@PathVariable String qrCodeId) {
try {
QrCodeInfo qrCodeInfo = qrCodeService.getQrCodeStatus(qrCodeId);
if (qrCodeInfo == null) {
return Result.error("二维码不存在或已过期");
}
QrCodeStatusResponse response = QrCodeStatusResponse.builder()
.qrCodeId(qrCodeId)
.status(qrCodeInfo.getStatus().getCode())
.statusDesc(qrCodeInfo.getStatus().getDescription())
.userInfo(qrCodeInfo.getUserInfo())
.build();
// 如果是已确认状态,生成token
if (qrCodeInfo.getStatus() == QrCodeStatus.CONFIRMED) {
String token = tokenService.generateToken(qrCodeInfo.getUserId());
response.setToken(token);
}
return Result.success(response);
} catch (Exception e) {
log.error("获取二维码状态失败", e);
return Result.error("获取二维码状态失败");
}
}
/**
* 手机端扫描二维码
*/
@PostMapping("/scan")
@ApiOperation("扫描二维码")
public Result<String> scanQrCode(@RequestBody ScanQrCodeRequest request) {
try {
boolean success = qrCodeService.scanQrCode(request.getQrCodeId(), request.getUserId());
if (success) {
return Result.success("扫描成功");
} else {
return Result.error("扫描失败");
}
} catch (Exception e) {
log.error("扫描二维码失败", e);
return Result.error("扫描二维码失败");
}
}
/**
* 确认登录
*/
@PostMapping("/confirm")
@ApiOperation("确认登录")
public Result<LoginResponse> confirmLogin(@RequestBody ConfirmLoginRequest request) {
try {
boolean success = qrCodeService.confirmLogin(request.getQrCodeId());
if (success) {
// 获取二维码信息
QrCodeInfo qrCodeInfo = qrCodeService.getQrCodeStatus(request.getQrCodeId());
// 生成token
String token = tokenService.generateToken(qrCodeInfo.getUserId());
LoginResponse response = LoginResponse.builder()
.token(token)
.userInfo(qrCodeInfo.getUserInfo())
.build();
return Result.success(response);
} else {
return Result.error("确认登录失败");
}
} catch (Exception e) {
log.error("确认登录失败", e);
return Result.error("确认登录失败");
}
}
/**
* 取消登录
*/
@PostMapping("/cancel")
@ApiOperation("取消登录")
public Result<String> cancelLogin(@RequestBody CancelLoginRequest request) {
try {
boolean success = qrCodeService.cancelLogin(request.getQrCodeId());
if (success) {
return Result.success("取消成功");
} else {
return Result.error("取消失败");
}
} catch (Exception e) {
log.error("取消登录失败", e);
return Result.error("取消登录失败");
}
}
}
3.4 请求响应对象
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class QrCodeResponse {
private String qrCodeId;
private String content;
private Long expireTime;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class QrCodeStatusResponse {
private String qrCodeId;
private Integer status;
private String statusDesc;
private UserInfo userInfo;
private String token;
}
@Data
public class ScanQrCodeRequest {
private String qrCodeId;
private Long userId;
}
@Data
public class ConfirmLoginRequest {
private String qrCodeId;
}
@Data
public class CancelLoginRequest {
private String qrCodeId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse {
private String token;
private UserInfo userInfo;
}
四、前端实现详解
4.1 二维码生成和展示
// 扫码登录组件
export default {
data() {
return {
qrCodeId: '',
qrCodeContent: '',
qrCodeStatus: 0, // 0:未扫描 1:已扫描 2:已确认 3:已取消 4:已过期
userInfo: null,
timer: null,
countdown: 300 // 倒计时
}
},
mounted() {
this.generateQrCode();
},
beforeDestroy() {
this.clearTimer();
},
methods: {
// 生成二维码
async generateQrCode() {
try {
const response = await this.$http.get('/api/qrcode/generate');
if (response.data.code === 200) {
this.qrCodeId = response.data.data.qrCodeId;
this.qrCodeContent = response.data.data.content;
// 生成二维码图片
this.$nextTick(() => {
this.renderQrCode();
});
// 开始轮询状态
this.startPolling();
// 开始倒计时
this.startCountdown();
}
} catch (error) {
console.error('生成二维码失败', error);
}
},
// 渲染二维码
renderQrCode() {
const qrCodeElement = this.$refs.qrCode;
if (qrCodeElement) {
// 使用qrcode.js库生成二维码
new QRCode(qrCodeElement, {
text: this.qrCodeContent,
width: 200,
height: 200,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H
});
}
},
// 开始轮询状态
startPolling() {
this.clearTimer();
this.timer = setInterval(() => {
this.checkQrCodeStatus();
}, 1000); // 每秒轮询一次
},
// 检查二维码状态
async checkQrCodeStatus() {
try {
const response = await this.$http.get(`/api/qrcode/status/${this.qrCodeId}`);
if (response.data.code === 200) {
const status = response.data.data.status;
this.qrCodeStatus = status;
// 根据状态处理
switch (status) {
case 1: // 已扫描
this.userInfo = response.data.data.userInfo;
break;
case 2: // 已确认
this.handleLoginSuccess(response.data.data.token);
break;
case 3: // 已取消
this.handleLoginCancel();
break;
case 4: // 已过期
this.handleQrCodeExpired();
break;
}
}
} catch (error) {
console.error('检查二维码状态失败', error);
}
},
// 开始倒计时
startCountdown() {
const countdownTimer = setInterval(() => {
this.countdown--;
if (this.countdown <= 0) {
clearInterval(countdownTimer);
this.handleQrCodeExpired();
}
}, 1000);
},
// 处理登录成功
handleLoginSuccess(token) {
// 保存token
localStorage.setItem('token', token);
// 跳转到首页
this.$router.push('/dashboard');
// 清理定时器
this.clearTimer();
},
// 处理登录取消
handleLoginCancel() {
this.$message.warning('登录已取消');
this.clearTimer();
},
// 处理二维码过期
handleQrCodeExpired() {
this.$message.error('二维码已过期,请刷新重试');
this.clearTimer();
},
// 清理定时器
clearTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
// 刷新二维码
refreshQrCode() {
this.clearTimer();
this.generateQrCode();
}
}
}
4.2 Vue组件模板
<template>
<div class="qrcode-login">
<div class="login-container">
<div class="qrcode-section">
<h3>扫码登录</h3>
<div class="qrcode-wrapper">
<div v-if="qrCodeContent" ref="qrCode" class="qrcode-display"></div>
<div v-else class="qrcode-loading">二维码生成中...</div>
</div>
<div class="qrcode-status">
<div v-if="qrCodeStatus === 0" class="status-text">
<i class="el-icon-monitor"></i>
请使用手机扫描二维码
</div>
<div v-else-if="qrCodeStatus === 1" class="status-text scanned">
<i class="el-icon-check"></i>
扫描成功,请在手机上确认登录
<div v-if="userInfo" class="user-info">
<img :src="userInfo.avatar" class="user-avatar" />
<span>{{ userInfo.nickname }}</span>
</div>
</div>
<div v-else-if="qrCodeStatus === 2" class="status-text confirmed">
<i class="el-icon-success"></i>
登录成功,正在跳转...
</div>
<div v-else-if="qrCodeStatus === 3" class="status-text cancelled">
<i class="el-icon-circle-close"></i>
登录已取消
</div>
<div v-else-if="qrCodeStatus === 4" class="status-text expired">
<i class="el-icon-time"></i>
二维码已过期
</div>
</div>
<div class="countdown">
二维码有效期: {{ Math.floor(countdown / 60) }}:{{ (countdown % 60).toString().padStart(2, '0') }}
</div>
<div class="refresh-btn">
<el-button v-if="qrCodeStatus === 3 || qrCodeStatus === 4"
type="primary"
@click="refreshQrCode">
刷新二维码
</el-button>
</div>
</div>
<div class="instructions">
<h4>扫码登录说明</h4>
<ol>
<li>打开手机应用</li>
<li>点击右上角"扫一扫"</li>
<li>扫描屏幕上的二维码</li>
<li>在手机上确认登录</li>
</ol>
</div>
</div>
</div>
</template>
<script>
import QRCode from 'qrcodejs2'
export default {
// ... 上面的JavaScript代码
}
</script>
<style scoped>
.qrcode-login {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f5f5f5;
}
.login-container {
display: flex;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px;
}
.qrcode-section {
text-align: center;
margin-right: 40px;
}
.qrcode-wrapper {
margin: 20px 0;
}
.qrcode-display {
display: inline-block;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.qrcode-loading {
width: 200px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background: #f9f9f9;
border: 1px dashed #ddd;
}
.qrcode-status {
margin: 20px 0;
min-height: 80px;
}
.status-text {
font-size: 16px;
color: #666;
}
.status-text.scanned {
color: #409EFF;
}
.status-text.confirmed {
color: #67C23A;
}
.status-text.cancelled, .status-text.expired {
color: #F56C6C;
}
.user-info {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.user-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
}
.countdown {
color: #999;
font-size: 14px;
margin: 10px 0;
}
.instructions {
border-left: 1px solid #eee;
padding-left: 40px;
}
.instructions h4 {
margin-top: 0;
}
.instructions ol {
text-align: left;
padding-left: 20px;
}
.instructions li {
margin: 10px 0;
color: #666;
}
</style>
五、手机端实现
5.1 扫描二维码处理
@RestController
@RequestMapping("/api/mobile")
@Api(tags = "移动端接口")
@Slf4j
public class MobileController {
@Autowired
private QrCodeService qrCodeService;
@Autowired
private UserService userService;
/**
* 处理扫描到的二维码
*/
@PostMapping("/scan-qrcode")
@ApiOperation("处理扫描到的二维码")
public Result<ScanResult> handleScannedQrCode(@RequestBody ScanQrCodeRequest request) {
try {
// 验证用户身份(这里简化处理,实际应该有token验证)
Long userId = request.getUserId();
UserInfo userInfo = userService.getUserById(userId);
if (userInfo == null) {
return Result.error("用户不存在");
}
// 处理二维码扫描
boolean success = qrCodeService.scanQrCode(request.getQrCodeId(), userId);
if (success) {
ScanResult result = ScanResult.builder()
.qrCodeId(request.getQrCodeId())
.userInfo(userInfo)
.build();
return Result.success(result);
} else {
return Result.error("二维码处理失败");
}
} catch (Exception e) {
log.error("处理扫描二维码失败", e);
return Result.error("处理失败");
}
}
/**
* 确认登录
*/
@PostMapping("/confirm-login")
@ApiOperation("确认登录")
public Result<ConfirmResult> confirmLogin(@RequestBody ConfirmLoginRequest request) {
try {
// 验证用户身份
Long userId = request.getUserId();
UserInfo userInfo = userService.getUserById(userId);
if (userInfo == null) {
return Result.error("用户不存在");
}
// 确认登录
boolean success = qrCodeService.confirmLogin(request.getQrCodeId());
if (success) {
ConfirmResult result = ConfirmResult.builder()
.qrCodeId(request.getQrCodeId())
.success(true)
.message("登录确认成功")
.build();
return Result.success(result);
} else {
return Result.error("登录确认失败");
}
} catch (Exception e) {
log.error("确认登录失败", e);
return Result.error("确认失败");
}
}
/**
* 取消登录
*/
@PostMapping("/cancel-login")
@ApiOperation("取消登录")
public Result<CancelResult> cancelLogin(@RequestBody CancelLoginRequest request) {
try {
// 验证用户身份
Long userId = request.getUserId();
UserInfo userInfo = userService.getUserById(userId);
if (userInfo == null) {
return Result.error("用户不存在");
}
// 取消登录
boolean success = qrCodeService.cancelLogin(request.getQrCodeId());
if (success) {
CancelResult result = CancelResult.builder()
.qrCodeId(request.getQrCodeId())
.success(true)
.message("登录已取消")
.build();
return Result.success(result);
} else {
return Result.error("取消登录失败");
}
} catch (Exception e) {
log.error("取消登录失败", e);
return Result.error("取消失败");
}
}
}
六、安全考虑和最佳实践
6.1 二维码安全
@Service
public class SecureQrCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 生成安全的二维码内容
*/
public String generateSecureQrContent(String qrCodeId) {
Map<String, Object> contentMap = new HashMap<>();
contentMap.put("qrCodeId", qrCodeId);
contentMap.put("timestamp", System.currentTimeMillis());
contentMap.put("nonce", generateNonce()); // 添加随机数防止重放攻击
// 对内容进行签名
String content = JSON.toJSONString(contentMap);
String signature = generateSignature(content);
contentMap.put("signature", signature);
return JSON.toJSONString(contentMap);
}
/**
* 验证二维码内容的签名
*/
public boolean verifyQrContent(String content) {
try {
JSONObject jsonObject = JSON.parseObject(content);
String signature = jsonObject.getString("signature");
jsonObject.remove("signature");
String originalContent = jsonObject.toJSONString();
String expectedSignature = generateSignature(originalContent);
return signature.equals(expectedSignature);
} catch (Exception e) {
return false;
}
}
/**
* 生成签名
*/
private String generateSignature(String content) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec("your-secret-key".getBytes(), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(content.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException("生成签名失败", e);
}
}
/**
* 生成随机数
*/
private String generateNonce() {
return UUID.randomUUID().toString().replace("-", "");
}
}
6.2 防止重放攻击
@Service
public class ReplayAttackProtectionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String NONCE_PREFIX = "nonce:";
private static final long NONCE_EXPIRE_TIME = 300; // 5分钟
/**
* 验证并记录nonce
*/
public boolean validateAndRecordNonce(String nonce) {
String key = NONCE_PREFIX + nonce;
// 检查nonce是否已存在
if (redisTemplate.hasKey(key)) {
return false; // nonce已存在,可能是重放攻击
}
// 记录nonce
redisTemplate.opsForValue().set(key, "1", NONCE_EXPIRE_TIME, TimeUnit.SECONDS);
return true;
}
}
6.3 限流保护
@Component
public class RateLimitService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 限制同一IP的请求频率
*/
public boolean isAllowed(String ip, int maxRequests, int timeWindow) {
String key = "rate_limit:" + ip;
Long current = redisTemplate.boundValueOps(key).increment(1);
if (current == 1) {
redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS);
}
return current <= maxRequests;
}
}
七、性能优化
7.1 使用WebSocket替代轮询
@Component
@ServerEndpoint("/websocket/qrcode/{qrCodeId}")
@Slf4j
public class QrCodeWebSocket {
private static Map<String, Session> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("qrCodeId") String qrCodeId) {
sessions.put(qrCodeId, session);
log.info("WebSocket连接已建立: {}", qrCodeId);
}
@OnClose
public void onClose(@PathParam("qrCodeId") String qrCodeId) {
sessions.remove(qrCodeId);
log.info("WebSocket连接已关闭: {}", qrCodeId);
}
@OnMessage
public void onMessage(String message, Session session, @PathParam("qrCodeId") String qrCodeId) {
// 处理客户端消息
}
/**
* 推送状态更新
*/
public static void pushStatusUpdate(String qrCodeId, QrCodeStatusResponse response) {
Session session = sessions.get(qrCodeId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(JSON.toJSONString(response));
} catch (IOException e) {
log.error("推送状态更新失败", e);
}
}
}
}
7.2 Redis优化
@Service
public class OptimizedQrCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 使用Pipeline批量操作
*/
public void batchUpdateQrCodes(List<QrCodeInfo> qrCodeInfos) {
List<Object> results = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
for (QrCodeInfo qrCodeInfo : qrCodeInfos) {
String key = "qrcode:" + qrCodeInfo.getQrCodeId();
stringRedisConn.set(key, JSON.toJSONString(qrCodeInfo));
stringRedisConn.expire(key, QR_CODE_EXPIRE_TIME);
}
return null;
}
});
}
}
八、总结
实现一套纯网页的扫码登录系统,核心要点包括:
- 清晰的架构设计:合理划分前后端职责,使用Redis存储状态
- 安全考虑:签名验证、防重放攻击、限流保护
- 用户体验:合理的轮询频率、清晰的状态提示、倒计时机制
- 性能优化:WebSocket替代轮询、Redis Pipeline优化、合理的过期时间
这套方案相比依赖第三方平台的优势:
- 独立性:不依赖任何第三方服务
- 可控性:完全掌控登录流程和用户体验
- 安全性:可以根据业务需求定制安全策略
- 扩展性:易于扩展到多端登录场景
记住,技术选型要根据实际业务需求来决定。对于简单的应用场景,轮询方案就足够了;对于高并发场景,可以考虑WebSocket方案。
希望今天的分享能帮助你在下次面对扫码登录需求时,能够从容应对!
标题:无需微信依赖,纯网页扫码登录实现方案解析及实战
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304301056.html
0 评论