前回の記事「WeChat開発 – WeChat開発環境構築」でWeChat開発の準備作業が完了しましたので、準備作業が完了したらいよいよ作業に入ります。
1. WeChat パブリック プラットフォームの基本原則
始める前に、WeChat パブリック プラットフォームの基本原則を簡単に紹介しましょう。
WeChatサーバーは転送サーバーに相当します。端末(携帯電話、Padなど)はWeChatサーバーへのリクエストを開始し、WeChatサーバーはそのリクエストをアプリケーションサーバーに転送します。アプリケーション サーバーは処理を完了した後、応答データを WeChat サーバーに送り返し、WeChat サーバーは特定の応答情報を WeChat アプリ端末に返信します。
通信プロトコルはHTTPです
データ送信形式はXMLです
具体的なプロセスは以下の図に示されています:

より直感的に見てみましょう:

必要なことこれは、WeChat サーバーによって転送された HTTP リクエストに応答することです。特定のリクエストのコンテンツを特定の XML 形式に従って解析し、処理後、それを特定の XML 形式に従って返す必要があります。
2. WeChat パブリック アカウントへのアクセス
WeChat パブリック プラットフォーム開発者ドキュメントのアクセス ガイドに、パブリック アカウントへのアクセスに関するセクションが詳しく記載されています。その手順は次のとおりです。 :
1. サーバー構成を入力します
2. サーバーアドレスの有効性を検証します
3. インターフェースドキュメントに基づいてビジネスロジックを実装します
実際、3 番目のステップは、もはや接続のステップとみなすことができません公開アカウントですが、アクセス後、開発者は WeChat 公式アカウントが提供するインターフェースに基づいて開発を行うことができます。
ステップ1のサーバー設定には、サーバーアドレス(URL)、トークン、EncodingAESKeyが含まれます。
サーバーアドレスは、公式アカウントのバックエンドが提供するビジネスロジックのエントリアドレスです。現在、ポート80のみをサポートしています。将来的には、アクセス認証やその他の操作(メッセージ送信、メニュー管理など)を含むリクエストもサポートされます。 、素材管理など)は必ずこちらから アドレスを入力してください。アクセス検証と他のリクエストの違いは、アクセス検証が get リクエストである場合と、post リクエストである場合があります
トークンは開発者が任意に入力でき、署名の生成に使用されます (トークンはインターフェイス URL に含まれるトークン セキュリティを検証するための比較);
EncodingAESKey は開発者によって手動で入力されるか、ランダムに生成され、メッセージ本文の暗号化キーと復号化キーとして使用されます。 この例では、すべてのメッセージは暗号化されていないプレーン テキストであり、この構成項目は関係しません。
ステップ 2、サーバー アドレスの有効性を確認します。「送信」ボタンをクリックすると、WeChat サーバーは、入力したサーバー アドレスに http get リクエストを送信し、次の 4 つのパラメーターを送信します。
リクエストを受信後、以下の3ステップを行う必要があり、GETリクエストがWeChatサーバーから来ていることが確認され、echostrパラメータの内容がそのまま返された場合はアクセスが有効となり、それ以外の場合はアクセスが無効になります。失敗。
1. 3つのパラメータのトークン、タイムスタンプ、nonceを辞書編集順に並べ替えます 2. 3つのパラメータ文字列をsha1暗号化用の1つの文字列に結合します
3. 開発者は暗号化された文字列と署名を比較できます。 WeChat より
Java コードを使用してこの検証プロセスをデモしましょう
IDE (Eclipse または IntelliJ IDEA) を使用して JavaWeb プロジェクトを作成します。 ここでは IntelliJ IDEA を使用します。 プロジェクトのディレクトリ構造は次のとおりです。 servlevt を取得し、doGet メソッドで検証メソッドを定義します。具体的なコードは次のとおりです。
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | package me.gacl.wx.web.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@WebServlet(urlPatterns= "/WxServlet" )
public class WxServlet extends HttpServlet {
private final String TOKEN = "gacl" ;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println( "开始校验签名" );
String signature = request.getParameter( "signature" );
String timestamp = request.getParameter( "timestamp" );
String nonce = request.getParameter( "nonce" );
String echostr = request.getParameter( "echostr" );
String sortString = sort(TOKEN, timestamp, nonce);
String mySignature = sha1(sortString);
if (mySignature != null && mySignature != "" && mySignature.equals(signature)) {
System.out.println( "签名校验通过。" );
response.getWriter().write(echostr);
} else {
System.out.println( "签名校验失败." );
}
}
public String sort(String token, String timestamp, String nonce) {
String[] strArray = {token, timestamp, nonce};
Arrays.sort(strArray);
StringBuilder sb = new StringBuilder();
for (String str : strArray) {
sb.append(str);
}
return sb.toString();
}
public String sha1(String str) {
try {
MessageDigest digest = MessageDigest.getInstance( "SHA-1" );
digest.update(str.getBytes());
byte messageDigest[] = digest.digest();
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "" ;
}
}
|
ログイン後にコピー
ここで使用する Servlet3.0 を使用する利点は、
@WebServlet
アノテーション Mapping を直接使用できることです。サーブレットのアクセス パスを Web で設定する必要はなくなりました。ブログ「WeChat 開発 - WeChat 開発環境の構築」を参照してください。下の写真に示すように:

