前言
众所周知,在微信公众平台开发中,其实就是一系列的API请求和自身业务系统的集成,而在API请求中,AccessToken是优势一个必不可少的参数。
注:
- 本文基于企业微信,故部分API请求可能和订阅号,服务号,小程序不太相同,但整体思路一致。
- 本项目代码基于Java语言,SpringBoot框架。
在企业微信开发文档中有这样一段:
- access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
- 由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
- access_token至少保留512字节的存储空间。
- 企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。
需要注意的点:
- 在有效期内,重复获取不会返回新的AccessToken,并且不会延长有消息。(即使不在同一个程序内获取)
- AccessToken可能会提前过期。(两个小时获取一次可能会出现提前过期的问题)
问题描述
原始方案:
V0.1 定时器(schedule)
描述:在SpringBoot项目中,使用@Scheduled注解,每一小时获取一次AccessToken。
问题:在运行一段时间后,因网络波动导致某次请求失败,程序出错,定时器没有继续执行。
影响:程序无法进行任何微信相关的API请求。
改进:V0.2 定时器+异常捕获
V0.2 定时器+异常捕获
描述:在上述版本的情况下,增加异常捕获。
问题:运行一段时间再次出现异常,程序在获取AccessToken过程中出现阻塞,后续代码均未执行,定时器也无法执行。
影响:两次带来的影响都是致命的,犹如定时炸弹,完全不清楚下次会何时继续出现。
改进方案
总结:
此方案的优点在于,程序无需通过定时器或线程去处理AccessToken,通过Redis的缓存,并设置过期时间,实现动态的管理,并能和其他程序共享AccessToken(对于多个程序需要使用同一个应用时,会存在此需求)。在过期也能自动获取,并不影响程序正常运行。
即使某次请求出现问题,不会影响之后的请求。
代码展示
图中设计的几个类实现如下:
- WorkWXAPI类:
定义了企业微信相关的API请求的URL地址,以及其他企业微信相关的常量等。
/**
* @Author: geekfly
* @Description: 企业微信相关API及常量定义
* @Date: 2017/10/12
* @Modified By:
*/
public class WorkWXAPI {
public static String CORPID = ""; //企业ID
public static Integer AGENTID = 1000017; //应用ID
public static String AUTH_APP_SECRET = ""; //应用1Secret
public static String CONTACTS_SECRET = ""; //应用2Secret
public static String TOKEN = ""; //API接收Token
public static String EncodingAESKey = "";//API接收EncodingAESKey
public static String GET_ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s";
public static String GET_USER_OPENID_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s";
public static String SEND_MESSAGE_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s";
public static String GET_USER_INFO = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s";
//标签相关
public static String TAG_ADD_USERS_URL = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers?access_token=%s";
public static String TAG_DELETE_USERS_URL = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers?access_token=%s";
/*
成员管理
*/
//获取部门成员
public static String CONTACTS_SIMPLE_LIST = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%s&fetch_child=%s";
//获取部门成员详情
public static String CONTACTS_LIST = "https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&department_id=%s&fetch_child=%s";
//更新成员
public static String CONTACTS_UPDATE= "https://qyapi.weixin.qq.com/cgi-bin/user/update?access_token=%s";
//创建成员
public static String CONTACTS_CREATE = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s";
//读取成员
public static String CONTACTS_GET = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s";
/*
部门管理
*/
//创建部门
public static String DEPARTMENT_CREATE = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s";
//更新部门
public static String DEPARTMENT_UPDATE = "https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=%s";
//获取部门列表
public static String DEPARTMENT_LIST = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%s";
//js api
public static String JS_API = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s";
//认证通过
public static String AUTH_SUCCESS = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=%s&userid=%s";
//不同身份人员所在部门ID
public static Integer DEPARTMENT_JZG_ID_OTHER = 4658; //教师(其他)
public static Integer DEPARTMENT_JZG_ID = 2638; //教师(新)
public static Integer DEPARTMENT_NO_AUTH = 1889; //未认证用户部门
public static Integer DEPARTMENT_ROOT = 1; //根部门ID
//菜单ID
public static String MENU_ID_USER_INFO = "user_info"; //个人信息
public static String MENU_ID_USER_BIND = "user_bind"; //身份认证
}
注:代码使用String.format进行占位符替换,使用时需注意参数的顺序,以免因替换出错导致的传参错误。
- WXAPIUtil类
WXAPIUtil为封装的企业微信相关API操作的类,如:获取AccessToken获取,更新,删除用户信息,获取,创建,更新部门信息等。
/**
* @Author geekfly
* @Date 2018/1/17 9:28
* @Desc 微信API帮助类
*/
public class WXAPIUtil {
private final static Logger logger = LoggerFactory.getLogger(WXAPIUtil.class);
private static String accessTokenAuth = "access_token:auth";
private static String accessTokenContacts = "access_token:contacts";
/**
* 获取access token
* @param key 根据此key获取
* @return
*/
public static Map<String, Object> getAccessToken(String key){
String secret = "";
if(key == accessTokenAuth){
secret = WorkWXAPI.AUTH_APP_SECRET;
}else if(key == accessTokenContacts){
secret = WorkWXAPI.CONTACTS_SECRET;
}
String json = HttpClientUtil.get(String.format(WorkWXAPI.GET_ACCESS_TOKEN_URL, WorkWXAPI.CORPID, secret));
Map<String, Object> data = JsonUtil.WXJsonToMap(json);
if (Integer.parseInt(data.get("errcode").toString()) == 0) {
return data;
} else {
logger.info("{} get access error,msg:{}", key, data.toString());
}
return null;
}
/**
* 根据code获取用户信息
* @param code
* @return
*/
public static Map<String, Object> getOpenId(String code){
return HttpClientUtil.wxRequest(accessTokenAuth, String.format(WorkWXAPI.GET_USER_OPENID_URL, accessTokenAuth, code)
, null, HttpClientUtil.METHOD_GET);
}
/**
* 根据UserId获取用户信息
* @param userId
* @return
*/
public static Map<String, Object> getContact(String userId){
return HttpClientUtil.wxRequest(accessTokenContacts, String.format(WorkWXAPI.CONTACTS_GET, accessTokenContacts, userId)
, null, HttpClientUtil.METHOD_GET);
}
}
注:因代码过长,此处只引入部门函数。
- HttpClientUti类
包括Get,Post,wxRequest方法,可执行普通的http请求。
package cn.edu.zut.wechat.utils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: geekfly
* @Description: HttpClient工具类,用于发送请求
* @Date: 2017/10/12
* @Modified By:
*/
@Component
public class HttpClientUtil {
private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);
public static String METHOD_GET = "Get";
public static String METHOD_POST = "Post";
private static StringRedisTemplate stringRedisTemplate;
public static void setStringRedisTemplate(StringRedisTemplate template) {
stringRedisTemplate = template;
}
/**
* 执行企业微信接口方法
* @param key AccessToken的key值
* @param url 接口地址
* @param json json数据
* @param method 请求方式 Get or Post
* @return
*/
public static Map<String, Object> wxRequest(String key, String url, String json, String method){
Map<String, Object> mapData = new HashMap<>();
try{
int num = 0;
boolean flag = false;
while(num < 3){
num++;
if(stringRedisTemplate.hasKey(key)){
url = url.replace(key, stringRedisTemplate.opsForValue().get(key));
String jsonData = null;
logger.info(stringRedisTemplate.opsForValue().get(key));
if(method.equals(METHOD_GET)){
jsonData = get(url);
}else if(method.equals(METHOD_POST)){
jsonData = post(url, json);
}
mapData = JsonUtil.toMap(jsonData);
int errCode = Integer.parseInt(mapData.get("errcode").toString());
if(errCode == 0){ //正常
return mapData;
}else if(errCode == 42001){ // access_token过期
stringRedisTemplate.delete(key); //删除key
logger.info("key:{},access token过期", key);
}else{
flag = true;
logger.info("key:{},url:{},json:{},method:{},msg:{}", key, url, json, method, jsonData);
}
}
if(flag == false){
mapData = WXAPIUtil.getAccessToken(key);
if(mapData != null){
stringRedisTemplate.opsForValue().set(key, mapData.get("access_token").toString(),
Integer.parseInt(mapData.get("expires_in").toString()), TimeUnit.SECONDS);
}
}
}
}catch (Exception ex){
ex.printStackTrace();
logger.error("wxRequest error:{}", ex.getMessage());
}
mapData.put("errcode", -1000);
mapData.put("errmsg", "未获取到信息");
return mapData;
}
public static String get(String url){
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
HttpResponse response = null;
try{
response = httpClient.execute(httpGet);
}catch (Exception e) {}
String temp="";
try{
HttpEntity entity = response.getEntity();
temp= EntityUtils.toString(entity,"UTF-8");
}catch (Exception e) {}
return temp;
}
public static String post(String url, String json){
HttpClient httpClient = null;
HttpPost httpPost = null;
String result = null;
try{
httpClient = new DefaultHttpClient();
httpPost = new HttpPost(url);
StringEntity entity = new StringEntity(json,"utf-8");//解决中文乱码问题
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);
if(response != null){
HttpEntity resEntity = response.getEntity();
if(resEntity != null){
result = EntityUtils.toString(resEntity);
}
}
}catch(Exception ex){
ex.printStackTrace();
}
return result;
}
}
核心方法为:wxRequest
其中StringRedisTemplate在主函数中注入
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(WechatApplication.class, args);
HttpClientUtil.setStringRedisTemplate(applicationContext.getBean(StringRedisTemplate.class));
}
@Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory){
return new StringRedisTemplate(connectionFactory);
}
Controller中调用如下:
Map<String, Object> jsonMap = WXAPIUtil.updateContact(JsonUtil.toJson(wxUser));
package cn.edu.xxx.wechat.utils;
import com.alibaba.fastjson.JSON;
import java.util.List;
import java.util.Map;
/**
* @Author: hanyunfei
* @Description: Json 工具类
* @Date: 2017/10/12
* @Modified By:
*/
public class JsonUtil {
/**
* 对象转Json
* @param object
* @return 转化后的Json字符串
*/
public static String toJson(Object object){
String string = null;
try {
string = JSON.toJSONString(object);
} catch (Exception e) {
e.printStackTrace();
}
return string;
}
/**
* 普通Json字符串转Map
* @param json
* @return 转化后的Map
*/
public static Map<String, Object> toMap(String json){
return JSON.parseObject(json, Map.class);
}
/**
* 微信接口返回Json字符串转Map(考虑版本区别,需判断是否有errcode)
* @param json
* @return 转化后的Map
*/
public static Map<String, Object> WXJsonToMap(String json){
Map<String, Object> map = JSON.parseObject(json, Map.class);
if(!map.containsKey("errcode")){ //如果不存在errcode键,则添加该键
map.put("errcode", 0);
}
return map;
}
public static <T> T toBean(String text, Class<T> clazz) {
return JSON.parseObject(text, clazz);
}
public static <T> List<T> toList(String text, Class<T> clazz) {
return JSON.parseArray(text, clazz);
}
public static void main(String[] args) {
String json = "{\"errcode\": 0,\"errmsg\": \"ok\",\"userid\": \"zhangsan\",\"name\": \"李四\",\"department\": [1, 2],\"order\": [1, 2],\"position\": \"后台工程师\",\"mobile\": \"15913215421\",\"gender\": \"1\",\"email\": \"zhangsan@gzdev.com\",\"isleader\": 1,\"avatar\": \"http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0\",\"telephone\": \"020-123456\",\"english_name\": \"jackzhang\",\"extattr\": {\"attrs\":[{\"name\":\"爱好\",\"value\":\"旅游\"},{\"name\":\"卡号\",\"value\":\"1234567234\"}]},\"status\": 1,\"qr_code\":\"https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx\"\"external_profile\": {\"external_attr\": [{\"type\": 0,\"name\": \"文本名称\",\"text\": { \"value\": \"文本\"}},{\"type\": 1,\"name\": \"网页名称\",\"web\": { \"url\": \"http://www.test.com\", \"title\": \"标题\"}},{\"type\": 2,\"name\": \"测试app\",\"miniprogram\": { \"appid\": \"wx8bd80126147df384\", \"pagepath\": \"/index\", \"title\": \"my miniprogram\"}}]}";
System.out.println(JsonUtil.toMap(json));
}
}