微信公众号开发-java版(一)

1.微信公众平台

微信公众平台,简称公众号。分类有:
1.订阅号:主要偏于为用户传达资讯(类似报纸杂志),认证前后都是每天只可以群发一条消息;
2.服务号:主要偏于服务交互(类似银行,114,提供服务查询),认证前后都是每个月可群发4条消息;
3.企业号:主要用于公司内部通讯使用,需要先有成员的通讯信息验证才可以关注成功企业号;

2.开发准备

1.服务器准备,如果要开发微信公众号需要一个自己的服务器,可以租,也可以使用内网穿透的方法(如果不会的,可以翻我以前的一篇ngrok的使用教程)来进行调试和开发。
2.开发者账号注册,如果要开发公众号需要注册一个开发者账号,小程序和公众号不能使用同一个邮箱地址。
3.开发配置,设置自己的服务器地址和token和密钥。
4.由于个人开发者使用服务号要费用,订阅号有的接口权限非常有限,所以希望能有服务号权限的可以通过测试号来进行接口测试。
(测试号申请地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login)

3.微信平台消息认证

由于不知道发送消息数据是哪一方发出的,为了解决这种情况,微信平台会向服务器发送字段来证明是微信平台发送的数据。详细介绍请看官方开发文档
我使用的是spring boot来开发的。
微信平台会发送四个字段,验证过程需要将timestamp,nonce,token进行字典序排序(就是按照a,b,c这样排序),然后进行sha1加密字符串,最后对比signature与加密后的字符串,如果一样则确定是微信平台发送的数据,那么就需要我们再将echostr原封不动的传回去,这样就验证OK了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void indexGet(HttpServletRequest request, HttpServletResponse response) throws NoSuchAlgorithmException {
String sign = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
if(new SignCheck().check(sign,nonce,timestamp)){
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print(echostr);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("接入成功");
}else {
System.out.println("接入失败");
}
}

SignCheck().check() 是我建的一个SignCheck类的check方法,我是将验证的操作封装成一个工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SignCheck {
private final String token = "abc";

public boolean check(String signature,String nonce,String timestamp){
String[] str = new String[]{token,nonce,timestamp};
Arrays.sort(str);
if (signature.equalsIgnoreCase(new Sha1Util().sha1(str[0].concat(str[1]).concat(str[2])))){
return true;
}else{
return false;
}
}
}

Sha1Util 类是我写的一个加密信息的工具类,封装了加密字符串的操作
MessageDigest 是自带的一个工具类,有不同的加密算法(md5,sha1…)
sha1加密数据后将其转成16进制字符串,所以需要进行对字节处理,8位先转高4位,然后转低4位,将字节与15的字节相与,那么我们就可以得到16进制的字节,转高4位的时候需要将高4位左移4位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Sha1Util {

public String sha1(String content){
StringBuilder str = new StringBuilder();
try {
MessageDigest messageDigest = MessageDigest.getInstance("sha1");
byte[] bytes = messageDigest.digest(content.getBytes());
char[] chars = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
for (byte b : bytes){
str.append(chars[(b>>4)&15]);
str.append(chars[b&15]);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return str.toString();
}
}

4.接受消息,回复消息

认证完成后,我们发送消息微信平台会转发到我们的服务器里。但是用的是post请求,并且是和认证同一个地址
如果用的是spring的同学可以设置RequestMapping里面的method的方法,就可以使用同一个地址不同的请求对应不同的方法体。
首先将接收到的xml消息解析后存到map里去,后面要回复消息时,需要里面双方的唯一id

1
2
3
4
5
6
7
8
9
10
11
12
public void indexPost(HttpServletRequest request, HttpServletResponse response){
try {
request.setCharacterEncoding("utf8");
response.setCharacterEncoding("utf8");
ServletInputStream inputStream = request.getInputStream();
Map<String, String> xmlmap = new Messageutil().getXml(inputStream);
message msg = new TextMessage(xmlmap,"收到消息!");
new Messageutil().setXml(response,msg);
} catch (IOException e) {
e.printStackTrace();
}
}

Messageutil 类是自己封装的工具类,将发送来得xml消息进行解析,需要dom4j,使用maven的同学可以使用以下依赖
由于微信平台接收的xml文件格式必须严格按照要求,所以我们需要对返回的xml字符串进行处理
xStream.processAnnotations(msg.getClass())这个是要加的,否则外面的标签就不会变成

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Messageutil {

public Map<String,String> getXml(InputStream is){
Map<String,String> xmlMap = new HashMap<>();
SAXReader saxReader = new SAXReader();
Document document = null;
try {
document = saxReader.read(is);
Element rootElement = document.getRootElement();
List<Element> elements = rootElement.elements();
for (Element element : elements){
xmlMap.put(element.getName(),element.getStringValue());
}
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println(xmlMap);
return xmlMap;
}

public void setXml(HttpServletResponse response,message msg){
try {
XStream xStream = new XStream();
xStream.processAnnotations(msg.getClass());
String xml = xStream.toXML(msg);
PrintWriter writer = response.getWriter();
writer.print(xml);
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

setXml 是将类转成xml的方法,里面用到的XStream是一个工具类,需要添加依赖

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.3</version>
</dependency>

由于消息有很多形式,如:文字,语言,图片,位置… 所以我们需要写一个基类,让不同的实体类去继承基类
@XStreamAlias(“CreateTime”) 是一个xml的注解,就是将文字里面的内容作为一个标签,包裹着下面的属性内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class message {
@XStreamAlias("CreateTime")
private String createTime;
@XStreamAlias("ToUserName")
private String toUserName;
@XStreamAlias("FromUserName")
private String fromUserName;

public String getCreateTime() {
return createTime;
}

public void setCreateTime(String createTime) {
this.createTime = createTime;
}

public String getToUserName() {
return toUserName;
}

public void setToUserName(String toUserName) {
this.toUserName = toUserName;
}

public String getFromUserName() {
return fromUserName;
}

public void setFromUserName(String fromUserName) {
this.fromUserName = fromUserName;
}

public message(Map<String,String> xmlMap) {
this.createTime = String.valueOf(System.currentTimeMillis()/1000);
this.toUserName = xmlMap.get("FromUserName");
this.fromUserName = xmlMap.get("ToUserName");
}
}

这是一个实体消息类,不同的消息类,属性不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@XStreamAlias("xml")
public class TextMessage extends message {
@XStreamAlias("MsgType")
private final String msgType = "text";
@XStreamAlias("Content")
private String content;

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getMsgType() {
return msgType;
}

public TextMessage(Map<String,String> xmlmap) {
super(xmlmap);
}

public TextMessage(Map<String,String> xmlmap,String content) {
super(xmlmap);
this.content = content;
}
}