测试是否可以通过http://xdp.ngrok.natapp.cn地址正常访问,测试结果如下:

可以看到,我们的项目已经可以被外网正常访问到了。
进入微信测试公众号管理界面,在接口配置信息中填入映射的外网地址和token,如下图所示:

点击提交按钮,页面会提示配置成功,

IDE的控制台中输出了校验通过的信息,如下图所示:

到此,我们的公众号应用已经能够和微信服务器正常通信了,也就是说我们的公众号已经接入到微信公众平台了。
三、access_token管理
3.1、access_token介绍
我们的公众号和微信服务器对接成功之后,接下来要做的就是根据我们的业务需求调用微信公众号提供的接口来实现相应的逻辑了。在使用微信公众号接口中都需要一个access_token。
关于access_token,在微信公众平台开发者文档上的获取接口调用凭据有比较详细的介绍:access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token,开发者需要妥善保存access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。并且每天调用获取access_token接口的上限是2000次。
总结以上说明,access_token需要做到以下两点:
1.因为access_token有2个小时的时效性,要有一个机制保证最长2个小时重新获取一次。
2.因为接口调用上限每天2000次,所以不能调用太频繁。
3.2、微信公众平台提供的获取access_token的接口
关于access_token的获取方式,在微信公众平台开发者文档上有说明,公众号可以调用一个叫"获取access token"的接口来获取access_token。
获取access token接口调用请求说明
http请求方式: GET
请求的URL地址:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

我们可以看到,调用过程中需要传递appID和AppSecret,appID和AppSecret是在申请公众号的时候自动分配给公众号的,相当于公众号的身份标示,使用微信公众号的注册帐号登录到腾讯提供的微信公众号管理后台就可以看到自己申请的公众号的AppID和AppSecret,如下图所示:

这是我申请公众号测试帐号时分配到的AppID和AppSecret。
3.3、获取access_token方案以及具体实现
这里采用的方案是这样的,定义一个默认启动的servlet,在init方法中启动一个Thread,这个进程中定义一个无限循环的方法,用来获取access_token,当获取成功后,此进程休眠7000秒(7000秒=1.944444444444444小时),否则休眠3秒钟继续获取。流程图如下:

