借助SpringBoot结合weixin-java-mp达成公众号扫码登录之法

引言

在现今的移动互联网时代,微信已然成为人们日常生活中不可或缺的一部分。利用微信公众号来实现第三方网站的扫码登录,不仅能够为用户提供便捷的体验,还能有效降低用户注册的门槛。本篇文章将会详细地介绍如何通过SpringBoot整合weixin - java - mp来实现公众号扫码登录功能,帮助开发者快速掌握这一技术。

借助SpringBoot结合weixin-java-mp达成公众号扫码登录之法

原理简介

不同于常见的OAuth2.0授权登录方式,本文所介绍的扫码登录流程是通过微信公众号的消息回调机制来实现的。其核心逻辑为:用户扫描网站上的临时二维码,当公众号接收到关注或者扫码事件后,会通过预先设置的回调接口将用户信息传递给网站服务器,从而完成身份验证和登录过程。

技术栈

  • SpringBoot 2.6.x

  • weixin - java - mp 4.4.0

  • Redis(用于存储临时登录凭证)

  • MySQL(用于存储用户信息)

  • Thymeleaf(前端模板引擎)

实现流程

  1. 用户访问登录页面,后端生成临时的登录标识并生成对应的二维码

  2. 用户通过微信扫描该二维码

  3. 微信公众号接收到扫码事件,将此事件通过回调接口传递给服务端

  4. 服务端对该事件进行处理,将用户信息与临时登录标识关联起来

  5. 公众号向用户发送登录成功的通知

  6. 前端页面通过轮询的方式检测登录状态,获取用户信息后完成登录

详细实现步骤

1. 公众号配置

首先,需要在微信公众平台完成基本配置:

  • 登录微信公众平台,进入“开发 -> 基本配置”

  • 配置服务器地址(URL),用于接收微信的消息通知

  • 设置Token和EncodingAESKey,用于消息的加解密

  • 开启“接收消息”和“网页授权”功能

2. 项目依赖配置

pom.xml中添加必要的依赖:

<!-- 微信Java SDK -->
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>4.4.0</version>
</dependency>

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

<!-- SpringBoot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Thymeleaf模板引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- 二维码生成 -->
<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>

3. 微信公众号配置类

创建微信公众号配置类,用于初始化SDK:

package com.example.wechatlogin.config;

import lombok.Data;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "wx.mp")
public class WxMpConfig {
    
    // 公众号appId
    private String appId;
    
    // 公众号appSecret
    private String secret;
    
    // 公众号token
    private String token;
    
    // 消息加解密密钥
    private String aesKey;
    
    /**
     * 初始化微信公众号服务
     * @return WxMpService实例
     */
    @Bean
    public WxMpService wxMpService() {
        WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
        config.setAppId(appId);
        config.setSecret(secret);
        config.setToken(token);
        config.setAesKey(aesKey);
        
        WxMpService service = new WxMpServiceImpl();
        service.setWxMpConfigStorage(config);
        return service;
    }
}

4. 配置application.yml

server:
  port: 8080
  
spring:
  redis:
    host: localhost
    port: 6379
  datasource:
    url: jdbc:mysql://localhost:3306/wx_login?useSSL=false&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  thymeleaf:
    cache: false
    
wx:
  mp:
    app-id: 你的公众号appId
    secret: 你的公众号secret
    token: 你设置的token
    aes-key: 你的EncodingAESKey

5. 二维码生成服务

package com.example.wechatlogin.service;

import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class QrCodeService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private WxMpService wxMpService;
    
    private static final long EXPIRE_TIME = 300; // 二维码有效期5分钟
    
    /**
     * 生成微信公众号临时二维码
     * @return 包含二维码链接和临时标识的对象
     */
    public QrCodeInfo generateLoginQrCode() {
        try {
            // 生成临时登录标识
            String loginId = UUID.randomUUID().toString();
            
            // 生成二维码场景值,用于公众号识别
            String sceneStr = "login_" + loginId;
            
            // 将登录标识存入Redis,设置过期时间
            redisTemplate.opsForValue().set("qrcode:login:" + loginId, "WAITING", EXPIRE_TIME, TimeUnit.SECONDS);
            
            // 调用微信接口生成带参数的临时二维码
            WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateTempTicket(sceneStr, (int)EXPIRE_TIME);
            
            // 获取二维码图片URL
            String qrcodeUrl = wxMpService.getQrcodeService().qrCodePictureUrl(ticket.getTicket());
            
            log.info("生成临时二维码 - loginId: {}, qrcodeUrl: {}", loginId, qrcodeUrl);
            
            QrCodeInfo qrCodeInfo = new QrCodeInfo();
            qrCodeInfo.setLoginId(loginId);
            qrCodeInfo.setQrCodeUrl(qrcodeUrl);
            qrCodeInfo.setExpireTime(EXPIRE_TIME);
            
            return qrCodeInfo;
        } catch (WxErrorException e) {
            log.error("生成微信二维码失败", e);
            throw new RuntimeException("生成微信二维码失败", e);
        }
    }
    
    /**
     * 二维码信息类
     */
    public static class QrCodeInfo {
        private String loginId;
        private String qrCodeUrl;
        private long expireTime;
        
        // getter和setter方法
        public String getLoginId() {
            return loginId;
        }
        
        public void setLoginId(String loginId) {
            this.loginId = loginId;
        }
        
        public String getQrCodeUrl() {
            return qrCodeUrl;
        }
        
        public void setQrCodeUrl(String qrCodeUrl) {
            this.qrCodeUrl = qrCodeUrl;
        }
        
        public long getExpireTime() {
            return expireTime;
        }
        
        public void setExpireTime(long expireTime) {
            this.expireTime = expireTime;
        }
    }
}

