• 手机扫码登录的原理是怎样的?
  • 发布于 1天前
  • 19 热度
    0 评论
一.前言
在移动互联网时代,扫码登录以其便捷性和安全性,成为众多应用首选的登录方式。

技术原理
扫码登录的核心流程基于WebSocket、Redis等技术,主要包含以下几个关键步骤:
1.生成二维码:用户点击扫码登录后,后端生成一个唯一的UUID作为标识,将该标识与用户设备信息等关联,存储到Redis中,并生成包含此UUID的二维码返回给前端展示。
2.前端轮询或WebSocket监听:前端通过轮询接口或使用WebSocket长连接,不断向后端查询二维码对应的登录状态。
3.扫码与授权:用户使用手机端应用扫描二维码,手机端应用携带扫码信息请求后端,后端验证扫码信息并标记二维码为已扫描状态,同时返回授权页面或直接进行授权操作。

4.完成登录:授权通过后,后端更新Redis中二维码对应的登录状态为已登录,前端监听到登录状态变化后,完成登录流程,跳转至相应页面。


二实现
演示效果
1.轮询方式

2.websocket

引入依赖
<!-- Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- WebSocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- JSON -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

<!-- ZXing for QR Code generation -->
<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>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
配置
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
  session:
    store-type: redis
生成二维码
/**
 * 堆代码 duidaima.com
 * 二维码生成工具类
 */
public class QRCodeUtil {
    
    private static final int WIDTH = 300;
    private static final int HEIGHT = 300;
    private static final String FORMAT = "png";
    
    /**
     * 生成二维码字节数组
     * @param content 二维码内容
     * @return 二维码图片字节数组
     */
    public static byte[] generateQRCode(String content) throws WriterException, IOException {
        Map<EncodeHintType, Object> hints = new HashMap<>();
        // 设置字符编码
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        // 设置容错级别
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        // 设置边距
        hints.put(EncodeHintType.MARGIN, 1);
        
        BitMatrix bitMatrix = new MultiFormatWriter().encode(
                content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, hints);
        
        BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
        
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image, FORMAT, outputStream);
        
        return outputStream.toByteArray();
    }
    
    /**
     * 生成二维码Base64字符串
     */
    public static String generateQRCodeBase64(String content) throws WriterException, IOException {
        byte[] bytes = generateQRCode(content);
        return "data:image/png;base64," + java.util.Base64.getEncoder().encodeToString(bytes);
    }
}
定义常量和实体类
/**
 * 常量类
 */
public class Constants {
    // Redis 中二维码状态的前缀
    public static final String QR_CODE_PREFIX = "qr:code:";
    // 二维码过期时间(秒)
    public static final long QR_CODE_EXPIRE = 5 * 60;
}

/**
 * 二维码状态枚举
 */
public enum QRCodeStatus {
    WAITING("waiting", "等待扫描"),
    SCANNED("scanned", "已扫描"),
    CONFIRMED("confirmed", "已确认"),
    CANCELLED("cancelled", "已取消"),
    EXPIRED("expired", "已过期"),
    ERROR("error", "错误");
    
    private String code;
    private String message;
    
    QRCodeStatus(String code, String message) {
        this.code = code;
        this.message = message;
    }
    
    // getter 方法
    public String getCode() {
        return code;
    }
    
    public String getMessage() {
        return message;
    }
}

/**
 * WebSocket消息实体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WebSocketMessage {
    private String uuid;
    private QRCodeStatus status;
    private String message;
    private Object data;
}

/**
 * 用户信息实体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
    private Long userId;
    private String username;
    private String nickname;
    private String avatar;
}
配置WebSocket
/**
 * WebSocket配置
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用简单消息代理,前缀为/topic的消息会被代理转发到订阅了相应主题的客户端
        config.enableSimpleBroker("/topic");
        // 客户端发送消息的前缀
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册STOMP端点,客户端通过此端点连接到WebSocket服务器
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
    
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
}
二维码服务实现
/**
 * 二维码服务
 */
