SpringBoot微信公众号开发

时间:2024-03-05 11:09:28

公司有个微信公众的项目需要开发,在经过不停的查看文档与尝试后,终于能上手了,顺便找到个相当不错的微信公众开发依赖包,借此机会分享下经验。
微信公众号开发文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html

概念

在此之前需要清楚几个概念:
公众号包括订阅号和服务号,所谓订阅号就是在微信聊天页面被收在订阅号消息里面的,服务号是和聊天消息同级的。
订阅号与服务号开发统称为公众号开发,但是订阅号和服务号的权限并不相同。
这是订阅号:

这是服务号:

测试公众号

在开发前需要先有一个公众号,幸好微信官方提供了测试公众号。
点击链接登录测试公众号 https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
测试公众号拥有的权限几乎涵盖了订阅号和服务号的所有权限,仔细查看下自己的公众号的权限,不要在上线的时候发现权限不够了。

可以在这个页面获取到appId和appSecret。

项目添加依赖

添加pom依赖

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>4.1.0</version>
</dependency>

application.yml添加配置
appId和appSecret填入测试公众号的
token是自己在测试公众号页面设置的
aesKey是加密的key,明文模式可以不使用

# 微信配置
wechat:
  appId: ******
  appSecret: *******
  token: Sakura
  aesKey:

然后添加对应的Config文件

@Data
@Configuration
@ConfigurationProperties(prefix = "wechat")
public class WxConfig {

    private String appId;
    private String appSecret;
    private String token;
    private String aesKey;

}

使用配置

添加WxConfiguration类

/**
 * wechat mp configuration
 *
 * @author Binary Wang(https://github.com/binarywang)
 */
@AllArgsConstructor
@Configuration
@EnableConfigurationProperties(WxConfig.class)
public class WxMpConfiguration {

    private final WxConfig wxConfig;
    private final StringRedisTemplate redisTemplate;

    @Bean
    public WxRedisOps wxRedisOps() {
        return new RedisTemplateWxRedisOps(redisTemplate);
    }

    @Bean
    public WxMpConfigStorage wxMpConfigStorage(WxRedisOps wxRedisOps) {
        WxMpRedisConfigImpl mpRedisConfig = new WxMpRedisConfigImpl(wxRedisOps, "wechat");
        mpRedisConfig.setAppId(wxConfig.getAppId());
        mpRedisConfig.setSecret(wxConfig.getAppSecret());
        mpRedisConfig.setToken(wxConfig.getToken());
        mpRedisConfig.setAesKey(wxConfig.getAesKey());
        return mpRedisConfig;
    }

    @Bean
    public WxMpService wxMpService(WxMpConfigStorage configStorage) {
        WxMpService service = new WxMpServiceImpl();
        service.setWxMpConfigStorage(configStorage);
        return service;
    }

    // 消息路由 通过设置规则可以将消息交给指定的MessageHandler去处理,再经过Controller返回给微信服务器
    @Bean
    public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
        final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
        // newRouter.rule().async(false).handler(this.globalMessageHandler).end();
        // newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
        // newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.subscribeHandler).end();
        // newRouter.rule().async(false).msgType(TEXT).handler(this.textMessageHandler).end();
        // newRouter.rule().async(false).msgType(EVENT).event(CLICK).handler(this.menuClickHandler).end();
        return newRouter;
    }
}

编写微信回调方法

如果是需要开发回复用户消息之类的需要微信服务器与开发服务器交互的内容,则需要编写微信回调接口并在公众号进行相应配置。
回调接口需要根据微信的接口文档实现相应的业务逻辑。
需要注意的是回调接口要能够公网访问,如果是在开发中可以使用内网穿透工具。

添加回调Controller类

/**
 * Description: 微信回调控制器
 *
 * @author ZhangJiaYu
 * @date 2021/10/21
 */
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/wx/callback")
public class WeChatCallbackController {

    private final WxMpService wxService;
    private final WxMpMessageRouter messageRouter;

