一、前言
最近一段时间都在进行公众号的开发,之前在实训的时候也开发过一次,但是过得太久早就忘记了如何做了,这次开发也是相当于重新开始,其实开发的步骤微信公众号开发文档上面写的也很清楚,所以总体来说开发的步骤也不是特别复杂。只要配置好需要的URL、TOKEN、和回调的域名就可以了。
二、开发过程
(1)配置服务器信息
在测试上面有两个重要信息。分别是appID 和 appsecret。这是开启公众号两个重要秘钥,一个公众号对应一个appID。有了这两个东西你就可以为所欲为为所欲为猥琐欲为了。
然后配置你的URL地址和token,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。
URL作为开发接口,主要告诉公众号,这是属于自己的服务器,以后公众号接收到的信息都会给我的服务器。配置这个信息时,公众号会给这个接口发送一个GET请求,并要接受四个参数
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。
此代码段是开放给公众号的接口。在这里接受GET请求的四个参数。
package com.itlink.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Calendar;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.internal.runners.TestMethod;
import com.itlink.domain.TextMessage;
import com.itlink.utils.MessageUtil;
import com.itlink.utils.SignUtil;
public class wx extends HttpServlet {
//private static final Logger logger = Logger.get
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
if(echostr != null && SignUtil.checkSignature(signature, timestamp, nonce)){
System.out.println("[signature: "+signature + "]<-->[timestamp: "+ timestamp+"]<-->[nonce: "+nonce+"]<-->[echostr: "+echostr+"]");
response.getOutputStream().println(echostr);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}
加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
package com.itlink.utils;
import java.security.MessageDigest;
import java.util.Arrays;
public class SignUtil {
private static String token = "activty";
public static boolean checkSignature(String signature, String timestamp,
String nonce) {
String checktext = null;
String[] params = new String[]{token, timestamp, nonce};
Arrays.sort(params);
String content = params[0].concat(params[1]).concat(params[2]);
try{
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(content.toString().getBytes());
checktext = bytetostr(digest);
}catch(Exception e){
e.printStackTrace();
}
return checktext !=null ? checktext.equals(signature.toUpperCase()) : false;
}
private static String bytetostr(byte[] digest) {
String str = "";
for (int i = 0; i < digest.length; i++) {
str += byteToHexStr(digest[i]);
}
return str;
}
private static String byteToHexStr(byte myByte) {
char[] Digit = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char[] tampArr = new char[2];
tampArr[0] = Digit[(myByte >>> 4) & 0X0F];
tampArr[1] = Digit[myByte & 0X0F];
String str = new String(tampArr);
return str;
}
}
此时提交的配置信息就会成功。(这是真的,如果还是不行的话,那是不存在的)
(2)获取网页权限。
打开第三方网页时需要获取到用户信息,就需要用户授权。所有操作的权限是通过微信公众号开放的接口获取的。
在微信公众号文档中也明确提到。在用户授权之后进行网页跳转时需要配置用户的回调地址。这是一个关键信息。如果没有的话,在网页跳转的时候就会报错,相当于找不到回家的路。
关于网页授权的两种scope的区别说明
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。这种方式不用用户手动授权。
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
3、用户管理类接口中的“获取用户基本信息接口”,是在用户和公众号产生消息交互或关注后事件推送后,才能根据用户OpenID来获取用户基本信息。这个接口,包括其他微信接口,都是需要该用户(即openid)关注了公众号后,才能调用成功的。
关于网页授权access_token和普通access_token的区别
微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息;其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用。
获取用户信息的基本步骤:
1、引导用户进入授权页面同意授权,获取code
2、通过code换取网页授权access_token
3、如果需要,开发者可以刷新网页授权access_token,避免过期
4、通过网页授权access_token和openid获取用户基本信息
跳转
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
REDIRECT_URI:是用户需要跳转的路径(我的代码中是一个servlet)
通过一个回调的路径跳转到一个页面并获取到code。
//String code = request.getParameter("code");//从授权页面获取到code,用于回去用户的openid
//code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
//通过code获取到token_access
//AccessToken access_token = WeixinUtil.getAccessToken(WeixinUtil.appid, WeixinUtil.appsecret);
//JSONObject json1 = WeixinUtil.getOAuthAccessToken(WeixinUtil.appid, WeixinUtil.appsecret, code);
String access_token = dao.getAccessToken().getAccess_token();
response.sendRedirect("activity/index.html?user="+openid);
// JSONObject user_json = WeixinUtil.getUserinfo(access_token, openid);
微信工具类。
package com.itlink.utils;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import org.junit.Test;
import com.itlink.domain.AccessToken;
import com.itlink.domain.Menu;
import net.sf.json.JSONObject;
public class WeixinUtil {
public static String appid = "";//自己公众号的APPID和appsecret
public static String appsecret = "";
//处理请求
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
try {
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect();
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
// log.info("Weixin server connection timed out.");
} catch (Exception e) {
// log.info("https request error:"+e);
}
return jsonObject;
}
//获取access_token接口
public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
public static AccessToken getAccessToken(String appid, String appsecret){
String url = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
JSONObject jsonObject = httpsRequest(url, "GET", null);
AccessToken accessToken = new AccessToken();
accessToken.setExpires_in((Integer)jsonObject.get("expires_in"));
accessToken.setAccess_token(jsonObject.getString("access_token"));
return accessToken;
}
//获取用户基本信息(UnionID机制)
public final static String userinfo_url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
public static JSONObject getUserinfo(String access_token,String openid){
String url = userinfo_url.replace("ACCESS_TOKEN", access_token).replace("OPENID", openid);
JSONObject jsonObject = httpsRequest(url, "GET", null);
return jsonObject;
}
//获取用户权限
public final static String access_token_oauth_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
public static JSONObject getOAuthAccessToken(String appid,String appsecret,String code){
String url = access_token_oauth_url.replace("APPID", appid).replace("SECRET", appsecret).replace("CODE", code);
JSONObject jsonObject = httpsRequest(url, "GET", null);
return jsonObject;
}
//拉取用户信息
public final static String user_info_oauth_url = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
public static JSONObject getUserInfoByOAuth(String access_token,String openid){
String url = user_info_oauth_url.replace("ACCESS_TOKEN", access_token).replace("OPENID", openid);
JSONObject jsonObject = httpsRequest(url, "GET", null);
return jsonObject;
}
}
MyX509TrustManager类证书信任管理器(用于https请求)
package com.itlink.utils;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class MyX509TrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
网页授权方式基本上就可以获取到用户的信息了。
(三)开发过程中遇到的坑
(1)遇到access_token过期。在开发过程中过一点时间就会无法访问到自己的网站,后面才发现微信授权调用的凭票过一段时间就会过去,就需要重新获取。access_token保存的时长是7200s,也就是两个小时,所以需要经过段时间就要去重新获取access_token。这个时间就需要做一个定时器定时的去执行任务。并将凭票保存起来,可以放在数据库中,也可以用一个文件保存起来,每次需要用到的时候就去获取。
package com.itlink.domain;
public class AccessToken {
private String access_token;
private int expires_in;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
@Override
public String toString() {
return "AccessToken [access_token=" + access_token + ", expires_in="
+ expires_in + "]";
}
}
将access_token保存起来
package com.itlink.dao;
import org.dom4j.Document;
import org.dom4j.Element;
import org.junit.Test;
import com.itlink.domain.AccessToken;
import com.itlink.utils.WeixinUtil;
import com.itlink.utils.dom4jUtil;
public class AccessTokenDao {
AccessToken accessToken = null;
public AccessToken getAccessToken(){
//获取保存的信息
}
public void setAccessToken(AccessToken accesstoken){
//this.accessToken = new AccessToken();
//保存信息
}
}
开启一个线程,每隔一段时间就获取信息。
package com.itlink.main;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import com.itlink.dao.AccessTokenDao;
import com.itlink.domain.AccessToken;
import com.itlink.utils.WeixinUtil;
public class TokenThread implements Runnable {
public static AccessToken accessToken = null;
@Override
public void run() {
accessToken = WeixinUtil.getAccessToken(WeixinUtil.appid, WeixinUtil.appsecret);
System.out.println("开始获取");
try {
if(accessToken != null){
System.out.println("accessToken获取成功:"+ accessToken.getExpires_in());
//将access——token存放起来
new AccessTokenDao().setAccessToken(accessToken);
//7000秒后重新获取
Thread.sleep((accessToken.getExpires_in()-200)*1000);
}else{
System.out.println("获取失败");
Thread.sleep(60*1000);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(2)启用开发者模式后,原先定义的菜单栏会被停用。要使用自定义菜单只能通过公众号给你提供的接口设置。
public final static String create_menu_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
public static Integer createMenu(String access_token, Menu menu){
String url = create_menu_url.replace("ACCESS_TOKEN", access_token);
String outputStr = JSONObject.fromObject(menu).toString();
System.out.println(outputStr);
JSONObject jsonObject = httpsRequest(url, "POST", outputStr);
Integer result = null;
if(null!=jsonObject){
result = jsonObject.getInt("errcode");
}
return result;
}
自己定义一个Menu类,将你需要的定义的菜单以json类型的格式以post请求的方式传递过去。
规定格式:
{
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"miniprogram",
"name":"wxa",
"url":"http://mp.weixin.qq.com",
"appid":"wx286b93c14bbf93aa",
"pagepath":"pages/lunar/index"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}]
}]
}
(四)最后
基本配置的格式就差不多是这样了。这个项目是在测试号上面进行开发的,测试号上面所有的权限都提供给了开发者,在实际开发中不同类型的公众号的权限是不一样的,所以在开发时候还是需要根据实际情况进行业务的调整。