springboot 整合微信公众号--验证码推送(spring boot+测试号)

一、公众号开发初探

这里会使用到自己的域名进行交互,没有域名的小伙伴可以使用  内网穿透(NATAPP) 如果没有使用过的的同学请移步 20 秒轻松上手 NATAAPP (内网穿透)

公众号整体流程:用户扫公众号二维码。然后发一条消息:验证码。我们通过 api 回复一个随机的验证码,并且存入 redis。用户在验证码框输入之后,点击登录,进入我们的注册模块,同时关联角色和权限。就实现了网关的统一鉴权。

二、微信公众平台使用(测试号的申请)

首先我们进入微信公众平台首页,这里我们选用订阅号,点击开发文档

然后按照图中位置,进入测试号申请

 申请完成后,我们会进入这样的页面,这样就成功啦,大家可以扫自己的测试二维码试一下

三、服务端环境搭建

首先,导入本次项目所需的pom依赖

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.4.2</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.18</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.12.7</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.9.0</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

 然后新建applicationyml,配置端口和 redis 

server:
  port: 3012
spring:
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # Redis服务器地址
    host: 123.60.123.123
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: 123456
    # 连接超时时间
    timeout: 2s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0

 这里给大家补充一点知识:

        1、因为微信公众平台他定义的参数是xml形式的数据,无论是请求还是回调,所以为了方便,这里我写了一个工具类将xml文件进行了一次转换

 /**
     * 解析微信发来的请求(XML).
     *
     * @param msg 消息
     * @return map
     */
    public static Map<String, String> parseXml(final String msg) {
        // 将解析结果存储在HashMap中
        Map<String, String> map = new HashMap<String, String>();

        // 从request中取得输入流
        try (InputStream inputStream = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8.name()))) {
            // 读取输入流
            SAXReader reader = new SAXReader();
            Document document = reader.read(inputStream);
            // 得到xml根元素
            Element root = document.getRootElement();
            // 得到根元素的所有子节点
            List<Element> elementList = root.elements();

            // 遍历所有子节点
            for (Element e : elementList) {
                map.put(e.getName(), e.getText());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return map;
    }

         2、开发者可选择消息加解密方式:明文模式、兼容模式和安全模式。模式的选择与服务器配置在提交后都会立即生效,大家谨慎填写及选择。加解密方式的默认状态为明文模式,选择兼容模式和安全模式需要提前配置好相关加解密代码,详情请参考消息体签名及加解密部分的文档。这里为了方便我也写了一个抽象类进行加密操作进行签名的加密。

  /**
     * 用SHA1算法生成安全签名
     *
     * @param token     票据
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param encrypt   密文
     * @return 安全签名
     */
    public static String getSHA1(String token, String timestamp, String nonce, String encrypt) {
        try {
            String[] array = new String[]{token, timestamp, nonce, encrypt};
            StringBuffer sb = new StringBuffer();
            // 字符串排序
            Arrays.sort(array);
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexStr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexStr.append(0);
                }
                hexStr.append(shaHex);
            }
            return hexStr.toString();
        } catch (Exception e) {
            log.error("sha加密生成签名失败:", e);
            return null;
        }
    }

然后我们新建一个接口,分别用来回调消息校验和被动消息回复,来先给大家测试一下我们的申请是否成功访问到微信公众平台。

 /**
     * 回调消息校验
     */
    @GetMapping("/callback")
    public String callback(@RequestParam("signature") String signature,
                           @RequestParam("timestamp") String timestamp,
                           @RequestParam("nonce") String nonce,
                           @RequestParam("echostr") String echostr) {
        log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}",
                signature, timestamp, nonce, echostr);
        String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
        if (signature.equals(shaStr)) {
            return echostr;
        }
        return "unknown";
    }

    /**
     * 被动消息回复
     */



    @PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
    public String callback(
            @RequestBody String requestBody,
            @RequestParam("signature") String signature,
            @RequestParam("timestamp") String timestamp,
            @RequestParam("nonce") String nonce,
            @RequestParam(value = "msg_signature", required = false) String msgSignature) {
        log.info("接收到的消息:requestBody:{},signature:{},timestamp:{},nonce:{}"
                , requestBody, signature, timestamp, nonce);
        Map<String, String> messageMap = MessageUtil.parseXml(requestBody);
        String fromUserName = messageMap.get("FromUserName");
        String toUserName = messageMap.get("ToUserName");
        String msg = "<xml>\n" +
                "  <ToUserName><![CDATA[" + toUserName + "]]></ToUserName>\n" +
                "  <FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>\n" +
                "  <CreateTime>12345678</CreateTime>\n" +
                "  <MsgType><![CDATA[text]]></MsgType>\n" +
                "  <Content><![CDATA[测试,被动回复消息]]></Content>\n" +
                "</xml>";
        return msg;
    }

