首页 > 其他分享 >springboot 整合微信公众号--验证码推送(spring boot+测试号)

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

时间:2024-08-12 23:25:03浏览次数:12  
标签:nonce return springboot -- 微信 signature timestamp public String

一、公众号开发初探

这里会使用到自己的域名进行交互,没有域名的小伙伴可以使用  内网穿透(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;
    }
}

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

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

标签:nonce,return,springboot,--,微信,signature,timestamp,public,String
From: https://blog.csdn.net/indexqian/article/details/141068889

相关文章

  • 智谱清影-CogVideoX-2b-部署与使用
    效果展示Astreetartist,cladinaworn-outdenimjacketandacolorfulbandana,standsbeforeavastconcretewallintheheart,holdingacanofspraypaint,spray-paintingacolorfulbirdonamottledwall.部署......
  • Auto-Logon After Windows-Reboot
    #askforlogoncredentials:$cred=Get-Credential-Message'Logonautomatically'$password=$cred.GetNetworkCredential().Password$username=$cred.UserName#savelogoncredentialstoregistry(WARNING:cleartextpasswordused):$path=&......
  • E-小红的序列乘积2.0(牛客周赛55)
    E-小红的序列乘积2.0题意:给定数组a,求子序列前缀积个位数为6的数字个数。分析:只要算个位数是否为6,所以把a数组都换成个位数上的数就好了。用a数组与1到9的数字进行组合,用组合数学算出组合数。代码:#include<bits/stdc++.h>usingnamespacestd;typedeflonglongll;const......
  • python格式化输出
    age=30score=77.5gender='男'name="贾宝玉"#想要去除默认的左右空格可以通过“+”将所有的对象连接成一个字符串来避免默认的空格print("个人信息:"+name+"--"+str(age))#使用%,称为占位符print("个人信息:%s-%d-%s-%.2f"%(name,age,gender,sc......
  • 机器学习——完整的基础概念学习,机器学习分类
    机器学习——完整的基础概念学习,机器学习分类一、机器学习与深度学习机器学习与深度学习的区别和联系机器学习是人工智能的一个分支,它使计算机能够通过学习数据和模式来自动改进和优化算法。相比之下,深度学习是机器学习的一个子集(是机器学习的一种),它依赖于类似于人脑的神......
  • 贪心法-一般背包问题
    一般背包问题问题描述想象你有一个背包,它最多可以放M公斤的物品。你面前有n个物品,每个物品的重量是Wi,如果将这个物品完全放入背包,你将获得Pi的收益。问题目标你需要决定如何放置这些物品,以便在不超过背包容量的前提下,获得最大的收益。问题类型0/1背包问题:每个......
  • 夏日狂欢,游戏新体验,植物大战僵尸杂交版 v2.3.5
    ......
  • 《企业微服务实战 · 接口鉴权思路分享》
    ......
  • 豆瓣影评数据抓取
    豆瓣影评数据抓取创建时间:2024-08-12抓取豆瓣影评相关数据的代码,包括封面、标题、评论内容以及影评详情页的数据。一、完整代码'''https://movie.douban.com/review/best/抓取封面标题评论內容抓取完整的评论内容也就是点击展开后的完整的抓取当前影评的详情页的数据......
  • 【Windows系列】网卡1访问外网,网卡2访问内网!
    背景一、实验环境准备二、查看ipv4服务是否勾选和开启三、修改网卡路由四、修改网卡路由背景当我们的Windows电脑有双网卡,若这时想要实现一张网卡用于访问外网,另一张网卡用于访问内网的功能。比如通过远程电脑,然后再通过电脑去访问我们家里的NAS存储等。通常这种需求下......