SpringBoot接入微信公众号【服务号】
一、服务号注册
注册地址:https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN
注册流程参考:https://kf.qq.com/touch/faq/150804UVr222150804quq6B7.html?platform=15
二、服务号配置
基本配置
用于服务配置
访问及服务配置
网页授权配置
三、核心代码
- 基于WxJava - 微信开发 Java SDK
- 实现关注、取关逻辑
- 实现消息被动回复
- 实现个人信息网页授权
- 提供基本业务功能
微信开发者文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
WxJava开源地址:https://gitee.com/binary/weixin-java-tools?_from=gitee_search
引入pom依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.5.0</version>
</dependency>
yml配置文件
# 微信公众号配置
wx:
mp:
appId: wx1111111
appSecret: 111111111111
# 配置消息回调地址接入公众号时需要的token
token: add2222
aesKey: xxxx
server: https://www.baidu.com/
属性配置类
package com.qiangesoft.wechat.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 微信公众号相关配置属性
*
* @author qiangesoft
* @date 2023-09-07
*/
@Data
@Component
@ConfigurationProperties(WxMpProperties.PREFIX)
public class WxMpProperties {
public static final String PREFIX = "wx.mp";
/**
* 公众号开发信息:appId
*/
private String appId;
/**
* 公众号开发信息:appSecret
*/
private String appSecret;
/**
* 服务器配置:token
*/
private String token;
/**
* 服务器配置:消息加解密密钥EncodingAESKey
*/
private String aesKey;
/**
* 服务器配置:微信服务器地址
*/
private String server;
}
bean配置类
package com.qiangesoft.wechat.config;
import com.qiangesoft.wechat.core.handler.LogHandler;
import com.qiangesoft.wechat.core.handler.event.SubscribeHandler;
import com.qiangesoft.wechat.core.handler.event.UnsubscribeHandler;
import com.qiangesoft.wechat.core.handler.message.ImageHandler;
import com.qiangesoft.wechat.core.handler.message.LocationHandler;
import com.qiangesoft.wechat.core.handler.message.MessageHandler;
import com.qiangesoft.wechat.core.handler.message.VoiceHandler;
import com.qiangesoft.wechat.core.interceptor.MessageInterceptor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 微信公众号配置
*
* @author qiangesoft
* @date 2023-09-07
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class WxMpConfiguration {
/**
* 微信公众号配置信息
*/
private final WxMpProperties wxMpProperties;
/**
* 日志处理器
*/
private final LogHandler logHandler;
/**
* 订阅事件处理器
*/
private final SubscribeHandler subscribeHandler;
/**
* 取消订阅事件处理器
*/
private final UnsubscribeHandler unsubscribeHandler;
/**
* 语音消息处理器
*/
private final VoiceHandler voiceHandler;
/**
* 文本消息处理器
*/
private final MessageHandler messageHandler;
/**
* 图片消息处理器
*/
private final ImageHandler imageHandler;
/**
* 位置消息处理器
*/
private final LocationHandler locationHandler;
/**
* 消息拦截器
*/
private final MessageInterceptor messageInterceptor;
/**
* 声明实例
*
* @return
*/
@Bean
public WxMpService wxMpService() {
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
/**
* 配置存储方式
*
* @return
*/
@Bean
public WxMpConfigStorage wxMpConfigStorage() {
WxMpDefaultConfigImpl configStorage = new WxMpDefaultConfigImpl();
// 公众号appId
configStorage.setAppId(wxMpProperties.getAppId());
// 公众号appSecret
configStorage.setSecret(wxMpProperties.getAppSecret());
// 公众号Token
configStorage.setToken(wxMpProperties.getToken());
// 公众号EncodingAESKey
configStorage.setAesKey(wxMpProperties.getAesKey());
return configStorage;
}
/**
* 配置公众号的事件路由
* <p>
* 1.用户发消息
* 2.关注事件
* 3.取消关注事件
* ......
*/
@Bean
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
// 记录所有消息事件的日志
newRouter.rule().async(false).handler(this.logHandler).next();
// 文本消息
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.TEXT).interceptor(this.messageInterceptor).handler(this.messageHandler).end();
// 语音消息
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.VOICE).handler(this.voiceHandler).end();
// 图片消息
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.IMAGE).handler(this.imageHandler).end();
// 位置消息
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION).handler(this.locationHandler).end();
// 关注事件
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler).end();
// 取消关注事件
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.UNSUBSCRIBE).handler(this.unsubscribeHandler).end();
return newRouter;
}
}
网页授权接入配置
下载验证文件放于项目目录
package com.qiangesoft.wechat.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 微信接入控制器
* ps:(JS接口安全域名、网页授权域名需要获取MP_verify_eerrereeee.txt)
* <a href="https://mp.weixin.qq.com/cgi-bin/settingpage?t=setting/function&action=function&token=854866943&lang=zh_CN">...</a>
*
* @author qiangesoft
* @date 2023-09-11
*/
@Slf4j
@Api(tags = "微信接入")
@RestController
@RequestMapping("/wechat")
public class WxAccessController {
/**
* 获取安全凭证
* ps:例如[http://主机:端口/wechat/MP_verify_eerrereeee.txt]
*
* @param fileName
* @param response
*/
@ApiOperation(value = "安全凭证")
@GetMapping("/{fileName}")
public void security(@PathVariable String fileName, HttpServletResponse response) {
InputStream is = null;
OutputStream os = null;
try {
// 获取文件
ClassPathResource classPathResource = new ClassPathResource(fileName);
String filename = classPathResource.getFilename();
is = classPathResource.getInputStream();
byte[] bytes = is.readAllBytes();
response.setCharacterEncoding("UTF-8");
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
response.addHeader(HttpHeaders.CONTENT_LENGTH, bytes.length + "");
response.setContentType("application/octet-stream");
os = response.getOutputStream();
os.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
微信请求验证及被动消息回复
package com.qiangesoft.wechat.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 微信消息控制器
* ps:请求验证、接收消息被动回复等
*
* @author qiangesoft
* @date 2023-09-07
*/
@Slf4j
@Api(tags = "微信消息")
@RestController
@RequestMapping("/wechat/message")
public class WxMessageController {
@Autowired
private WxMpService wxMpService;
@Autowired
private WxMpMessageRouter wxMpMessageRouter;
@ApiOperation(value = "验证接口")
@GetMapping
public String validate(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String echostr) {
// 校验消息是否来自微信
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}
return echostr;
}
@ApiOperation(value = "接收消息")
@PostMapping(produces = "application/xml; charset=UTF-8")
public String handleMessage(@RequestBody String requestBody,
@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
// 校验消息是否来自微信
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}
// 消息处理【被动消息回复】
String message = null;
if (encType == null) {
// 明文传输
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
if (outMessage == null) {
return null;
}
message = outMessage.toXml();
} else if ("aes".equalsIgnoreCase(encType)) {
// aes加密
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxMpService.getWxMpConfigStorage(), timestamp, nonce, msgSignature);
WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
if (outMessage == null) {
return null;
}
message = outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
}
return message;
}
}
用户信息网页授权
package com.qiangesoft.wechat.controller;
import com.qiangesoft.wechat.config.WxMpProperties;
import com.qiangesoft.wechat.dto.UserDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.service.WxOAuth2Service;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
/**
* 微信用户控制器
* ps:获取用户信息、授权网页用户信息等
*
* @author qiangesoft
* @date 2023-09-11
*/
@Slf4j
@Api(tags = "微信用户")
@Controller
@RequestMapping("/wechat/user")
public class WxUserController {
@Autowired
private WxMpProperties wxMpProperties;
@Autowired
private WxMpService wxMpService;
@ApiOperation(value = "个人信息")
@GetMapping("/mine")
public String mine() {
WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service();
// 构建授权url
String authorizationUrl = oAuth2Service.buildAuthorizationUrl(wxMpProperties.getServer() + "wechat/user/callback", WxConsts.OAuth2Scope.SNSAPI_USERINFO, null);
return "redirect:" + authorizationUrl;
}
@ApiOperation(value = "个人信息网页授权回调")
@GetMapping("/callback")
public String callback(String code, Model model) throws WxErrorException {
WxOAuth2UserInfo userInfo = this.getUserInfo(code);
String openid = userInfo.getOpenid();
// todo 通过openid拿取业务系统详细用户信息 IWxUserService
UserDTO userDTO = new UserDTO();
userDTO.setOpenId(openid);
userDTO.setNickName(userInfo.getNickname());
model.addAttribute("user", userDTO);
return "mine";
}
/**
* 获取微信用户信息
*
* @param code
* @return
* @throws WxErrorException
*/
private WxOAuth2UserInfo getUserInfo(String code) throws WxErrorException {
WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service();
// 利用code获取accessToken
WxOAuth2AccessToken accessToken = oAuth2Service.getAccessToken(code);
// 利用accessToken获取用户信息
WxOAuth2UserInfo userInfo = oAuth2Service.getUserInfo(accessToken, null);
return userInfo;
}
}
消息拦截器
package com.qiangesoft.wechat.core.interceptor;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpMessageInterceptor;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 对微信公众号消息进行预处理、过滤等操作,根据具体业务需求决定是否允许继续执行后面的路由处理方法
* <p>
* 1.如果要中止消息的继续处理,需要返回 false
* 2.在执行完当前拦截器操作后,允许消息的继续处理,返回 true
*
* @author qiangesoft
* @date 2023-09-07
*/
@Component
public class MessageInterceptor implements WxMpMessageInterceptor {
@Override
public boolean intercept(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
String msgType = wxMessage.getMsgType();
String content = wxMessage.getContent();
// 文本处理
if (msgType.equals(WxConsts.XmlMsgType.TEXT)) {
if (content.contains("混蛋")) {
String newContent = content.replace("混蛋", "***");
wxMessage.setContent(newContent);
}
return true;
}
// todo 音频、视频处理
return true;
}
}
消息事件处理器
1.关注事件处理器
package com.qiangesoft.wechat.core.handler.event;
import com.qiangesoft.wechat.core.builder.TextOutMessageBuilder;
import com.qiangesoft.wechat.entity.WxUser;
import com.qiangesoft.wechat.service.IWxUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
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.stereotype.Component;
import java.util.Map;
/**
* 微信公众号用户关注处理器
*
* @author qiangesoft
* @date 2023-09-07
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SubscribeHandler implements WxMpMessageHandler {
private final IWxUserService wxUserService;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
if (!wxMessage.getMsgType().equals(WxConsts.EventType.SUBSCRIBE)) {
return null;
}
String openId = wxMessage.getFromUser();
// 微信关注用户
WxUser wxUser = wxUserService.getByOpenId(openId);
if (wxUser == null) {
wxUser = new WxUser();
wxUser.setOpenId(openId);
wxUser.setAttentionFlag(true);
wxUserService.save(wxUser);
} else {
wxUser.setAttentionFlag(true);
wxUserService.updateById(wxUser);
}
String outContent = "您好,欢迎关注xxx!";
return new TextOutMessageBuilder().build(outContent, wxMessage, wxMpService);
}
}
2.取关事件处理器
package com.qiangesoft.wechat.core.handler.event;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.qiangesoft.wechat.core.builder.TextOutMessageBuilder;
import com.qiangesoft.wechat.entity.WxUser;
import com.qiangesoft.wechat.service.IWxUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
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.stereotype.Component;
import java.util.Map;
/**
* 微信公众号用户取消关注处理器
*
* @author qiangesoft
* @date 2023-09-07
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class UnsubscribeHandler implements WxMpMessageHandler {
private final IWxUserService wxUserService;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
if (!wxMessage.getMsgType().equals(WxConsts.EventType.UNSUBSCRIBE)) {
return null;
}
String openId = wxMessage.getFromUser();
// 取消关注
LambdaUpdateWrapper<WxUser> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(WxUser::getOpenId, openId)
.set(WxUser::getAttentionFlag, false);
wxUserService.update(updateWrapper);
String outContent = "已取消关注xxx,再见!";
return new TextOutMessageBuilder().build(outContent, wxMessage, wxMpService);
}
}
3.文本消息处理器
package com.qiangesoft.wechat.core.handler.message;
import com.qiangesoft.wechat.core.builder.TextOutMessageBuilder;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
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.stereotype.Component;
import java.util.Map;
/**
* 文本消息处理器
*
* @author qiangesoft
* @date 2023-09-07
*/
@Slf4j
@Component
public class MessageHandler implements WxMpMessageHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
if (!wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) {
return null;
}
// 接收的消息内容
String content = wxMessage.getContent();
log.info("Received wechat text message, content [{}] !", content);
String outContent = "您好,欢迎访问xxx!";
if (content.contains("你好") || content.contains("您好")) {
outContent = "您好";
}
return new TextOutMessageBuilder().build(outContent, wxMessage, wxMpService);
}
}
4.日志记录处理器
package com.qiangesoft.wechat.core.handler;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
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.stereotype.Component;
import java.util.Map;
/**
* 微信公众号日志记录处理器
*
* @author qiangesoft
* @date 2023-09-07
*/
@Slf4j
@Component
public class LogHandler implements WxMpMessageHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
log.info("Received wechat message, content is [{}]", JSONObject.toJSONString(wxMessage));
return null;
}
}
四、源码地址
地址:https://gitee.com/qiangesoft/boot-business/tree/master/boot-business-wechat