SpringBoot集成weixin-java-mp实现公众号扫码登录
不同于常见的OAuth2.0授权登录方式,本文介绍的是通过微信公众号的消息回调机制实现的扫码登录流程。其核心逻辑是:用户扫描网站上的临时二维码,公众号接收到关注或扫码事件后,通过预先配置的回调接口将用户信息传递给网站服务器,从而完成身份验证和登录过程。本文详细介绍了如何通过SpringBoot集成weixin-java-mp实现微信公众号扫码登录功能。与传统的OAuth2.0授权方式不同,这种实现
引言
在当今移动互联网时代,微信已成为人们日常生活中不可或缺的一部分。利用微信公众号实现第三方网站的扫码登录,不仅能提供便捷的用户体验,还能有效减少用户注册的门槛。本文将详细介绍如何通过SpringBoot集成weixin-java-mp实现公众号扫码登录功能,帮助开发者快速掌握这一技术。

原理简介
不同于常见的OAuth2.0授权登录方式,本文介绍的是通过微信公众号的消息回调机制实现的扫码登录流程。其核心逻辑是:用户扫描网站上的临时二维码,公众号接收到关注或扫码事件后,通过预先配置的回调接口将用户信息传递给网站服务器,从而完成身份验证和登录过程。
技术栈
-
SpringBoot 2.6.x
-
weixin-java-mp 4.4.0
-
Redis (用于存储临时登录凭证)
-
MySQL (用户信息存储)
-
Thymeleaf (前端模板引擎)
实现流程
-
用户访问登录页面,后端生成临时登录标识并生成对应二维码
-
用户通过微信扫描二维码
-
微信公众号接收到扫码事件,将事件通过回调接口传递给服务端
-
服务端处理事件,将用户信息与临时登录标识关联
-
公众号向用户发送登录成功通知
-
前端页面通过轮询检测登录状态,获取用户信息并完成登录
详细实现步骤
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. 微信接口控制器
package com.example.wechatlogin.controller;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
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.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@RestController
@RequestMapping("/wx/portal")
public class WxPortalController {
@Autowired
private WxMpService wxMpService;
@Autowired
private WxMpMessageRouter wxMpMessageRouter;
/**
* 处理微信服务器发来的验证请求
*/
@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(HttpServletRequest request) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
log.info("收到微信验证请求 - signature: {}, timestamp: {}, nonce: {}, echostr: {}",
signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
log.error("请求参数不完整");
return "请求参数不完整";
}
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
log.info("微信验证成功");
return echostr;
}
log.error("微信验证失败");
return "非法请求";
}
/**
* 处理微信服务器发来的消息
*/
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(HttpServletRequest request, HttpServletResponse response) throws IOException {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
log.info("收到微信消息 - signature: {}, timestamp: {}, nonce: {}", signature, timestamp, nonce);
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
log.error("非法请求,可能属于伪造的请求");
return "非法请求";
}
// 解析XML消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(request.getInputStream());
log.info("收到微信消息:{}", inMessage.toString());
// 路由消息并获取响应
WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
// 有响应消息则返回
return outMessage == null ? "" : outMessage.toXml();
}
}
9. 登录控制器
package com.example.wechatlogin.controller;
import com.example.wechatlogin.service.QrCodeService;
import com.example.wechatlogin.service.QrCodeService.QrCodeInfo;
import com.example.wechatlogin.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/login")
public class LoginController {
@Autowired
private QrCodeService qrCodeService;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserService userService;
/**
* 显示登录页面
*/
@GetMapping
public String loginPage(Model model) {
// 生成公众号二维码
QrCodeInfo qrCodeInfo = qrCodeService.generateLoginQrCode();
// 将二维码信息传递给页面
model.addAttribute("loginId", qrCodeInfo.getLoginId());
model.addAttribute("qrCodeUrl", qrCodeInfo.getQrCodeUrl()); // 使用公众号返回的二维码URL
model.addAttribute("expireTime", qrCodeInfo.getExpireTime());
return "login";
}
/**
* 检查登录状态
*/
@GetMapping("/check")
@ResponseBody
public Map<String, Object> checkLoginStatus(@RequestParam String loginId, HttpSession session) {
Map<String, Object> result = new HashMap<>();
// 从Redis获取登录状态
String key = "qrcode:login:" + loginId;
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 二维码已过期
result.put("success", false);
result.put("status", "expired");
result.put("message", "二维码已过期,请刷新页面");
} else if ("WAITING".equals(value)) {
// 等待扫码
result.put("success", false);
result.put("status", "waiting");
result.put("message", "等待扫码");
} else {
// 已登录,获取用户信息
result.put("success", true);
result.put("status", "logged");
result.put("message", "登录成功");
// 获取用户信息
Map<String, Object> userInfo = userService.getUserInfoByOpenId(value);
result.put("userInfo", userInfo);
// 将用户信息存入Session
session.setAttribute("user", userInfo);
// 登录成功后删除Redis中的临时数据
redisTemplate.delete(key);
}
return result;
}
}
10. 用户服务
package com.example.wechatlogin.service;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class UserService {
@Autowired
private WxMpService wxMpService;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 根据OpenID保存或更新用户信息
* @param openId 用户OpenID
* @return 用户信息
*/
public Map<String, Object> saveOrUpdateUserByOpenId(String openId) {
try {
// 获取微信用户信息
WxMpUser wxUser = wxMpService.getUserService().userInfo(openId);
// 检查用户是否已存在
String checkSql = "SELECT COUNT(*) FROM wx_user WHERE open_id = ?";
Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class, openId);
if (count != null && count > 0) {
// 更新用户信息
String updateSql = "UPDATE wx_user SET nickname = ?, avatar = ?, update_time = NOW() WHERE open_id = ?";
jdbcTemplate.update(updateSql, wxUser.getNickname(), wxUser.getHeadImgUrl(), openId);
} else {
// 插入新用户
String insertSql = "INSERT INTO wx_user (open_id, nickname, avatar, create_time, update_time) VALUES (?, ?, ?, NOW(), NOW())";
jdbcTemplate.update(insertSql, openId, wxUser.getNickname(), wxUser.getHeadImgUrl());
}
// 返回用户信息
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("openId", wxUser.getOpenId());
userInfo.put("nickname", wxUser.getNickname());
userInfo.put("avatar", wxUser.getHeadImgUrl());
return userInfo;
} catch (WxErrorException e) {
throw new RuntimeException("获取微信用户信息失败", e);
}
}
/**
* 根据OpenID获取用户信息
* @param openId 用户OpenID
* @return 用户信息
*/
public Map<String, Object> getUserInfoByOpenId(String openId) {
String sql = "SELECT open_id, nickname, avatar FROM wx_user WHERE open_id = ?";
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql, openId);
if (results.isEmpty()) {
return saveOrUpdateUserByOpenId(openId);
}
return results.get(0);
}
}
11. 前端登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信扫码登录</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.login-container {
text-align: center;
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 320px;
}
.qrcode-container {
margin: 20px 0;
}
.qrcode-img {
width: 200px;
height: 200px;
}
.status-text {
color: #666;
margin: 10px 0;
}
.countdown {
color: #999;
font-size: 14px;
}
.success-info {
display: none;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin: 10px auto;
}
</style>
</head>
<body>
<div class="login-container">
<h2>微信扫码登录</h2>
<div class="qrcode-container">
<!-- 使用微信公众号返回的二维码图片URL -->
<img th:src="${qrCodeUrl}" class="qrcode-img" alt="微信扫码登录">
<p class="status-text">请使用微信扫描二维码登录</p>
<p class="countdown">二维码有效期: <span id="countdown">300</span>秒</p>
</div>
<div class="success-info">
<img src="" class="avatar" id="userAvatar" alt="用户头像">
<p>欢迎, <span id="userNickname"></span></p>
<p>登录成功,即将跳转...</p>
</div>
</div>
<script th:inline="javascript">
// 获取登录ID
const loginId = [[${loginId}]];
const expireTime = [[${expireTime}]];
let countdownTimer;
let checkStatusTimer;
let countdown = expireTime;
// 启动倒计时
function startCountdown() {
const countdownEl = document.getElementById('countdown');
countdownTimer = setInterval(() => {
countdown--;
countdownEl.textContent = countdown;
if (countdown <= 0) {
clearInterval(countdownTimer);
document.querySelector('.status-text').textContent = '二维码已过期,请刷新页面';
stopCheckingStatus();
}
}, 1000);
}
// 开始检查登录状态
function startCheckingStatus() {
checkStatusTimer = setInterval(() => {
fetch(`/login/check?loginId=${loginId}`)
.then(response => response.json())
.then(data => {
if (data.status === 'expired') {
document.querySelector('.status-text').textContent = '二维码已过期,请刷新页面';
stopCheckingStatus();
} else if (data.status === 'waiting') {
document.querySelector('.status-text').textContent = '等待扫码';
} else if (data.status === 'logged') {
// 登录成功
handleLoginSuccess(data.userInfo);
}
})
.catch(error => {
console.error('检查登录状态出错:', error);
});
}, 2000);
}
// 停止检查登录状态
function stopCheckingStatus() {
if (checkStatusTimer) {
clearInterval(checkStatusTimer);
}
if (countdownTimer) {
clearInterval(countdownTimer);
}
}
// 处理登录成功
function handleLoginSuccess(userInfo) {
stopCheckingStatus();
// 显示用户信息
document.querySelector('.qrcode-container').style.display = 'none';
document.querySelector('.success-info').style.display = 'block';
document.getElementById('userNickname').textContent = userInfo.nickname;
document.getElementById('userAvatar').src = userInfo.avatar;
// 3秒后跳转到首页
setTimeout(() => {
window.location.href = '/';
}, 3000);
}
// 页面加载后启动
window.addEventListener('load', () => {
startCountdown();
startCheckingStatus();
});
</script>
</body>
</html>
12. 数据库表结构
CREATE DATABASE IF NOT EXISTS wx_login;
USE wx_login;
CREATE TABLE IF NOT EXISTS wx_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
open_id VARCHAR(64) NOT NULL COMMENT '用户OpenID',
nickname VARCHAR(128) COMMENT '用户昵称',
avatar VARCHAR(512) COMMENT '用户头像URL',
create_time DATETIME NOT NULL COMMENT '创建时间',
update_time DATETIME NOT NULL COMMENT '更新时间',
UNIQUE KEY uk_open_id (open_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';
总结
本文详细介绍了如何通过SpringBoot集成weixin-java-mp实现微信公众号扫码登录功能。与传统的OAuth2.0授权方式不同,这种实现方式通过微信公众号的消息回调机制,实现了更灵活的扫码登录流程。开发者可以根据实际需求,对代码进行适当调整和优化,以满足不同场景下的使用需求。
更多推荐
所有评论(0)