网上有很多文章都有讲微信公众号开发的,内容很丰富,从配置公众号到连通URL,再到开发消息回复。所有我这里我就跟他们不一样。我是以一个真实的案例(个人的毕业设计作品)来讲,这系列文章就教教刚学公众号开发的同学怎么在自己的公众号里添加菜单,从菜单里授权自己的网页。先让大家看看我的案例:
1、微信公众号介绍
微信公众号有三种,个人订阅号、企业号、服务号。个人订阅号每个人都可以申请,只是功能比较少,而企业号和服务号功能齐全,但是单位或组织才能申请。我们个人学习只能用个人订阅号,由于有些功能没有,但是自己开发又有需要,所以我们也可以申请一个测试号。测试号是微信官方专门给我们提供开发练习作用的公众号。
2、开始开发
(1)获取access_token。(access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。)参考微信官方文档https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183
这里我来简单总结一下:
a、用AppID和AppSecret调用本接口来获取access_token
b、access_token的有效期未7200,但每天只能获取2000次,所以一般的做法是获取access_token并存起来(例如存在一个文件里,主要把时间也存上去),下次需要access_token时先判断之前的access_token是否有效,有效则继续用,无效则获取新access_token,然后覆盖旧的access_token。
请求的URL:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
数据返回格式:{“access_token”:”ACCESS_TOKEN”,”expires_in”:7200}
代码如下
//封装access_token
public class AccessToken {
private String token;
private int expiresIn;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public int getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(int expiresIn) {
this.expiresIn = expiresIn;
}
}
public class WeixinUtil {
private static final String APPID = "xxxxxxxxxxx";
private static final String APPSECRET = "xxxxxxxxxxxxx";
/*
* get请求
*/
public static JSONObject doGetStr(String url) {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
JSONObject jsonObject = null;
try {
HttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if (null != entity) {
String result = EntityUtils.toString(entity, "UTF-8");
System.out.println("result"+result);
jsonObject = JSONObject.fromObject(result);
jsonObject.put("time", System.currentTimeMillis());
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return jsonObject;
}
/*
* post请求
*/
public static JSONObject doPostStr(String url, String outStr) {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(url);
JSONObject jsonObject = null;
try {
httpPost.setEntity(new StringEntity(outStr, "UTF-8"));
HttpResponse response = httpClient.execute(httpPost);
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
jsonObject = JSONObject.fromObject(result);
jsonObject.put("time", System.currentTimeMillis());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonObject;
}
/*
* 获取access_token
*/
public static AccessToken getAccessToken() {
JSONObject jsonObject=new JSONObject();
String file_path="WebContent/file/access_token.txt";
try {
String str=FileUtil.readFile(file_path);
if(str!=null&&str.trim().length()>0){
jsonObject=JSONObject.fromObject(str);
long oldTime=Long.parseLong(jsonObject.getString("time"));
long nowTime=System.currentTimeMillis();
//这里用7100而不是7200是出于安全性考虑,因为程序的读写文件及其他操作需要一定的时间
if(nowTime-oldTime>7100*1000){
System.out.println("this is a new access_token");
String url = ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
jsonObject = doGetStr(url);
if(jsonObject!=null){
FileUtil.writeFile(file_path, jsonObject.toString());
}
}
}else{
String url = ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
jsonObject = doGetStr(url);
if(jsonObject!=null){
FileUtil.writeFile(file_path, jsonObject.toString());
}
}
AccessToken token = new AccessToken();
if (jsonObject != null) {
token.setToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
return token;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
(2)微信网页授权(如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑)
参考官方文档:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
如果没有授权,那么微信并信任你的网页,情况就有可能是这样子的
所以,授权很重要。而且这个项目的后台我们还用到微信用户的OpenID,所以必须授权。(很多公众号都是借助微信授权登录,通过微信授权可以拿到用户的信息,而不用用户重新去注册的过程,所以这个也是公众号的优势之一啦)
以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
就像这样子的:
而我的案例用的是第一种,所有没有这个页面弹出。
请求URL:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
代码:
private static final String APPID = "xxxxxxxxxxx";
private static final String APPSECRET = "xxxxxxxxxxxxx";
private static final String AUTHORIZE_URL= "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
/*
* 获取网页授权url
*/
public static String getAuthorize_url(){
//表白墙
String REDIRECT_URI=URLEncoder.encode("http://ngroktest.ngrok.cc/GSWX_System/expressWallAction");
String url=AUTHORIZE_URL.replace("APPID", APPID).replace("REDIRECT_URI", REDIRECT_URI).replace("SCOPE", "snsapi_base").replace("STATE", "123");
return url;
}
public static void main(String[] args) {
getAuthorize_url();
}
这里获取的网页授权后的URL只是在菜单哪里用到,然后在进入后台后,还需要一次检查以及获取openID。
action代码:
package action;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.struts2.ServletActionContext;
import org.json.JSONObject;
import com.opensymphony.xwork2.ActionSupport;
public class ExpressWallAction extends ActionSupport {
private static final String APPID = "xxxxxxx";
private static final String APPSECRET = "xxxxxxxx";
private static final String AUTHORIZE_URL="https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
private static final String OAUTH2_CODE_URL="https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
@Override
public String execute() throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
String CODE=request.getParameter("code");
//已经获取到openID,直接进入表白墙后台 if(ServletActionContext.getRequest().getSession().getAttribute("express_wall_openid")!=null){
return SUCCESS;
}
//如果CODE为空,则重新进入授权URL
if(CODE==null){
//你要转向的页面的地址.
String oauth2_url ="https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxx&redirect_uri=http%3A%2F%2Fngroktest.ngrok.cc%2FGSWX_System%2FexpressWallAction&response_type=code&scope=snsapi_base&state=123#wechat_redirect";
//你要转向的页面的地址.
HttpServletResponse response = ServletActionContext.getResponse();
response.sendRedirect(oauth2_url);
//重定向后,别忘了返回null值,而不能再返回return
//mapping.findForward("****");
System.out.println("---------------------------------------------------------------------");
System.out.println("error---------------------------------------------------------------------");
return null;
//获得CODE,通过CODE获取openID,如果获取openID失败则又重新进入授权url
}else{
String oauth2_code_url=OAUTH2_CODE_URL.replace("APPID", APPID).replace("SECRET", APPSECRET).replace("CODE", CODE);
System.out.println(oauth2_code_url);
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(oauth2_code_url);
HttpResponse code_response = httpClient.execute(httpGet);
HttpEntity entity = code_response.getEntity();
if (null != entity) {
String result = EntityUtils.toString(entity, "UTF-8");
System.out.println("result"+result);
org.json.JSONObject json =new JSONObject(result);
if(json.has("openid")){
String express_wall_openid=json.getString("openid");
ServletActionContext.getRequest().getSession().setAttribute("express_wall_openid", express_wall_openid);
}
else{
//你要转向的页面的地址.
String oauth2_url ="https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxx&redirect_uri=http%3A%2F%2Fngroktest.ngrok.cc%2FGSWX_System%2FexpressWallAction&response_type=code&scope=snsapi_base&state=123#wechat_redirect";
//你要转向的页面的地址.
ServletActionContext.getResponse().sendRedirect(oauth2_url);
//重定向后,别忘了返回null值,而不能再返回return
//mapping.findForward("****");
return null;
}
}else{
//你要转向的页面的地址.
String oauth2_url ="https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxx&redirect_uri=http%3A%2F%2Fngroktest.ngrok.cc%2FGSWX_System%2FexpressWallAction&response_type=code&scope=snsapi_base&state=123#wechat_redirect";
//你要转向的页面的地址.
ServletActionContext.getResponse().sendRedirect(oauth2_url);
//重定向后,别忘了返回null值,而不能再返回return
//mapping.findForward("****");
return null;
}
return SUCCESS;
}
}
}
当用户进入授权URL时页面将跳转至 redirect_uri/?code=CODE&state=STATE,所以这里的大概做法是获取CODE,如果CODE为空证明进入授权URL出错(有可能微信服务器未响应之类)则重新进入,否则继续通过CODE获取openID(openID是我们最终需要的,得到openID才能进入表白墙后台)
(3)自定义菜单(用到access_token)。刚才已经把重要的数据获取了。但是,我们怎么进入授权URL呢?这里就需要我们把授权URL放到菜单里啦。自定义菜单的组装和创建参考官方文档:
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013
a、组装菜单
先创建button类,自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。按钮类型有多种,详细请参考文档,而我们这里用到的是clickButton,和viewButton。
代码:
package wx_utils.menu;
public class Button {
private String type;
private String name;
private Button[] sub_button;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Button[] getSub_button() {
return sub_button;
}
public void setSub_button(Button[] sub_button) {
this.sub_button = sub_button;
}
}
package wx_utils.menu;
//继承Button类,
public class ClickButton extends Button{
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
package wx_utils.menu;
//继承Button类
public class ViewButton extends Button{
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
最后封装菜单类
package wx_utils.menu;
public class Menu {
private Button[] button;
public Button[] getButton() {
return button;
}
public void setButton(Button[] button) {
this.button = button;
}
}
现在就开始组装和创建
public class WeixinUtil {
private static final String APPID = "xxxxxxxxxxxx";
private static final String APPSECRET = "xxxxxxxxxxx";
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
private static final String CREATE_MENU_URL="https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
private static final String QUERY_MENU_URL="https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN";
private static final String DELETE_MENU_URL="https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN";
private static final String AUTHORIZE_URL="https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
/*
* get请求
*/
public static JSONObject doGetStr(String url) {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
JSONObject jsonObject = null;
try {
HttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if (null != entity) {
String result = EntityUtils.toString(entity, "UTF-8");
System.out.println("result"+result);
jsonObject = JSONObject.fromObject(result);
jsonObject.put("time", System.currentTimeMillis());
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return jsonObject;
}
/*
* post请求
*/
public static JSONObject doPostStr(String url, String outStr) {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(url);
JSONObject jsonObject = null;
try {
httpPost.setEntity(new StringEntity(outStr, "UTF-8"));
HttpResponse response = httpClient.execute(httpPost);
String result = EntityUtils.toString(response.getEntity(), "UTF-8");
jsonObject = JSONObject.fromObject(result);
jsonObject.put("time", System.currentTimeMillis());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonObject;
}
/*
* 获取access_token
*/
public static AccessToken getAccessToken() {
JSONObject jsonObject=new JSONObject();
String file_path="WebContent/file/access_token.txt";
try {
String str=FileUtil.readFile(file_path);
if(str!=null&&str.trim().length()>0){
jsonObject=JSONObject.fromObject(str);
long oldTime=Long.parseLong(jsonObject.getString("time"));
long nowTime=System.currentTimeMillis();
if(nowTime-oldTime>7100*1000){
System.out.println("this is a new access_token");
String url = ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
jsonObject = doGetStr(url);
if(jsonObject!=null){
FileUtil.writeFile(file_path, jsonObject.toString());
}
}
}else{
String url = ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
jsonObject = doGetStr(url);
if(jsonObject!=null){
FileUtil.writeFile(file_path, jsonObject.toString());
}
}
AccessToken token = new AccessToken();
if (jsonObject != null) {
token.setToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
return token;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/**
* 组装菜单
* @return
*/
public static Menu initMenu(){
Menu menu=new Menu();
String url11="http://baike.baidu.com/link?url=1sVwJxNjWc_6kmbOUMsUStWIAo9hEU_oRf-lS-paOmkwVRUNDrsf7RmyLKrxtg2_bYRrxmQ2d-inlthby0noxgRAnRvNbJsKyy_NGQZuq3q0wv-jPVZq2efjdoyTUnrdTWnUCDYtmIZZYqM5HWJZZhBE_WnFH8AvFi-9Yl-O1yxXYXo7JUBwscnc2mNGL8le";
ViewButton btn11=new ViewButton();
btn11.setName("校园介绍");
btn11.setType("view");
btn11.setUrl(url11);
String url12="http://map.baidu.com/?newmap=1&s=con%26wd%3D广东技术师范学院%26c%3D257&from=alamap&tpl=mapdots";
ViewButton btn12=new ViewButton();
btn12.setName("校园地图");
btn12.setType("view");
btn12.setUrl(url12);
String url13="http://www.gpnu.edu.cn/index/tzgg.htm";
ViewButton btn13=new ViewButton();
btn13.setName("通知公告");
btn13.setType("view");
btn13.setUrl(url13);
Button button1 =new Button();
button1.setName("校园生活");
button1.setSub_button(new Button[]{btn11,btn12,btn13});
String url21="https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxx&redirect_uri=http%3A%2F%2Fngroktest.ngrok.cc%2FGSWX_System%2FexpressWallAction&response_type=code&scope=snsapi_base&state=123#wechat_redirect";
ViewButton btn21=new ViewButton();
btn21.setName("表白墙");
btn21.setType("view");
btn21.setUrl(url21);
Button button2 =new Button();
button2.setName("广师论坛");
button2.setSub_button(new Button[]{btn21});
///
String url31="https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxx&redirect_uri=http%3A%2F%2Fngroktest.ngrok.cc%2FGSWX_System%2FcampusCardRechargeAction&response_type=code&scope=snsapi_userinfo&state=123#wechat_redirect";
ViewButton btn31=new ViewButton();
btn31.setName("校园卡充值");
btn31.setType("view");
btn31.setUrl(url31);
String url32="http://h5.gmccopen.com/act/recharge!index.action?channelId=gzweidian&type=08&storeid=C17679";
ViewButton btn32=new ViewButton();
btn32.setName("移动话费");
btn32.setType("view");
btn32.setUrl(url32);
String url33="http://h5.gmccopen.com/h5/flowq!index.action?channelId=ydhl0000&storeid=C17679";
ViewButton btn33=new ViewButton();
btn33.setName("移动流量");
btn33.setType("view");
btn33.setUrl(url33);
String url34="http://zqhd.chainew.com/flowq/weixinPay/order/findGdltProductSizeList?code=003epMSg0FnCGB1xUtQg0nwKSg0epMSj&state=STATE";
ViewButton btn34=new ViewButton();
btn34.setName("联通流量");
btn34.setType("view");
btn34.setUrl(url34);
String url35="http://qq.75510010.com/recharge/init.action?showwxpaytitle=1";
ViewButton btn35=new ViewButton();
btn35.setName("联通话费");
btn35.setType("view");
btn35.setUrl(url35);
Button button3 =new Button();
button3.setName("便利充值");
button3.setSub_button(new Button[]{btn31,btn32,btn33,btn34,btn35});
menu.setButton(new Button[]{button1,button2,button3});
return menu;
}
/**
* 创建自定义菜单
* @param token
* @param menu
* @return
*/
public static JSONObject createMenu(String token,String menu){
String url=CREATE_MENU_URL.replace("ACCESS_TOKEN", token);
JSONObject jsonObject=doPostStr(url, menu);
if(jsonObject!=null){
return jsonObject;
}
return null;
}
/**
* 查询自定义菜单
* @param token
* @return
*/
public static JSONObject queryMenu(String token){
String url=QUERY_MENU_URL.replace("ACCESS_TOKEN", token);
JSONObject jsonObject=doGetStr(url);
return jsonObject;
}
/**
* 删除自定义菜单
* @param token
* @return
*/
public static JSONObject deleteMenu(String token){
String url=DELETE_MENU_URL.replace("ACCESS_TOKEN", token);
JSONObject jsonObject=doGetStr(url);
if(jsonObject!=null){
return jsonObject;
}
return null;
}
public static void main(String[] args) {
AccessToken access_token=WeixinUtil.getAccessToken();
if(access_token!=null){
System.out.println("票据:"+access_token.getToken());
System.out.println("有效时间:"+access_token.getExpiresIn());
}else{
System.out.println("null");
}
//自定义菜单创建
String menu=JSONObject.fromObject(WeixinUtil.initMenu()).toString();
System.out.println(menu);
JSONObject jsonObject=WeixinUtil.createMenu(access_token.getToken(), menu);
if(jsonObject==null){
System.out.println("创建失败");
}else {
System.out.println(jsonObject.toString());
}
}
好了,项目到这里基本结束了。当然我这里是没有讲怎么做后台,因为后台其实就是一个网站啊,自己做个小网站就可以啦。我做的那个是一个表白墙的小网站,需要注意的是网站要有域名,端口80或者443(不是需要安全性特别高的个人网站就用80好了,毕竟443还有很多要配置,一步一步来嘛),外网能访问。下一篇我就简单得介绍一个外网映射工具吧,如果有服务器有域名的朋友就不用看啦。