    @GetMapping(produces = "text/plain;charset=utf-8")
    public String authGet(@RequestParam(name = "signature", required = false) String signature,
                          @RequestParam(name = "timestamp", required = false) String timestamp,
                          @RequestParam(name = "nonce", required = false) String nonce,
                          @RequestParam(name = "echostr", required = false) String echostr) {

        log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,
                timestamp, nonce, echostr);
        if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
            throw new IllegalArgumentException("请求参数非法,请核实!");
        }
        if (wxService.checkSignature(timestamp, nonce, signature)) {
            return echostr;
        }
        return "非法请求";
    }

    @PostMapping(produces = "application/xml; charset=UTF-8")
    public String post(@RequestBody String requestBody,
                       @RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam("openid") String openid,
                       @RequestParam(name = "encrypt_type", required = false) String encType,
                       @RequestParam(name = "msg_signature", required = false) String msgSignature) {
        log.info("\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
                        + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
                openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
        if (!wxService.checkSignature(timestamp, nonce, signature)) {
            throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
        }
        String out = null;
        if (encType == null) {
            // 明文传输的消息
            WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
            // 将路由的消息进行返回
            WxMpXmlOutMessage outMessage = this.route(inMessage);
            if (outMessage == null) {
                return "";
            }
            out = outMessage.toXml();
        } else if ("aes".equalsIgnoreCase(encType)) {
            // aes加密的消息
            WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(),
                    timestamp, nonce, msgSignature);
            log.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
            WxMpXmlOutMessage outMessage = this.route(inMessage);
            if (outMessage == null) {
                return "";
            }
            out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage());
        }
        log.debug("\n组装回复信息:{}", out);
        return out;
    }
    
    private WxMpXmlOutMessage route(WxMpXmlMessage message) {
        try {
            return this.messageRouter.route(message);
        } catch (Exception e) {
            log.error("路由消息时出现异常!", e);
        }
        return null;
    }
}

编写完成后在公众号进行设置即可

微信网页授权

文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
微信网页授权是个难点,但是捋清楚了,就很简单。

在微信中访问targetUrl(目标页面)的时候想要获取用户的信息,需要先通过redirect接口(重定向接口)去构建一个重定向地址到微信授权的地址,授权完成后,微信会再次重定向到targetUrl并携带code参数,通过code参数就可以换取用户信息。

redirect(构建授权地址并重定向)->微信授权地址(携带redirect_url=targetUrl的参数)->targetUrl(携带code参数)->通过code参数换取用户信息

编写Controller类

@RestController
@RequestMapping("/wxlogin")
public class WeChatLoginController {

    private final WxMpService wxMpService;

    // 公网地址
    @Value("${wechat.domain}")
    private String domain;
    
    // 当前controller的path
    private String path = "/wxlogin";

    public WeChatLoginController(WxMpService wxMpService) {
        this.wxMpService = wxMpService;
    }
  
    // 通过此接口构建微信授权地址并重定向
    @SneakyThrows
    @GetMapping("/redirect")
    public void login(HttpServletResponse response) {
        /**
         * 重定向地址 能够获取到code参数 {@link #callback}
         */
        String redirectUrl = domain + path + "/callback";
        WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service();
        String authorizationUrl = oAuth2Service.buildAuthorizationUrl(redirectUrl, WxConsts.OAuth2Scope.SNSAPI_USERINFO, null);
        response.sendRedirect(authorizationUrl);
    }
    
    // 微信授权通过后会写携带code重定向到此地址
    @SneakyThrows
    @GetMapping("/callback")
    public void callback(String code, HttpServletResponse response) {
        // 通过code换取用户信息
        WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service();
        WxOAuth2AccessToken accessToken = oAuth2Service.getAccessToken(code);
        WxOAuth2UserInfo userInfo = oAuth2Service.getUserInfo(accessToken, null);
        // 可以携带参数重定向到前端页面或者执行其他操作
    }
}

weixin-java-mp的Demo和文档:https://github.com/binarywang/weixin-java-mp-demo