下面正式开始在工程中实现以上思路,因为返回的数据都是json格式,这里会用到阿里的fastjson库,为构造请求和处理请求后的数据序列化和反序列化提供支持。
1.定义一个AccessToken实体类
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 | package me.gacl.wx.entry;
public class AccessToken {
private String accessToken;
private int expiresin;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public int getExpiresin() {
return expiresin;
}
public void setExpiresin(int expiresin) {
this.expiresin = expiresin;
}
}
|
ログイン後にコピー
2.定义一个AccessTokenInfo类,用于存放获取到的AccessToken,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | package me.gacl.wx.Common;
import me.gacl.wx.entry.AccessToken;
public class AccessTokenInfo {
public static AccessToken accessToken = null;
}
|
ログイン後にコピー
3.编写一个用于发起https请求的工具类NetWorkHelper,代码如下:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | package me.gacl.wx.util;
import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class NetWorkHelper {
public String getHttpsResponse(String reqUrl, String requestMethod) {
URL url;
InputStream is;
String resultData = "" ;
try {
url = new URL(reqUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
TrustManager[] tm = {xtm};
SSLContext ctx = SSLContext.getInstance( "TLS" );
ctx.init(null, tm, null);
con.setSSLSocketFactory(ctx.getSocketFactory());
con.setHostnameVerifier( new HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
});
con.setDoInput(true);
con.setDoOutput(false);
con.setUseCaches(false);
if (null != requestMethod && !requestMethod.equals( "" )) {
con.setRequestMethod(requestMethod);
} else {
con.setRequestMethod( "GET" );
}
is = con.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader bufferReader = new BufferedReader(isr);
String inputLine;
while ((inputLine = bufferReader.readLine()) != null) {
resultData += inputLine + "\n" ;
}
System.out.println(resultData);
} catch (Exception e) {
e.printStackTrace();
}
return resultData;
}
X509TrustManager xtm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
};
}
|
ログイン後にコピー
getHttpsResponse方法是请求一个https地址,参数requestMethod为字符串“GET”或者“POST”,传null或者“”默认为get方式。
4.定义一个默认启动的servlet,在init方法中启动一个新的线程去获取accessToken
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | package me.gacl.wx.web.servlet;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import me.gacl.wx.Common.AccessTokenInfo;
import me.gacl.wx.entry.AccessToken;
import me.gacl.wx.util.NetWorkHelper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
@WebServlet(
name = "AccessTokenServlet" ,
urlPatterns = { "/AccessTokenServlet" },
loadOnStartup = 1,
initParams = {
@WebInitParam(name = "appId" , value = "wxbe4d433e857e8bb1" ),
@WebInitParam(name = "appSecret" , value = "ccbc82d560876711027b3d43a6f2ebda" )
})
public class AccessTokenServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println( "启动WebServlet" );
super.init();
final String appId = getInitParameter( "appId" );
final String appSecret = getInitParameter( "appSecret" );
new Thread( new Runnable() {
@Override
public void run() {
while (true) {
try {
AccessTokenInfo.accessToken = getAccessToken(appId, appSecret);
if (AccessTokenInfo.accessToken != null) {
Thread.sleep(7000 * 1000);
} else {
Thread.sleep(1000 * 3);
}
} catch (Exception e) {
System.out.println( "发生异常:" + e.getMessage());
e.printStackTrace();
try {
Thread.sleep(1000 * 10);
} catch (Exception e1) {
}
}
}
}
}).start();
}
private AccessToken getAccessToken(String appId, String appSecret) {
NetWorkHelper netHelper = new NetWorkHelper();
String Url = String.format( "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" , appId, appSecret);
String result = netHelper.getHttpsResponse(Url, "" );
System.out.println( "获取到的access_token=" +result);
JSONObject json = JSON.parseObject(result);
AccessToken token = new AccessToken();
token.setAccessToken(json.getString( "access_token" ));
token.setExpiresin(json.getInteger( "expires_in" ));
return token;
}
}
|
ログイン後にコピー
AccessTokenServlet采用注解的方式进行配置
至此代码实现完毕,将项目部署,看到控制台输出如下:

为了方便看效果,可以把休眠时间设置短一点,比如10秒获取一次,然后将access_token输出。
下面做一个测试jsp页面,并把休眠时间设置为10秒,这样过10秒刷新页面,就可以看到变化
1 2 3 4 5 6 7 8 9 10 11 12 13 | <%-- Created by IntelliJ IDEA. --%>
<%@ page contentType= "text/html;charset=UTF-8" language= "java" %>
<%@ page import= "me.gacl.wx.Common.AccessTokenInfo" %>
<html>
<head>
<title></title>
</head>
<body>
微信学习
<hr/>
access_token为:<%=AccessTokenInfo.accessToken.getAccessToken()%>
</body>
</html>
|
ログイン後にコピー
10秒钟后刷新页面,access_token变了,如下图所示:

