微信公众号开发学习笔记
1、创建测试号
登录微信公众平台 https://mp.weixin.qq.com/cgi-bin/loginpage,设置与开发->开发者工具->公众平台测试账户
2、接口配置
2.1、文档内容
配置的Url可以选择服务器or内网穿透工具(https://natapp.cn/)
配置时微信会向配置的Url发送一条Get请求,会携带4个参数,分别是signature、timestamp、nonce、echostr。参考文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
2.2、代码实现
package cn.darkiris.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/wechat")
public class WechatController {
private static final String token = "wxtest";
@GetMapping("")
public String token(@RequestParam String signature, @RequestParam String timestamp, @RequestParam String nonce, @RequestParam String echostr) throws NoSuchAlgorithmException {
List<String> params = Arrays.asList(token, timestamp, nonce);
Collections.sort(params);
StringBuilder builder = new StringBuilder();
for (String param : params) {
builder.append(param);
}
MessageDigest sha1 = MessageDigest.getInstance("sha1");
byte[] bytes = sha1.digest(builder.toString().getBytes());
builder.setLength(0);
for (byte b : bytes) {
builder.append(Integer.toHexString(b >> 4 & 0x0f));
builder.append(Integer.toHexString(b & 0x0f));
}
if (builder.toString().equals(signature)) {
return echostr;
}
return null;
}
}
3、消息
3.1、文档内容
当有用户向微信公众号发送消息时,微信微信会向配置的Url发送一条Post请求,消息体是xml,携带用户信息和消息信息。参考文案:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
3.2、代码实现
1、消息实体类
这里因为要处理解析和生成时候的额外字符,所以有一个自定义的适配器CustomerXmlAdapter
package cn.darkiris.wechat;
import cn.darkiris.adapter.CustomerXmlAdapter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class Message {
@XmlElement(name = "ToUserName")
@XmlJavaTypeAdapter(CustomerXmlAdapter.class)
private String toUserName;
@XmlElement(name = "FromUserName")
@XmlJavaTypeAdapter(CustomerXmlAdapter.class)
private String fromUserName;
@XmlElement(name = "CreateTime")
@XmlJavaTypeAdapter(CustomerXmlAdapter.class)
private String createTime;
@XmlElement(name = "MsgType")
@XmlJavaTypeAdapter(CustomerXmlAdapter.class)
private String msgType;
@XmlElement(name = "Content")
@XmlJavaTypeAdapter(CustomerXmlAdapter.class)
private String content;
}
2、CustomerXmlAdapter
package cn.darkiris.adapter;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class CustomerXmlAdapter extends XmlAdapter<String, String> {
@Override
public String unmarshal(String v) {
if (v.startsWith("<![CDATA[")) {
return v.substring(9, v.length() - 3);
}
return v;
}
@Override
public String marshal(String v) {
return "<![CDATA[" + v + "]]>";
}
}
3、xml解析工具
这里注意使用的不是jdk8自带的jaxb,是额外引入的包jakarta.xml.bind-api@3.0.1,再后面的版本就不是jdk8能用的了
package cn.darkiris.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
import java.io.StringWriter;
@Slf4j
@Component
public class XmlUtil {
public Object xmlToObject(String xml, Class clazz) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return unmarshaller.unmarshal(new StringReader(xml));
} catch (Exception e) {
log.info("parse xml error: {}", e.getMessage());
return null;
}
}
public String objectToXml(Object object, Class clazz) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
StringWriter stringWriter = new StringWriter();
marshaller.marshal(object, stringWriter);
return stringWriter.toString().replace("<", "<").replace(">", ">");
} catch (Exception e) {
log.info("generate xml error: {}", e.getMessage());
}
return null;
}
}
4、SignatureUtil
package cn.darkiris.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
@Component
public class SignatureUtil {
private static final String token = "xxxxxxx";
public boolean matches(String signature, String timestamp, String nonce) {
List<String> params = Arrays.asList(token, timestamp, nonce);
Collections.sort(params);
StringBuilder builder = new StringBuilder();
for (String param : params) {
builder.append(param);
}
try {
MessageDigest sha1 = MessageDigest.getInstance("sha1");
byte[] bytes = sha1.digest(builder.toString().getBytes());
builder.setLength(0);
for (byte b : bytes) {
builder.append(Integer.toHexString(b >> 4 & 0x0f));
builder.append(Integer.toHexString(b & 0x0f));
}
return builder.toString().equals(signature);
} catch (NoSuchAlgorithmException e) {
log.info("no such algorithm: {}", e.getMessage());
}
return false;
}
}
5、WechatController
回复消息统一设置成hello,用户名!
package cn.darkiris.controller;
import cn.darkiris.util.XmlUtil;
import cn.darkiris.wechat.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/wechat")
public class WechatController {
@Autowired
private SignatureUtil signatureUtil;
@Autowired
private XmlUtil xmlUtil;
@GetMapping("")
public String token(@RequestParam String signature, @RequestParam String timestamp, @RequestParam String nonce, @RequestParam String echostr) {
if (signatureUtil.matches(signature, timestamp, nonce)) {
return echostr;
}
return null;
}
@PostMapping("")
public String message(@RequestParam String signature, @RequestParam String timestamp, @RequestParam String nonce, @RequestBody String xml) {
System.out.println("=============message=============");
System.out.println(xml);
System.out.println("signature: " + signature + ", timestamp: " + timestamp + ", nonce: " + nonce);
if (!signatureUtil.matches(signature, timestamp, nonce)) {
return "";
}
Message message = (Message) xmlUtil.xmlToObject(xml, Message.class);
if (message == null) {
return "";
}
message.setOpenid(message.getFromUserName());
Message reply = new Message(message.getFromUserName(), message.getToUserName(), String.valueOf(System.currentTimeMillis() / 1000), "text", "hello " + message.getFromUserName() + "!");
return xmlUtil.objectToXml(reply, Message.class);
}
}