6. 微信消息处理器

package com.example.wechatlogin.handler;

import com.example.wechatlogin.service.UserService;
import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpMessageHandler;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class ScanQrCodeHandler implements WxMpMessageHandler {

    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private UserService userService;

    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
                                   WxMpService wxMpService, WxSessionManager sessionManager) {
        // 处理扫码事件
        if (wxMessage.getMsgType().equals(XmlMsgType.EVENT)) {
            // 如果是扫码事件
            if ("SCAN".equals(wxMessage.getEvent()) || "subscribe".equals(wxMessage.getEvent())) {
                return handleScanEvent(wxMessage, wxMpService);
            }
        }
        
        // 默认返回空消息
        return null;
    }
    
    /**
     * 处理扫码事件
     * @param wxMessage 微信消息
     * @param wxMpService 微信服务
     * @return 回复消息
     */
    private WxMpXmlOutMessage handleScanEvent(WxMpXmlMessage wxMessage, WxMpService wxMpService) {
        // 获取事件KEY值,也就是二维码参数
        String eventKey = wxMessage.getEventKey();
        
        // 根据不同类型事件处理
        if ("subscribe".equals(wxMessage.getEvent())) {
            // 如果是关注事件,需要处理qrscene_前缀
            if (eventKey != null && eventKey.startsWith("qrscene_")) {
                eventKey = eventKey.substring(8);
            } else {
                // 普通关注,不是扫码关注
                return WxMpXmlOutMessage.TEXT().content("感谢关注!").fromUser(wxMessage.getToUser())
                        .toUser(wxMessage.getFromUser()).build();
            }
        }
        
        // 处理登录场景
        if (eventKey != null && eventKey.startsWith("login_")) {
            String loginId = eventKey.substring(6);
            String openId = wxMessage.getFromUser();
            
            // 处理登录逻辑
            handleLogin(loginId, openId);
            
            // 回复消息通知用户登录成功
            return WxMpXmlOutMessage.TEXT().content("登录成功,请返回网页继续操作!")
                    .fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser()).build();
        }
        
        return null;
    }
    
    /**
     * 处理登录逻辑
     * @param loginId 登录标识
     * @param openId 用户openId
     */
    private void handleLogin(String loginId, String openId) {
        // 检查登录标识是否存在
        String key = "qrcode:login:" + loginId;
        Boolean exists = redisTemplate.hasKey(key);
        
        if (exists != null && exists) {
            // 获取用户信息并存储登录状态
            userService.saveOrUpdateUserByOpenId(openId);
            
            // 更新Redis中的登录状态
            redisTemplate.opsForValue().set(key, openId, 60, TimeUnit.SECONDS);
        }
    }
}

7. 微信消息路由配置

package com.example.wechatlogin.config;

import com.example.wechatlogin.handler.ScanQrCodeHandler;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WxMpMessageRouterConfig {

    @Autowired
    private ScanQrCodeHandler scanQrCodeHandler;
    
    @Bean
    public WxMpMessageRouter wxMpMessageRouter(WxMpService wxMpService) {
        final WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
        
        // 注册扫码事件处理器
        router.rule()
            .async(false)
            .msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.EventType.SCAN)
            .handler(scanQrCodeHandler)
            .end();
            
        // 注册关注事件处理器
        router.rule()
            .async(false)
            .msgType(WxConsts.XmlMsgType.EVENT)
            .event(WxConsts.EventType.SUBSCRIBE)
            .handler(scanQrCodeHandler)
            .end();
            
        return router;
    }
}

8. 微信接口控制器

```java
package com.example.wechatlogin.controller;

import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.Wx

相关文章

暂无评论

暂无评论...