四、接收微信服务器发送的消息并做出响应
经过上述的三步,我们开发前的准备工作已经完成了,接下来要做的就是接收微信服务器发送的消息并做出响应
从微信公众平台接口消息指南中可以了解到,当用户向公众帐号发消息时,微信服务器会将消息通过POST方式提交给我们在接口配置信息中填写的URL,而我们就需要在URL所指向的请求处理类WxServlet的doPost方法中接收消息、处理消息和响应消息。
4.1.编写一个用于处理消息的工具类
编写处理消息的工具栏,工具类代码如下:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | package me.gacl.wx.util;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util. Date ;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MessageHandlerUtil {
public static Map<String,String> parseXml(HttpServletRequest request) throws Exception {
Map<String,String> map = new HashMap();
InputStream inputStream = request.getInputStream();
System.out.println( "获取输入流" );
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
Element root = document.getRootElement();
List<Element> elementList = root.elements();
for (Element e : elementList) {
System.out.println(e.getName() + "|" + e. getText ());
map.put(e.getName(), e. getText ());
}
inputStream.close();
inputStream = null;
return map;
}
public static String buildXml(Map<String,String> map) {
String result;
String msgType = map.get( "MsgType" ).toString();
System.out.println( "MsgType:" + msgType);
if (msgType.toUpperCase().equals( "TEXT" )){
result = buildTextMessage(map, "孤傲苍狼在学习和总结微信开发了,构建一条文本消息:Hello World!" );
} else {
String fromUserName = map.get( "FromUserName" );
String toUserName = map.get( "ToUserName" );
result = String
.format(
"<xml>" +
"<ToUserName><![CDATA[%s]]></ToUserName>" +
"<FromUserName><![CDATA[%s]]></FromUserName>" +
"<CreateTime>%s</CreateTime>" +
"<MsgType><![CDATA[text]]></MsgType>" +
"<Content><![CDATA[%s]]></Content>" +
"</xml>" ,
fromUserName, toUserName, getUtcTime(),
"请回复如下关键词:\n文本\n图片\n语音\n视频\n音乐\n图文" );
}
return result;
}
private static String buildTextMessage(Map<String,String> map, String content) {
String fromUserName = map.get( "FromUserName" );
String toUserName = map.get( "ToUserName" );
return String.format(
"<xml>" +
"<ToUserName><![CDATA[%s]]></ToUserName>" +
"<FromUserName><![CDATA[%s]]></FromUserName>" +
"<CreateTime>%s</CreateTime>" +
"<MsgType><![CDATA[text]]></MsgType>" +
"<Content><![CDATA[%s]]></Content>" + "</xml>" ,
fromUserName, toUserName, getUtcTime(), content);
}
private static String getUtcTime() {
Date dt = new Date ();
DateFormat df = new SimpleDateFormat( "yyyyMMddhhmm" );
String nowTime = df.format(dt);
long dd = (long) 0;
try {
dd = df.parse(nowTime).getTime();
} catch (Exception e) {
}
return String.valueOf(dd);
}
}
|
ログイン後にコピー
为了方便解析微信服务器发送给我们的xml格式的数据,这里我们借助于开源框架dom4j去解析xml(这里使用的是dom4j-2.0.0-RC1.jar)
4.2.在WxServlet的doPost方法中处理请求
WxServlet的doPost方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding( "UTF-8" );
response.setCharacterEncoding( "UTF-8" );
System.out.println( "请求进入" );
String result = "" ;
try {
Map<String,String> map = MessageHandlerUtil.parseXml(request);
System.out.println( "开始构造消息" );
result = MessageHandlerUtil.buildXml(map);
System.out.println(result);
if (result.equals( "" )){
result = "未正确响应" ;
}
} catch (Exception e) {
e.printStackTrace();
System.out.println( "发生异常:" + e.getMessage());
}
response.getWriter().println(result);
}
|
ログイン後にコピー
到此,我们的WxServlet已经可以正常处理用户的请求并做出响应了.接下来我们测试一下我们开发好的公众号应用是否可以正常和微信用户交互
将WxStudy部署到Tomcat服务器,启动服务器,记得使用ngrok将本地Tomcat服务器的8080端口映射到外网,保证接口配置信息的URL地址:http://xdp.ngrok.natapp.cn/WxServlet可以正常与微信服务器通信
登录到我们的测试公众号的管理后台,然后用微信扫描一下测试号的二维码,如下图所示:

关注成功后,我们开发好的公众号应用会先给用户发一条提示用户操作的文本消息,微信用户根据提示操作输入"文本",我们的公众号应用接收到用户请求后就给用户回复了一条我们自己构建好的文本消息,如下图所示:
我们的公众号应用响应给微信用户的文本消息的XML数据如下:
1 2 3 4 5 6 7 | <xml>
<ToUserName><![CDATA[ojADgs0eDaqh7XkTM9GvDmdYPoDw]]></ToUserName>
<FromUserName><![CDATA[gh_43df3882c452]]></FromUserName>
<CreateTime>1453755900000</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[孤傲苍狼在学习和总结微信开发了,构建一条文本消息:Hello World!]]></Content>
</xml>
|
ログイン後にコピー
测试公众号的管理后台也可以看到关注测试号的用户列表,如下图所示:
通过这个简单的入门程序,我们揭开了微信开发的神秘面纱了.
更多WeChat開発の入門学習のまとめ相关文章请关注PHP中文网!