如果大家配置正确,应该能得到下面的结果

                ​​​​​​​        

四、整合工厂+策略模式 +redis进行二次改造

如果不熟悉策略+工厂模式的小伙伴,请移步   >     策略 + 工厂 + springboot实战结合

首先新建一个策略的枚举类,用来做登陆状态识别

/**
 * 公众号登录状态识别
 */
public enum WxChatMsgTypeEnum {

        SUBSCRIBE("event.subscribe","用户关注事件"),
        TEXT_MSG("text","接收用户文本信息");

    private String msgType;

    private String desc;

    WxChatMsgTypeEnum(String msgType, String desc) {
        this.msgType = msgType;
        this.desc = desc;
    }

    public static WxChatMsgTypeEnum getByMsgType(String msgType) {
        for (WxChatMsgTypeEnum wxChatMsgTypeEnum : WxChatMsgTypeEnum.values()) {
            if (wxChatMsgTypeEnum.msgType.equals(msgType)) {
                return wxChatMsgTypeEnum;
            }
        }
        return null;
    }
}

然后新建一个公众号策略接口

/**
 * 策略接口
 */
public interface WxChatMsgHandler {

    WxChatMsgTypeEnum getMsgType();

    String dealMsg(Map<String, String> messageMap);
}

新建一个公众号工厂类 

/**
 * 公众号工厂
 */
@Component
public class WxChatMsgFactory implements InitializingBean {

    @Resource
    private List<WxChatMsgHandler>wxChatMsgHandlerList;

    private Map<WxChatMsgTypeEnum,WxChatMsgHandler> handlerMap = new HashMap<>();

    public WxChatMsgHandler getHandlerByMsgType(String msgType){
        WxChatMsgTypeEnum msgTypeEnum = WxChatMsgTypeEnum.getByMsgType(msgType);
        return handlerMap.get(msgTypeEnum);
    }


    @Override
    public void afterPropertiesSet() throws Exception {
        for (WxChatMsgHandler wxChatMsgHandler : wxChatMsgHandlerList) {
            handlerMap.put(wxChatMsgHandler.getMsgType(),wxChatMsgHandler);
        }

    }
}

 然后,我们需要新建两个策略去实现我们的策略接口,这两个策略分别对应的处理:

        1、关注事件:当用户关注时,我们应该返回这里是XXX公众号

        2、验证码回复:当用户发送验证码时,我们应该捕获用户发送的信息,并进行比对,如果是我们定义的关键字,那么我们返回一个随机验证码并存入redis中返回

关注事件

/**
 * 关注事件文本
 */
@Component
@Slf4j
public class SubscribeMsgHandler implements WxChatMsgHandler{
    @Override
    public WxChatMsgTypeEnum getMsgType() {
        return WxChatMsgTypeEnum.SUBSCRIBE;
    }

    @Override
    public String dealMsg(Map<String, String> messageMap) {
        log.info("接收到关注事件");
        String fromUserName = messageMap.get("FromUserName");
        String toUserName = messageMap.get("ToUserName");
        String content = "你好,这里是布川ku子的博客,欢迎大家留言评论!";
        String msg = "<xml>\n" +
                "  <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
                "  <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
                "  <CreateTime>12345678</CreateTime>\n" +
                "  <MsgType><![CDATA[text]]></MsgType>\n" +
                "  <Content><![CDATA["+content+"]]></Content>\n" +
                "</xml>";
        return msg;
    }
}

验证码回复

/**
 * 接收文本策略
 */
@Component
@Slf4j
public class ReceiveTextMsgHandler implements WxChatMsgHandler{

    private static final String KEY_WORD = "验证码";
    private static final String LOGIN_PREFIX = "loginCode";

    @Override
    public WxChatMsgTypeEnum getMsgType() {
        return WxChatMsgTypeEnum.TEXT_MSG;
    }

    @Resource
    private RedisUtil redisUtil;

    @Override
    public String dealMsg(Map<String, String> messageMap) {

        log.info("接收到文本消息事件");
        String context = messageMap.get("Content");
        if (!KEY_WORD.equals(context)){
            return "";
        }
        String fromUserName = messageMap.get("FromUserName");
        String toUserName = messageMap.get("ToUserName");
        Random random = new Random();
        int num = random.nextInt(235234);
        String numKey = redisUtil.buildKey(LOGIN_PREFIX, String.valueOf(num));
        redisUtil.setNx(numKey,fromUserName,5L, TimeUnit.MINUTES);
        String numContent = "您当前的验证码是:" + num + "! 5分钟内有效";
        String replyContent = "<xml>\n" +
                "  <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
                "  <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
                "  <CreateTime>12345678</CreateTime>\n" +
                "  <MsgType><![CDATA[text]]></MsgType>\n" +
                "  <Content><![CDATA[" + numContent + "]]></Content>\n" +
                "</xml>";
        return replyContent;
    }
}

