说明:
微信公众号开发只作参考,不作限制,条条大路通罗马,本文只是实现当前业务的一种方式,需要优化的地方还有很多,共勉。
应系统业务拓展,加入微信公众号模块。微信公众号开发流程如下,本文共分六个部分:
1)微信后台与项目对接;(开启开发者模式)
2)获取AccessToken;
3)自定义菜单;
4)获取用户openid(用户在微信注册的基本信息)
5)页面跳转控制;
6)接收事件推送
一、微信后台与项目对接
通过阅读官方文档,可以得知若要接入微信公众号平台开发,开发者需要按照以下三步流程:
1、填写服务器配置
解释:
1)服务器地址(URL):域名指向的IP端口必须是80端口和443端口
2)令牌(Token):须与后台配置的一致
3)消息加解密密钥(EncodingAESKey):随机生成即可
4)消息加解密方式:我选择的是明文模式,选加密模式也可以,看项目的私密性而定
5)最重要的一点:配服务器信息之后,需要添加白名单(域名指向的IP)
2、验证服务器地址的有效性
点击上图的提交,验证服务器地址等相关信息的有效性
验证通过后选择启用,则进入开发者模式,通过微信后台自定义事件失去效果
注意:
我们的应用服务器要接受微信服务器的get请求,其中包括四个参数(signature、timestamp、nonce、echostr),开 发者通过检验signature对请求进行校验。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生 效,成为开发者成功,否则接入失败。
1) 排序:将token、timestamp、nonce三个参数进行字典序排序
2) 加密:将三个参数字符串拼接成一个字符串进行SHA1加密,开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
// 此处TOKEN即我们刚刚所填的token,须与微信后台配置的token保持一致
private static final String TOKEN = "obdkurongshenji";
@GetMapping(value = "/login")
public String checkName(@RequestParam(name = "signature") String signature,
@RequestParam(name = "timestamp") String timestamp, @RequestParam(name = "nonce") String nonce,
@RequestParam(name = "echostr") String echostr) throws UnsupportedEncodingException {
// 排序
String sortString = sort(TOKEN, timestamp, nonce);
// 加密
String myString = sha1(sortString);
// 校验
if (myString != null && myString != "" && myString.equals(signature)) {
// 如果检验成功原样返回echostr,微信服务器接收到此输出,才会确认检验完成。
logger.debug("-----------------------签名校验通过------------------------");
// 首次接入打开,成功创建之后请注释掉(生成自定义菜单)
Long id = tbWxAccesstokenService.selectMaxId();// 获取最后一个(未过期)token
String access_token = tbWxAccesstokenService.selectAccessTokeById(id);
logger.debug("-----------后端服务器地址:" + backUrl);
if (null != access_token) {
Menu menu = WebchatMenuUtil.initMenu(backUrl, appId);
WebchatMenuUtil.createMenu(access_token, JSON.toJSONString(menu));
} else {
logger.debug("-----------access_token获取失败------------");
}
return echostr;
} else {
logger.debug("-----------------------签名校验失败------------------------");
return null;
}
}
解释:
代码中涉及到的排序,加密的代码示例如下:
// 排序方法
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();
}
/**
* 将字符串进行sha1加密
* @param str 需要加密的字符串
* @return 加密后的内容
*/
public String sha1(String str) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(str.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
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 null;
}
二、获取AccessToken
1、定义:access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
2、在库融审计项目中,用的是access_token持久化,每隔一个小时获取到新的access_token,则保存到数据库中,调用微信接口需要access_token时,从数据库中取出最新的access_token即可。
3、接口请求方式:GET
4、接口:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
5、接口详情:
三、自定义菜单
1、自定义菜单能够帮助公众号丰富界面,让用户更好更快的理解公众号的功能。开启自定义菜单后,公众号界面如图所示:
注意:
1)自定义菜单最多包含3个一级菜单,每个菜单最多包含5个二级菜单
2)一级菜单最多四个汉字,二级菜单最多七个汉字,多出来的部分将会以”…”代替
3)创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
2、接口请求方式:POST
3、接口:https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
4、菜单示例:
private final static String CREATE_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
private static Logger logger = LoggerFactory.getLogger(WebchatMenuUtil.class);
public static int createMenu(String token, String menu) {
int result = 0;
String url = CREATE_MENU_URL.replace("ACCESS_TOKEN", token);
JSONObject jsonObject = WebchatHttpsUtil.httpsRequest(url, "POST", menu);
if (null != jsonObject) {
result = jsonObject.getInteger("errcode");
}
logger.debug("----------------公众号菜单初始化成功-------------------");
return result;
}
// 默认自定义菜单初始化
public static Menu initMenu(String backUrl, String appId) throws UnsupportedEncodingException {
Menu menu = new Menu();
ViewButton button11 = new ViewButton();
button11.setName("车辆列表");
button11.setType("view");
button11.setUrl(backUrl + "/freeter-api/wechatredirect/auth?state=carListPage");
ViewButton button12 = new ViewButton();
button12.setName("报警列表");
button12.setType("view");
button12.setUrl(backUrl + "/freeter-api/wechatredirect/auth?state=stockListPage");
Button button1 = new Button();
button1.setName("数据列表"); // 将button11/button12两个button作为二级菜单封装第一个一级菜单
button1.setSub_button(new Button[] { button11, button12 });
// 创建公众号跳转到小程序的菜单
MiniprogramButton miniprogramButton = new MiniprogramButton();
miniprogramButton.setName("上线验证");
miniprogramButton.setType("miniprogram");
miniprogramButton.setAppid("appid");
miniprogramButton.setUrl("http://mp.weixin.qq.com");
miniprogramButton.setPagepath("pages/index/index");
ViewButton button3 = new ViewButton();
button3.setName("个人中心");
button3.setType("view");
button3.setUrl(backUrl + "/freeter-api/wechatredirect/auth?state=userInfoPage");
menu.setButton(new Button[] { button1, miniprogramButton, button3 });// 将31Button直接作为一级菜单
return menu;
}
四、获取用户openid
1、作用:
开发者可通过OpenID来获取用户基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的unionid来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的unionid是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。
2、在本项目中,采用的是通过OAuth2的方式获取用户对于当前公众号的openId。步骤如下:
1)配置回调域名
注意:须将MP_verify_wtfJFVgIjlkfwy2I.txt文件放在项目的根目录下
注意:1)redirect_uri表示回调地址
2)snsapi_userinfo表示应用授权作用域为请求用户信息,scope=snsapi_base表示不需要用户授权
3)state表示我们在后面需要的参数
3)通过调用第二步的url,微信服务器会携带code和state参数将请求转发到回调地址,也就是我们在后台定义的接口,再使用code调用微信提供的接口,获取用户的openid,url如下:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=APPSECRET&code=CODE&grant_type=authorization_code
通过接口调用,返回详情如下:
此时就获取到用户的openId了
五、页面跳转控制
1、流程说明:
在项目中,页面跳转控制是在获取到用户openid之后,根据openid判断用户是否绑定到项目,如果没有,则页面统一跳转到用户绑定页;如果已经绑定,再根据获取code时,传的state参数值,判断当前用户点击的菜单是去哪个页面,再通过重定向的方式,携带必要的参数重定向到指定的页面。
2、示例:
@Value("${wechat.frontUrl}")
private String frontUrl;
@Value("${wechat.backUrl}")
private String backUrl;
@Value("${wechat.appId}")
private String appId;
@Value("${wechat.appSecret}")
private String appSecret;
// 用户同意授权,获取code
private final static String GET_CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPIDVALUE"
+ "&redirect_uri=REDIRECTURIVALUE&response_type=code&scope=snsapi_base&state=STATEVALUE#wechat_redirect";
// 获取openId地址
private final static String GET_OPENID_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPIDVALUE"
+ "&secret=SECRETVALUE&code=CODEVALUE&grant_type=authorization_code";
@Autowired
private TbWxUserbindingService tbWxUserbindingService;
@ResponseBody
@RequestMapping(value = "/auth", method = RequestMethod.GET)
public void GuideServlet(HttpServletRequest request, HttpServletResponse response, @RequestParam("state") String state) throws Exception {
System.out.println("------state:" + state);
// 设置编码
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");
/**
* 第一步:用户同意授权,获取code
*/
String redirect_uri = URLEncoder.encode("后端接口url", "UTF-8");// 授权后重定向的回调链接地址,请使用urlencode对链接进行处理(文档要求)
// 按照文档要求拼接访问地址
String url = GET_CODE_URL.replace("APPIDVALUE", appId).replace("REDIRECTURIVALUE", redirect_uri).replace("STATEVALUE", state);
System.out.println("------url:" + url);
response.sendRedirect(url);// 跳转到要访问的地址
}
@RequestMapping("/distribute")
@ApiOperation(value = "微信端页面跳转中央控制器")
public String distribute(HttpServletRequest request, @RequestParam("code") String code, @RequestParam("state") String state) throws UnsupportedEncodingException {
System.out.println("----------获取到的code:" + code + "----------state:" + state);
String openId = null;
String returnUrl = null;
// 第三步:同意授权,通过code换取网页授权access_token
if (code != null) {
// 拼接请求地址
String url = GET_OPENID_URL.replace("APPIDVALUE", appId).replace("SECRETVALUE", appSecret).replace("CODEVALUE", code);
System.out.println(url);
JSONObject jsonObject = HttpUtil.httpGet(url);
openId = jsonObject.getString("openid");
System.out.println("返回信息:"+ jsonObject);
}
if (null != openId) {
// 判断用户是否绑定
TbWxUserbindingEntity user = tbWxUserbindingService.isAlreadyBinding(openId);
// 用户已绑定,根据返回的state判断需要跳转的页面
if (null != user) {
switch (state) {
case "carListPage":// 车辆列表
returnUrl = "url" + user.getUserid();
break;
case "stockListPage":// 报警列表
returnUrl = "url" + user.getUserid();
break;
case "userInfoPage":// 用户个人中心页面
returnUrl = "url" + user.getUserid() + "&openId=" + openId;
break;
default:
break;
}
} else {
// 用户未绑定,页面跳转到用户绑定页面
returnUrl = "url";
}
System.out.println("------跳转地址为:-----" + returnUrl);
} else {
return "openId获取失败";
}
return "redirect:" + returnUrl;
}
六、接收事件推送
1、官方解读
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许,详细内容如下:
1)关注/取消关注事件
2)扫描带参数二维码事件
3)上报地理位置事件
4)自定义菜单事件
5)点击菜单拉取消息时的事件推送
6)点击菜单跳转链接时的事件推送
2、在我的项目中,目前涉及到的事件包括:
消关注事件:用户取消关注,自动解除该用户与库融审计项目的绑定关系自定义菜单事件:用户点击菜单,请求统一指向同一个接口,获取用户的openid,判断用户是否绑定库融审计项目,如果未绑定,则页面统一跳转到用户绑定页,绑定则跳转到该菜单对应的页面。
接收普通消息:统一回复文本消息
1)文本消息
2)图片消息
3)语音消息
4)视频消息
5)地理位置消息
6)链接消息