@Service
public class QRCodeService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 创建新的二维码
     * @return 二维码UUID
     */
    public String createQRCode() {
        String uuid = java.util.UUID.randomUUID().toString();
        String redisKey = Constants.QR_CODE_PREFIX + uuid;

        // 存储二维码状态到Redis
        redisTemplate.opsForHash().put(redisKey, "status", QRCodeStatus.WAITING.getCode());
        redisTemplate.expire(redisKey, Constants.QR_CODE_EXPIRE, TimeUnit.SECONDS);
        return uuid;
    }

    /**
     * 更新二维码状态
     * @param uuid 二维码UUID
     * @param status 新状态
     */
    public void updateQRCodeStatus(String uuid, QRCodeStatus status) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        redisTemplate.opsForHash().put(redisKey, "status", status.getCode());
        redisTemplate.expire(redisKey, getRemainingTime(uuid), TimeUnit.SECONDS);
    }

    /**
     * 更新二维码状态并关联用户信息
     * @param uuid 二维码UUID
     * @param status 新状态
     * @param userInfo 用户信息
     */
    public void updateQRCodeStatusWithUser(String uuid, QRCodeStatus status, UserInfo userInfo) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        // 使用Hash结构存储状态和用户信息
        redisTemplate.opsForHash().put(redisKey, "status", status.getCode());
        redisTemplate.opsForHash().put(redisKey, "userInfo", userInfo);
        redisTemplate.expire(redisKey, getRemainingTime(uuid), TimeUnit.SECONDS);
    }

    /**
     * 获取二维码状态
     * @param uuid 二维码UUID
     * @return 状态
     */
    public QRCodeStatus getQRCodeStatus(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        Object status = redisTemplate.opsForHash().get(redisKey, "status");

        if (status == null) {
            return QRCodeStatus.EXPIRED;
        }

        for (QRCodeStatus qrCodeStatus : QRCodeStatus.values()) {
            if (qrCodeStatus.getCode().equals(status.toString())) {
                return qrCodeStatus;
            }
        }

        return QRCodeStatus.ERROR;
    }

    /**
     * 获取二维码关联的用户信息
     * @param uuid 二维码UUID
     * @return 用户信息
     */
    public UserInfo getUserInfo(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        return (UserInfo) redisTemplate.opsForHash().get(redisKey, "userInfo");
    }

    /**
     * 获取Redis键的剩余时间
     * @param uuid 二维码UUID
     * @return 剩余时间(秒)
     */
    private Long getRemainingTime(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        return redisTemplate.getExpire(redisKey, TimeUnit.SECONDS);
    }

    /**
     * 使二维码过期
     * @param uuid 二维码UUID
     */
    public void expireQRCode(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        redisTemplate.delete(redisKey);
    }
}
后端控制器实现
/**
 * 登录控制器
 */
@RestController
@RequestMapping("/api/login")
public class LoginController {

    @Autowired
    private QRCodeService qrCodeService;

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /**
     * 生成二维码
     */
    @GetMapping("/qrCode")
    public Map<String, Object> generateQRCode() throws Exception {
        String uuid = qrCodeService.createQRCode();
        String qrCodeBase64 = QRCodeUtil.generateQRCodeBase64(uuid);

        Map<String, Object> result = new HashMap<>();
        result.put("uuid", uuid);
        result.put("qrCode", qrCodeBase64);
        result.put("expireTime", Constants.QR_CODE_EXPIRE);

        return result;
    }

    /**
     * 处理扫码请求
     */
    @PostMapping("/scan")
    public Map<String, Object> scanQRCode(@RequestBody Map<String, String> request) {

        String uuid = request.get("uuid");
        Long userId = Long.parseLong(request.get("userId"));
        Map<String, Object> result = new HashMap<>();

        // 检查二维码是否存在且有效
        QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
        if (status != QRCodeStatus.WAITING) {
            result.put("success", false);
            result.put("message", "二维码无效或已过期");
            return result;
        }

        // 获取用户信息(这里应该从数据库查询,简化示例)
        UserInfo userInfo = new UserInfo(userId, "一安", "一安未来", "https://picsum.photos/200/200");

        // 更新二维码状态为已扫描
        qrCodeService.updateQRCodeStatusWithUser(uuid, QRCodeStatus.SCANNED, userInfo);

        // 通过WebSocket通知前端二维码已被扫描
        WebSocketMessage message = new WebSocketMessage(
                uuid,
                QRCodeStatus.SCANNED,
                "二维码已被扫描,请确认登录",
                userInfo
        );
        messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);

