目录
开发的准备
- 准备一个公众号测试账号,微信提供了测试账号的平台,微信扫码登录即可。链接地址:mp.weixin.qq.com/debug/cgi-b…。
- 微信服务器需要回调应用,所以需要一个公网环境。这里采用的是内网击穿工具,笔者是网上百度搜的,小伙伴们也可以用自己常用的工具。这里提供一个可用的内网击穿工具natapp,链接地址:natapp.cn/。注册后开通免费隧道即可满足本次开发需求。
- natapp使用教程:natapp.cn/article/nat… 非使用此工具的可以自行跳过。
- 了解微信获取access_token相关接口,这里不过多阐述,有需要的可以看官方文档:developers.weixin.qq.com/doc/offiacc…
微信接入认证
第一步:填写服务器配置
文档地址:developers.weixin.qq.com/doc/offiacc…
关于URL和Token:
URL为应用解析微信报文对应的接口地址,因为使用了内网击穿工具,因此笔者提供的接口为: bpcgnd.natappfree.cc/wechat/wxAu…
Token采用的是明文模式,简单的设置成:123456,如果小伙伴们有安全要求可以根据文档选用加密模式
第二步:验证消息的确来自微信服务器
应用通过检验 signature 对请求进行校验。若确认此次 GET 请求来自微信服务器,请原样返回 echostr 参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信
微信认证接口:
@RequestMapping(value="/wxAuth",method = RequestMethod.GET)
@ResponseBody
public void wxAuthGet(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
request.setCharacterEncoding("UTF-8");
log.info("微信服务器接入验证接口,请求参数:{}", JSON.toJSONString(request.getParameterMap()));
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
try (PrintWriter out = response.getWriter()) {
if (CheckUtils.checkSignature(signature, timestamp, nonce, TOKEN)) {
out.write(echostr);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
checkSignature对应方法:
//校验微信请求
public static boolean checkSignature(String signature, String timestamp, String nonce,String token) {
String[] str = new String[]{token, timestamp, nonce};
//排序
Arrays.sort(str);
//拼接字符串
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < str.length; i++) {
buffer.append(str[i]);
}
//进行sha1加密
String temp = SHA1.encode(buffer.toString());
//与微信提供的signature进行匹对
return signature.equals(temp);
}
获取登录二维码
微信公众号提供了生成带参数的二维码的接口,这里我们需要生成用于关注公众号的二维码,用户扫码关注公众号后触发登录事件,完成关注公众号登录整个流程。其次是用户扫描带场景值二维码时,微信公众号可能推送以下两种事件:
- 如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
- 如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。 关于二维码类型:
- 临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
- 永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。 需求分析:这里用户扫码登录后二维码是需要失效的且一个二维码只能一个用户扫码登录,所以这里采取的是临时二维码的模式,相关文档:developers.weixin.qq.com/doc/offiacc…
获取临时二维码相关代码:
private String getQRcodeByPost(String url){
String accessToken = this.getAccessToken();
url = url+"?access_token=" + accessToken;;
JSONObject param = new JSONObject();
param.put("action_name", "QR_SCENE");
param.put("expire_seconds", "120");
param.put("scene_id", UUID.randomUUID().toString());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<JSONObject> entity = new HttpEntity<>(param, headers);
log.info("获取二维码请求url,{}", url);
ResponseEntity<String> result = this.restTemplate.postForEntity(url, entity, String.class);
log.info("获取二维码响应报文,{}", result.toString());
if (result.getStatusCode() == HttpStatus.OK) {
return result.getBody();
} else {
throw new BusinessException("获取二维码失败");
}
}
解析微信服务器推送事件报文
相关方法:
/**
* 微信服务器推送消息处理方法
* @param req
* @param resp
* @throws Exception
*/
@RequestMapping(value="/wxAuth",method = RequestMethod.POST)
@ResponseBody
public void wxAuthPost(HttpServletRequest req, HttpServletResponse resp) throws Exception{
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
try (PrintWriter out = resp.getWriter()) {
//把微信返回的xml信息转义成map
Map<String, String> map = XmlUtils.xmlToMap(req);
log.info("微信服务器推送报文信息:{}", map);
String message = wechatService.wxAuth(map);
out.write(message);
} catch (Exception e) {
log.error(e.getMessage());
}
}
这里的方法跟微信接入认证的方法名是一样的,不同的是接入认证请求方式是get,这里解析微信服务器推送事件请求方式是post且推送的报文为xml格式需要解析xml获取对应参数,相关文档:developers.weixin.qq.com/doc/offiacc…
关注/取消关注事件:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
</xml>
扫描带参数二维码事件:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_123123]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>
解析xml相关方法(这里只是简单的把xml里的内容丢到了map里):
/**
* xml转map
*/
public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
HashMap<String, String> map = new HashMap<String,String>();
SAXReader reader = new SAXReader();
InputStream ins = request.getInputStream();
Document doc = reader.read(ins);
Element root = doc.getRootElement();
List<Element> list = (List<Element>)root.elements();
for(Element e:list){
map.put(e.getName(), e.getText());
}
ins.close();
return map;
}
依赖解析xml相关jar包:
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.2.2</version>
</dependency>
从报文中可以看出,扫带参数二维码事件比关注/取消关注事件多了一个Ticket参数,于是我们可以从报文参数的差异中判断用户的操作,下边就简单的写下大概流程:
if ("subscribe".equals(eventType)) {
// 关注公众号触发事件,编写对应的处理逻辑
...
} else if ("unsubscribe".equals(eventType)){
// 取消关注公众号触发事件,编写对应的处理逻辑
...
} else if ("SCAN".equals(eventType)) {
// 扫带参数二维码触发事件,编写对应的处理逻辑
...
}
注意:如果用户没关注过公众号,扫带参数二维码时候触发的是关注公众号事件,只是参数携带了Ticket字段,因此在编写业务代码时候需要添加相关处理,用户关注公众号后登录。