然后我们给我们写好的callback接口加一些新的功能,重写一下

/**
 * 公众号接口
 */
@RestController
@Slf4j
public class CallBackController {

    private final String token = "buchuankuzi";

    @Resource
    private WxChatMsgFactory wxChatMsgFactory;
    
    /**
     * 回调消息校验
     */
    @GetMapping("/callback")
    public String callback(@RequestParam("signature") String signature,
                           @RequestParam("timestamp") String timestamp,
                           @RequestParam("nonce") String nonce,
                           @RequestParam("echostr") String echostr) {
        log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}",
                signature, timestamp, nonce, echostr);
        String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
        if (signature.equals(shaStr)) {
            return echostr;
        }
        return "unknown";
    }

    /**
     *
     */
    @PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
    public String callback(
            @RequestBody String requestBody,
            @RequestParam("signature") String signature,
            @RequestParam("timestamp") String timestamp,
            @RequestParam("nonce") String nonce,
            @RequestParam(value = "msg_signature", required = false) String msgSignature) {
        log.info("接收到微信消息:requestBody:{}", requestBody);
        Map<String, String> messageMap = MessageUtil.parseXml(requestBody);
        String msgType = messageMap.get("MsgType");
        String event = messageMap.get("Event") == null ? "" : messageMap.get("Event");
        log.info("msgType:{},event:{}", msgType, event);

        StringBuilder sb = new StringBuilder();
        sb.append(msgType);
        if (!StringUtils.isEmpty(event)) {
            sb.append(".");
            sb.append(event);
        }
        String msgTypeKey = sb.toString();
        WxChatMsgHandler handlerByMsgType = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey);
        String replyContent = handlerByMsgType.dealMsg(messageMap);
        return replyContent;
    }
}

到此,我们的公众号验证码发送就完成啦,我们再次扫码验证一下,出现下面这个界面就代表成功啦!

 博主主页还有很多有趣的知识分享,大家感兴趣请移步主页!

要集成微信公众号的消息推送功能,可以使用Spring Boot和WeChat SDK。以下是一些步骤: 1. 在微信公众平台上创建一个公众号,并获得AppID和AppSecret。 2. 在Spring Boot项目中添加WeChat SDK依赖项。 3. 创建一个Controller类来处理微信服务器发送过来的请求,比如验证URL有效性和处理用户消息。 4. 配置服务器URL和Token,并在微信公众平台上进行验证。 5. 使用SDK提供的方法实现消息的回复和推送。 以下是一个简单的代码示例: ```java @RestController @RequestMapping("/wechat") public class WeChatController { @Autowired private WxMpService wxMpService; @GetMapping(produces = "text/plain;charset=utf-8") public String validate(@RequestParam(name = "signature") String signature, @RequestParam(name = "timestamp") String timestamp, @RequestParam(name = "nonce") String nonce, @RequestParam(name = "echostr") String echostr) { if (wxMpService.checkSignature(timestamp, nonce, signature)) { return echostr; } return "error"; } @PostMapping(produces = "application/xml; charset=UTF-8") public String handleMessage(@RequestBody String requestBody, @RequestParam(name = "signature") String signature, @RequestParam(name = "timestamp") String timestamp, @RequestParam(name = "nonce") String nonce, @RequestParam(name = "openid") String openid) { // 处理用户发送的消息 WxMpXmlMessage wxMessage; try { wxMessage = WxMpXmlMessage.fromXml(requestBody); } catch (Exception e) { return "error"; } // 构造回复消息 WxMpXmlOutMessage outMessage = WxMpXmlOutMessage.TEXT() .content("你好,欢迎关注我的公众号!") .fromUser(wxMessage.getToUser()) .toUser(wxMessage.getFromUser()) .build(); return outMessage.toXml(); } } ``` 在上面的代码中,我们首先验证了微信服务器发送过来的请求是否有效。如果有效,我们返回echostr作为响应。接下来,我们处理用户发送的消息,并构造一个回复消息。最后,我们将回复消息作为响应返回给微信服务器。 请注意,我们使用了WxMpService类提供的方法来验证签名、解析消息和构造回复消息。要使用这个类,我们需要在Spring Boot项目的配置文件中添加以下内容: ``` # WeChat SDK wx.mp.appId=<your app ID> wx.mp.secret=<your app secret> wx.mp.token=<your token> wx.mp.aesKey=<your AES key> ``` 在上面的代码中,我们使用了WxMpXmlOutMessage.TEXT()方法来构造文本消息的回复。如果你需要回复其他类型的消息,比如图文消息或音频消息,请查看WeChat SDK的文档。 最后,我们需要在微信公众平台上配置服务器URL和Token。可以在公众号的基本配置页面中找到这些设置。在配置完成后,我们可以向公众号发送消息,然后观察是否收到了回复。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值