        result.put("success", true);
        result.put("message", "扫码成功,请在PC端确认登录");
        return result;
    }

    /**
     * 处理授权请求
     */
    @PostMapping("/authorize")
    public Map<String, Object> authorize(@RequestBody Map<String, String> request) {


        String uuid = request.get("uuid");
        Boolean confirm = Boolean.parseBoolean(request.get("confirm"));
        Map<String, Object> result = new HashMap<>();

        // 检查二维码是否存在且已扫描
        QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
        if (status != QRCodeStatus.SCANNED) {
            result.put("success", false);
            result.put("message", "二维码状态无效");
            return result;
        }

        if (confirm) {
            // 获取用户信息
            UserInfo userInfo = qrCodeService.getUserInfo(uuid);

            // 更新二维码状态为已确认
            qrCodeService.updateQRCodeStatusWithUser(uuid, QRCodeStatus.CONFIRMED, userInfo);

            // 通过WebSocket通知前端登录成功
            WebSocketMessage message = new WebSocketMessage(
                    uuid,
                    QRCodeStatus.CONFIRMED,
                    "登录成功",
                    userInfo
            );
            messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);

            result.put("success", true);
            result.put("message", "授权成功");
            result.put("userInfo", userInfo);
        } else {
            // 更新二维码状态为已取消
            qrCodeService.updateQRCodeStatus(uuid, QRCodeStatus.ERROR);

            // 通过WebSocket通知前端登录已取消
            WebSocketMessage message = new WebSocketMessage(
                    uuid,
                    QRCodeStatus.ERROR,
                    "用户取消登录",
                    null
            );
            messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);

            result.put("success", false);
            result.put("message", "用户取消登录");
        }

        return result;
    }

    /**
     * 检查二维码状态(轮询方式)
     */
    @GetMapping("/checkStatus")
    public Map<String, Object> checkStatus(@RequestParam String uuid) {
        Map<String, Object> result = new HashMap<>();

        QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
        result.put("status", status.getCode());
        result.put("message", status.getMessage());

        if (status == QRCodeStatus.SCANNED) {
            // 获取用户信息
            UserInfo userInfo = qrCodeService.getUserInfo(uuid);
            result.put("userInfo", userInfo);
        }

        return result;
    }
}
WebSocket消息处理器
/**
 * WebSocket消息处理器
 */
@Controller
public class WebSocketController {
    
    /**
     * 处理客户端订阅二维码状态的消息
     */
    @MessageMapping("/subscribeQr")
    @SendTo("/topic/qr/{uuid}")
    public WebSocketMessage subscribeQr(@PathVariable String uuid) {
        // 这里可以根据需要返回初始状态
        return new WebSocketMessage(
                uuid, 
                QRCodeStatus.WAITING, 
                "等待扫描", 
                null
        );
    }
}
三.总结
二维码生成阶段
1.用户打开Web登录页面
2.前端请求后端生成唯一的二维码ID
3.后端生成二维码ID,初始状态为等待扫描
4.后端将二维码ID及状态存储到Redis
5.后端生成包含二维码ID的二维码图片并返回给前端

6.前端建立WebSocket连接,准备接收状态更新(轮询方式不需要)


扫描确认阶段
1.用户通过移动端App扫描二维码,获取二维码ID
2.移动端发送扫描请求到服务端
3.服务端更新二维码状态为已扫描
4.服务端通过WebSocket推送状态变更到Web端(轮询方式不需要)
5.Web端更新UI显示已扫描状态
6.移动端显示用户选择界面

7.用户在移动端选择要登录的账号并确认


登录完成阶段
1.移动端发送确认登录请求到服务端
2.服务端验证二维码状态,生成用户令牌
3.服务端更新二维码状态为已确认,并附带用户信息
4.服务端通过WebSocket推送登录成功信息到Web端(轮询方式不需要)
5.Web端接收到登录成功消息,获取用户信息
6.Web端完成登录流程,显示用户信息

7.移动端显示登录成功界面


功能优化与扩展
安全增强
1.数据加密:对二维码内容和传输的数据进行加密处理,防止信息泄露。
2.防重放攻击:为每个请求添加时间戳和签名,防止请求被截获后重放。
3.访问控制:限制对Redis数据的访问权限,只允许授权的请求操作相关数据。
4.IP限制:对频繁请求的IP进行限制,防止恶意攻击。
多设备支持
1.设备管理:记录和管理用户登录的设备信息,支持查看和管理已登录设备。
2.异地登录提醒:当检测到用户在异地登录时,发送提醒通知。
3.单点登录:实现同一账号在不同设备上的单点登录功能。
用户评论