一、前言
最近一段时间都在进行公众号的开发,之前在实训的时候也开发过一次,但是过得太久早就忘记了如何做了,这次开发也是相当于重新开始,其实开发的步骤微信公众号开发文档上面写的也很清楚,所以总体来说开发的步骤也不是特别复杂。只要配置好需要的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" }] }] }
(四)最后
基本配置的格式就差不多是这样了。这个项目是在测试号上面进行开发的,测试号上面所有的权限都提供给了开发者,在实际开发中不同类型的公众号的权限是不一样的,所以在开发时候还是需要根据实际情况进行业务的调整。