微信公众平台,简称weixinMP, 微信公众平台发布以前叫媒体平台,提供给合作方与用户互动,MP是media platform的简写。
说难也难,说容易也容易,看微信接入文档,会让人一头雾水,蒙逼的感觉,因为官方文档都是晦涩难懂的,显的逼格很高,下面用普通语言走一遍,让我们开始微信接入之旅吧。
1. 首先,微信服务器使用的是必需是80端口,而我们常常使用的是tomcat是8080端口,当然我们可以修改端口,而本机的80端口被浏览器占用,是不可能把tomcat改在80端口的,而且微信服务器会向我们自己的服务器以验证请求,需要内网穿透,所以大家可能看看我写的ngrok内网穿透的文章:微信开发-ngrok内网穿透部署
2. 开发者未必有自己的微信公众号,微信官方考虑好这一点,所以提供了微信测试号,在测试号上有些功能限制,但接入以及走完部分流程是没问题的,首先当然是申请测试号了,百度“微信 测试号”或打开https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
用我信自己的微信号扫一扫后在微信界面点确认登录。
其中appID,appsecret是我们微信公众号接入的关键信息。
其中下方需要我们配置自己服务器的URl和JS接口安全域名。那URL填写注意:
1. 必需是80端口
2. 是自己的服务器的地址,其中该地址是微信服务器向我们自己服务器发送的数据统一入口,每次微信服务器给我们服务器推送消息时都推送到该url地址,根据消息类型的事件类型选择不同的handler去处理不同推送消息。稍后会写个统一入口,这里暂不写
js域名是我们服务器域名,注意不带http://等协议头。授权回调域名配置规范为全域名,意思是,比如我现在填chenyuanx.tunnel.2bdata.com,那么该域名下的所以请求都可以进行OAuth2.0授权。
3. 写个自己的服务器接收微信服务器推送消息的统一入口。
代码如下:
/**
* 微信公众号webservice主服务接口,提供与微信服务器的信息交互
*
* @param request
* @param response
* @throws Exception
*/
@RequestMapping(value = "protal")
public void wechatCore(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
String signature = request.getParameter("signature");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
// 消息签名不正确,说明不是公众平台发过来的消息
response.getWriter().println("非法请求");
return;
}
String echoStr = request.getParameter("echostr");
if (StringUtils.isNotBlank(echoStr)) {
// 说明是一个仅仅用来验证的请求,回显echostr
String echoStrOut = String.copyValueOf(echoStr.toCharArray());
response.getWriter().println(echoStrOut);
return;
}
String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ? "raw"
: request.getParameter("encrypt_type");
if ("raw".equals(encryptType)) {
// 明文传输的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(request.getInputStream());
// 微信消息路由 把相关类型的消息交给对应的handler去处理
WxMpXmlOutMessage outMessage = this.weixinService.route(inMessage);
if (null != outMessage) {
response.getWriter().write(outMessage.toXml());
}
return;
}
if ("aes".equals(encryptType)) {
// 是aes加密的消息
String msgSignature = request.getParameter("msg_signature");
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(request.getInputStream(), configStorage,
timestamp, nonce, msgSignature);
this.logger.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
WxMpXmlOutMessage outMessage = this.weixinService.route(inMessage);
this.logger.info(response.toString());
response.getWriter().write(outMessage.toEncryptedXml(configStorage));
return;
}
response.getWriter().println("不可识别的加密类型");
return;
}
当我们填写好url和token(开发者自己随机写)点提交时,微信服务器就以get方式推送消息到填写的url上去,携带
参数 | 描述 |
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
加密/校验流程如下:
1. 将token、timestamp、nonce三个参数进行字典序排序
2. 将三个参数字符串拼接成一个字符串进行sha1加密
3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。
我们不用自己去加密然后比较,有个weixin-java-mp.jar包提供了wxMpService.checkSignature(timestamp,nonce, signature)方法,只要传入参数就帮我们做好验证,以后还会介绍该jar包,它包装了很好url及方法供开发者使用。成功返回echostr则网页会提示“配置成功”,至此服务器配置成功,该url是核心接口,是接收微信服务器推送消息的总控。1. 获取用户信息。
有2种access_token,一种是使用AppID和AppSecret获取的access_token,常常用在关注公众号,一种是OAuth2.0授权中产生的access_token,常用在打开页面需要用户点击授权时
4.1.全局票据access_token。
access_token出现的原因是什么呢?appId和appSecret是定位我们公众号的2个关键数据,其实appid已经可以唯一确定一个公众号,但为了安全考虑加个appsecret,用于验证公众号相关权限信息。如果每次都用2个参数唯一定位公众号不仅麻烦,而且也不安全,容易将消息暴露在公开环境,故微信出于安全及方便考虑,让开发者用appid和appsecret去拿access_token,即用access_token就可定位一个公众号,不仅安全,而且方便,当然也可以有其它的深意,这里不做深入研究。获得access_token的方式:
用get方式请求:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=myappid&secret=mysappsecret
(ps:微信相关的请求都是https而不是http,目的是为了安全)grant_type固定是client_credential,appid和appsecret是我们自己的消息。
比如我的测试号的请求返回的结果:
在jar里可以使用WxMpService.getAccessToken()方法获得,
源码:
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
//获得锁
Lock lock = this.wxMpConfigStorage.getAccessTokenLock();
try {
lock.lock();
//如果是强制刷新则强制将access token过期掉
if (forceRefresh) {
this.wxMpConfigStorage.expireAccessToken();
}
//如果accesstoken已经过期,则重新请求并保存到wxMpconfigStorage,否则则从
//wxMpConfigStorage里取
if (this.wxMpConfigStorage.isAccessTokenExpired()) {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" +
"&appid=" + this.wxMpConfigStorage.getAppId() + "&secret="
+ this.wxMpConfigStorage.getSecret();
try {
HttpGet httpGet = new HttpGet(url);
if (this.httpProxy != null) {
RequestConfig config = RequestConfig.custom().setProxy(this.httpProxy).build();
httpGet.setConfig(config);
}
try (CloseableHttpResponse response = getHttpclient().execute(httpGet)) {
String resultContent = new BasicResponseHandler().handleResponse(response);
WxError error = WxError.fromJson(resultContent);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
this.wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(),
accessToken.getExpiresIn());
} finally {
httpGet.releaseConnection();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} finally {
lock.unlock();
}
return this.wxMpConfigStorage.getAccessToken();
}
已经帮我们封装了很多方法,不用我们亲自去写url以及亲自使用httpClient去请求,如果缓存里有accessToken且没过期,否则刷新请求新的accessToken.(测试号一天可以请求2000)。
拿到accessToken就代表拿到了公众号,就可以拿到用户信息。
获得用户基本信息(包括昵称、头像、性别、所在城市、语言和关注时间。注意是拿不到用户的微信名的(openId只能算是用户标识符并不是用户名),只提供基本信息):
http请求方式:
GET https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
参数解释:access_token代表公众号,openid代表用户。lang代表语言,默认中文zh_CN。解释一下为什么要传递公众号,按正常理解传递用户号即可以拿到基本信息,和关注的公众号没关系,还是那句话,安全,安全,安全。微信服务器为了限制和管理公众号以及安全考虑,是不会无限制的提供服务的。
上图即是获得的用户信息。weixin-java-mp.jar提供了WxMpUserService.userinfo(openid,lang);获得用户基本信息。注意并没有传入access_token,因为access_token交给框架去管理生命周期,当缓存没有或已经过期再请求,如果存在则直接返回,还是那句话,微信服务器并发量很多,出支效率和安全考虑会限制token的请求次数。
1.2. 授权access_token。
当通过OAuth2.0方式弹出需要网页授权页面想获得用户基本信息时才需要用到该token,比如打开朋友圈分享的链接,该页面需要获得微信用户基本信息时使用,因为这时候用户并不有关注该公众号。
我还需要配置回调安全域名,否则会报出错了或未授权等错误提示:
找开微信 公众号后台管理,找到网页授权获取用户基本信息,如下图
点修改,添加安全域名,我的是
点确认,再弹出授权页面时就不会报相关错误了。
一般常见的OAuth2.0弹出授权页面出下:
第一步:用户同意授权,获取code
appid:公众号唯一标识号,redirect_uri就是当用户点“确认登录”时回到的链接,response_type固定写code,scope有2种,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息),state值随便写。
当用户确认登录后,微信服务器会将包括code值的参数传到上面redirect_uri的地址上,如下:redirect_uri/?code=CODE&state=STATE。当然若用户禁止授权,则重定向后不会带上code参数,仅会带上state参数redirect_uri?state=STATE。
注意参数顺序必需一致。
比如我的请求:https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxc5b2995ebfba4cf1& redirect_uri= http://chenyuanx.tunnel.2bdata.com /wx/weixin/home&response_type=code&scope=snsapi_userinfo &state=”11”#wechat_redirect.
回调打开链接地址:
http://chenyuanx.tunnel.2bdata.com /wx/weixin/home?code=061oz8Kb1DlFvt0eOcLb1piRJb1oz8Kg& state=”11”
第二步:通过code换取网页授权access_token
当后台通过回调地址获取code后,请求以下链接获取access_token:
注意这里需要填appsecret
第三步:拉取用户信息(需scope为 snsapi_userinfo)
http:GET(请使用https协议)
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
注意请求的地址和全局access_token请求地址是不一样的,
https://api.weixin.qq.com/cgi-bin/user/info。
这些请求如果使用sdk是不用自己写的。
第二步可以用:wxMpserver. oauth2getAccessToken(cdoe).
看看源码:
@Override
public WxMpOAuth2AccessToken oauth2getAccessToken(Stringcode) throws WxErrorException {
StringBuilder url = new StringBuilder();
url.append("https://api.weixin.qq.com/sns/oauth2/access_token?");
url.append("appid=").append(this.wxMpConfigStorage.getAppId());
url.append("&secret=").append(this.wxMpConfigStorage.getSecret());
url.append("&code=").append(code);
url.append("&grant_type=authorization_code");
return this.getOAuth2AccessToken(url);
}
第三步可以用:wxMpServer.oauth2getUserInfo(WxMpOAuth2AccessToken oAuth2AccessToken, String lang);
源码如下:
@Override
public WxMpUser oauth2getUserInfo(WxMpOAuth2AccessTokenoAuth2AccessToken, String lang) throws WxErrorException {
StringBuilder url = new StringBuilder();
url.append("https://api.weixin.qq.com/sns/userinfo?");
url.append("access_token=").append(oAuth2AccessToken.getAccessToken());
url.append("&openid=").append(oAuth2AccessToken.getOpenId());
if (lang == null) {
url.append("&lang=zh_CN");
} else {
url.append("&lang=").append(lang);
}
try {
RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
String responseText = executor.execute(getHttpclient(), this.httpProxy, url.toString(), null);
return WxMpUser.fromJson(responseText);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
至此初步接入微信成功了。
第一, 向微信服务器配置了统一的推送消息入口。一切的推送消息都从该入口进去
第二, 填写了正确的JS回调安全域名,是不带http://等协议头的
第三, 拿到用户基本信息。分2种,一种是当我们关注了公众号,公众号即有权拿到用户信息,另一个是Oauth2.0方式的授权页面获得用户基本信息。
5.公众号自定义菜单生成。
微信公众号自定义菜单规则:
自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
一级菜单最多4个汉字,二级菜单最多7个汉字,多出来的部分将会以“...”代替。
比如我的测试号:
那这么些自定义菜单是如何生成的呢,代码如下Main.java:
/**
* 自定义菜单
*
* @author lilw
*
*/
public class MenuConfig {
private static final String wx_appid = "wxc5b2995ebfba4cXXXX";
private static final String wx_appsecret = "61ef8021ff4d1376c096ade1a0XXXXX";
private static final String wx_token = "20170502aaAAXXXX";
public static final String prefix_url = "http://chenyuanxXXXXXX.tunnel.2bdata.com";
protected static WxMenu getMenu() {
WxMenu menu = new WxMenu();
WxMenuButton button1 = new WxMenuButton();
button1.setName("我要洗衣");
button1.setType(WxConsts.BUTTON_VIEW);
button1.setUrl(mainConfig.wxMpService().oauth2buildAuthorizationUrl(prefix_url + "/wx/weixin/home", "snsapi_userinfo", "123"));
WxMenuButton button2 = new WxMenuButton();
button2.setName("优惠活动");
WxMenuButton button21 = new WxMenuButton();
button21.setType(WxConsts.BUTTON_VIEW);
button21.setName("分享有礼");
button21.setUrl(prefix_url + "/mortgage/weixin/loan/needLoan");
WxMenuButton button22 = new WxMenuButton();
button22.setType(WxConsts.BUTTON_VIEW);
button22.setName("我在抵扣券");
button22.setUrl(prefix_url + "/mortgage/weixin/member/toLogin");
button2.getSubButtons().add(button21);
button2.getSubButtons().add(button22);
WxMenuButton button3 = new WxMenuButton();
button3.setName("我的");
WxMenuButton button31 = new WxMenuButton();
button31.setType(WxConsts.BUTTON_VIEW);
button31.setName("我的订单");
button31.setUrl(prefix_url + "/wx/weixin/myOrd");
WxMenuButton button32 = new WxMenuButton();
button32.setType(WxConsts.BUTTON_VIEW);
button32.setName("我的帐户");
button32.setUrl(prefix_url + "/mortgage/weixin/introduce");
WxMenuButton button33 = new WxMenuButton();
button33.setType(WxConsts.BUTTON_VIEW);
button33.setName("联系我们");
button33.setUrl(prefix_url + "/wx/page/joinUs.html");
WxMenuButton button34 = new WxMenuButton();
button34.setType(WxConsts.BUTTON_VIEW);
button34.setName("司机管理");
button34.setUrl("http://xd.sdaishu.com:6868/index.php?m=user&a=login");
WxMenuButton button35 = new WxMenuButton();
button35.setType(WxConsts.BUTTON_VIEW);
button35.setName("帮助与投诉");
button35.setUrl(prefix_url + "/mortgage/weixin/contactme");
button3.getSubButtons().add(button31);
button3.getSubButtons().add(button32);
button3.getSubButtons().add(button33);
button3.getSubButtons().add(button34);
button3.getSubButtons().add(button35);
menu.getButtons().add(button1);
menu.getButtons().add(button2);
menu.getButtons().add(button3);
return menu;
}
private static MainConfig mainConfig;
public static void main(String[] args) {
mainConfig = new MainConfig(wx_appid, wx_appsecret, wx_token);
WxMpService wxMpService = mainConfig.wxMpService(); // 获取微信创建订单的service
try {
wxMpService.getMenuService().menuCreate(getMenu());
System.out.println("success");
} catch (WxErrorException e) {
e.printStackTrace();
}
}
}
MainConfig.java:
/**
* 微信的主要配置信息 由wx.properties注入
*
*/
@Configuration
public class MainConfig {
@Value("${wx_appid}")
public String appid;
@Value("${wx_appsecret}")
public String appsecret;
@Value("${wx_token}")
public String token;
public static String prefix_url;
/**
* 如果出现 org.springframework.beans.BeanInstantiationException
* https://github.com/Wechat-Group/weixin-java-tools-springmvc/issues/7
* 请添加以下默认无参构造函数
*/
protected MainConfig(){}
/**
* 为了生成自定义菜单使用的构造函数,其他情况Spring框架可以直接注入
*
* @param appid
* @param appsecret
* @param token
* @param aesKey
*/
protected MainConfig(String appid, String appsecret, String token) {
this.appid = appid;
this.appsecret = appsecret;
this.token = token;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage() {
WxMpInMemoryConfigStorage configStorage = new WxMpInMemoryConfigStorage();
configStorage.setAppId(this.appid);
configStorage.setSecret(this.appsecret);
configStorage.setToken(this.token);
return configStorage;
}
@Bean
public WxMpService wxMpService() {
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
}
执行main方法返回结果:
[URL]: https://api.weixin.qq.com/cgi-bin/menu/create
[PARAMS]:{"button":[{"type":"view","name":"我要洗衣","url":"https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxc5b2995ebfba4cf1&redirect_uri=http%3A%2F%2Fchenyuanx.tunnel.2bdata.com%2Fwx%2Fweixin%2Fhome&response_type=code&scope=snsapi_userinfo&state=123#wechat_redirect"},{"name":"优惠活动","sub_button":[{"type":"view","name":"分享有礼","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/loan/needLoan"},{"type":"view","name":"我在抵扣券","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/member/toLogin"}]},{"name":"我的","sub_button":[{"type":"view","name":"我的订单","url":"http://chenyuanx.tunnel.2bdata.com/wx/weixin/myOrd"},{"type":"view","name":"我的帐户","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/introduce"},{"type":"view","name":"联系我们","url":"http://chenyuanx.tunnel.2bdata.com/wx/page/joinUs.html"},{"type":"view","name":"司机管理","url":"http://xd.sdaishu.com:6868/index.php?m=user&a=login"},{"type":"view","name":"帮助与投诉","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/contactme"}]}]}
[RESPONSE]:{"errcode":0,"errmsg":"ok"}
15:42:01.473 [main] DEBUGme.chanjar.weixin.mp.api.impl.WxMpMenuServiceImpl - 创建菜单:{"button":[{"type":"view","name":"我要洗衣","url":"https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxc5b2995ebfba4cf1&redirect_uri=http%3A%2F%2Fchenyuanx.tunnel.2bdata.com%2Fwx%2Fweixin%2Fhome&response_type=code&scope=snsapi_userinfo&state=123#wechat_redirect"},{"name":"优惠活动","sub_button":[{"type":"view","name":"分享有礼","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/loan/needLoan"},{"type":"view","name":"我在抵扣券","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/member/toLogin"}]},{"name":"我的","sub_button":[{"type":"view","name":"我的订单","url":"http://chenyuanx.tunnel.2bdata.com/wx/weixin/myOrd"},{"type":"view","name":"我的帐户","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/introduce"},{"type":"view","name":"联系我们","url":"http://chenyuanx.tunnel.2bdata.com/wx/page/joinUs.html"},{"type":"view","name":"司机管理","url":"http://xd.sdaishu.com:6868/index.php?m=user&a=login"},{"type":"view","name":"帮助与投诉","url":"http://chenyuanx.tunnel.2bdata.com/mortgage/weixin/contactme"}]}]},结果:{"errcode":0,"errmsg":"ok"}
success
说明生成菜单成功,可以取消公众号关注再关注即可看到最新结果。
自定义菜单官方API:https://mp.weixin.qq.com/wiki/10/0234e39a2025342c17a7d23595c6b40a.html
微信接入已经成功,当然这只是一小部分,还有很多功能需要开发,不过这只是相关API 的事了,至少,我们找到了入口。下一篇会详细介绍weixin-java-mp.jar工具包。