前段時間因公司業務需要,進行了微信公衆號相關功能的開發,在此之前這方面我也是沒有接觸過,所以做的過程中也踩了很多坑,查了不少資料,應小夥伴要求,就把代碼貼出來,做個記錄,也方便以後再開發這方面的功能。
首先是幾個model類
靜態常量類,這裏是一些微信公衆號的幾個核心信息
public class WechatConstants {
public static final String APPID = "xxxxxxxx";
public static final String APPSECRET = "xxxxxxxx";
private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
public static String getAccess_token_url(){
return ACCESS_TOKEN_URL.replace("APPID",APPID).replace("APPSECRET",APPSECRET);
}
}
access_token實體類
public class AccessToken {
private String access_token;
private int expires_in;
public AccessToken(String access_token, int expires_in) {
this.access_token = access_token;
this.expires_in = expires_in;
}
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
}
微信模板實體類(顯示內容)
public class WxTemplate {
private String template_id;
private String touser;
private String url;
private String topcolor;
private Map<String, TemplateData> data;
public String getTemplate_id() {
return template_id;
}
public void setTemplate_id(String template_id) {
this.template_id = template_id;
}
public String getTouser() {
return touser;
}
public void setTouser(String touser) {
this.touser = touser;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTopcolor() {
return topcolor;
}
public void setTopcolor(String topcolor) {
this.topcolor = topcolor;
}
public Map<String, TemplateData> getData() {
return data;
}
public void setData(Map<String, TemplateData> data) {
this.data = data;
}
@Override
public String toString() {
return "WxTemplate [template_id=" + template_id + ", touser=" + touser + ", url=" + url + ", topcolor="
+ topcolor + ", data=" + data + "]";
}
}
模板消息中的一個數據的實體類,比如{{first.DATA}}
public class TemplateData {
private String value;
private String color;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "TemplateData [value=" + value + ", color=" + color + "]";
}
}
然後是兩個核心util類
用於檢查證書
public class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateEncodingException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateEncodingException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
發起https請求工具類
public class WeixinUtil {
private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);
public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
try {
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect();
if (null != outputStr && outputStr != "") {
OutputStream outputStream = httpUrlConn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
log.error("Weixin server connection timed out.");
} catch (Exception e) {
log.error("https request error:{}", e);
}
return jsonObject;
}
}
最後就是調用微信官方接口的util類
調用官方接口工具類
@Component
public class SendTemplateUtil {
private static Logger log = LoggerFactory.getLogger(SendTemplateUtil.class);
public static AccessToken accessToken = null;
@Autowired
private RedisUtils redisUtils;
@Scheduled(fixedDelay = 2*2700*1000)
public void getAccessToken(){
JSONObject accessTokenJson = WeixinUtil.httpRequest(WechatConstants.getAccess_token_url(), "GET", null);
System.out.println(accessTokenJson.toString());
String access_token = accessTokenJson.getString("access_token");
int expires_in = accessTokenJson.getInt("expires_in");
log.info("成功獲取access_token:"+access_token);
accessToken = new AccessToken(access_token,expires_in);
}
public int WeiXinSend(WxTemplate wxTemplate) throws Exception{
String maxCount = redisUtils.get("maxCount", 1);
if(StringUtils.isNoneBlank(maxCount)){
if(Integer.parseInt(maxCount)>500){
throw new Exception("accesstoken獲取次數頻繁");
}
}
Integer result = 0;
String access_token = accessToken.getAccess_token();
log.info("access_token**********"+access_token);
log.info("執行微信公衆號發送模板消息方法**********");
String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token="+access_token;
String jsonString = JSONObject.fromObject(wxTemplate).toString();
JSONObject jsonObject = WeixinUtil.httpRequest(url, "POST", jsonString);
if (null != jsonObject) {
if (0 != jsonObject.getInt("errcode")) {
result = jsonObject.getInt("errcode");
log.error("錯誤 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
if(result.equals(40001)){
Integer incr = redisUtils.incr("accesstoken_req_count");
if(incr>5){
try {
log.error("重新獲取access_token:");
getAccessToken();
} catch (Exception e) {
}
redisUtils.del(1,"accesstoken_req_count");
if(StringUtils.isBlank(maxCount)){
redisUtils.set("maxCount", "0", 1);
redisUtils.expire("maxCount", DateUtil.getSeconds(), 1);
}
redisUtils.incr("maxCount");
}
}
}
}
log.info("模板消息發送結果(0代表發送成功):"+result);
return result;
}
public int verifyAttention(String openId){
int subscribe = -1;
String access_token = accessToken.getAccess_token();
log.info("執行驗證用戶是否關注公衆號方法**********");
String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token="+access_token+"&openid="+openId+"&lang=zh_CN";
JSONObject jsonObject = WeixinUtil.httpRequest(url, "GET", "");
if (null != jsonObject){
if (jsonObject.has("errcode")){
log.error("錯誤 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}else {
subscribe = jsonObject.getInt("subscribe");
System.out.println(jsonObject.getInt("subscribe")+"--------------"+subscribe);
log.info("****************驗證返回結果:", jsonObject.getInt("subscribe"));
}
}
return subscribe;
}
public String getUserOpenId(String code){
try {
System.out.println(WechatConstants.APPID);
String appId = WechatConstants.APPID;
String appSecret = WechatConstants.APPSECRET;
log.info("執行微信授權獲取用戶openId方法**********");
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appId+"&secret="+appSecret+"&code="+code+"&grant_type=authorization_code";
String jsonString = "";
String access_token = "";
String openid = "";
JSONObject jsonObject = WeixinUtil.httpRequest(url, "POST", jsonString);
if (null != jsonObject) {
Iterator<String> it = jsonObject.keys();
while(it.hasNext()){
String key = it.next();
String value = jsonObject.getString(key);
log.info("key: "+key+",value:"+value);
}
openid = jsonObject.getString("openid");
access_token = jsonObject.getString("access_token");
}
return openid;
} catch (Exception e) {
return null;
}
}
}
微信公衆號後臺的IP白名單和授權域名別忘了配置哈!access_token也可以選擇存到redis,定時去刷新redis,我這裏是寫了一個全局變量。獲取access_token每天次數是有限的哦,不能超過兩千次,access_token是請求微信第三方接口的重要憑證,如果失效了很多請求都會失敗哦!看一下access_token官方的說明:
踩過的坑
1.access_token不定期會失效,按理說access_token失效時間還沒到的時候我這邊就會定時刷新access_token,爲什麼會失效呢?很明顯就是定時器刷新之前access_token就失效了,官方不是說失效時間2小時嗎,怎麼還沒到就不間斷失效呢?這裏注意看官方一個很重要的說明:
五分鐘內新老access_token都可以用,過了五分鐘老的就失效了,經過排查才知道,因爲其他系統也去刷新了access_token,所以導致我這邊的失效了。
2.由於微信公衆號授權域名最多隻能配置兩個,但是系統數量較多,每個系統都需要用同一個公衆號進行微信授權登錄。怎麼辦呢?那就公用一個授權域名,將授權頁面、js、css等靜態文件從項目中抽出來,單獨做成一個純前端的web項目,然後放到web容器中,我是將整個web項目放到了Nginx服務器(一款輕量級的Web服務器、反向代理服務器)上面,這樣不僅解決了這個問題,還可以保證再靜態資源內容改變的時候後臺不用重新打包發版。我這邊是做了一個關鍵字自動回覆,然後點擊回覆鏈接跳轉登錄頁進行靜默授權,如果你要獲取用戶微信的信息,可以採用非靜默授權的方式。下面是核心的js代碼:
var appid = 'xxxxxx';
var href = window.location.href;
var code = getUrlParam('code');
var openid = '';
if(code){
$.ajax({
url: 'https://配置的授權域名/loginapp/getUserOpenId',
type:'post',
dataType:'json',
data:{code:code},
success:function(datas){
if(datas.code>0){
console.log("用戶openid獲取成功:"+datas.data);
openid = datas.data;
$.ajax({
type:"post",
url:"https:///配置的授權域名/loginapp/verifyAttendation",
dataType:'json',
data:{openId:openid},
success:function(datas){
if(datas.code>0){
console.log("驗證結果:"+datas.data);
var subscribe = datas.data;
if(subscribe == 0){
console.log("沒有關注公衆號");
window.location.href = "auth.html";
}else if(subscribe == 1){
console.log("已經關注公衆號");
}else{
console.log("錯誤判斷返回")
}
}else{
console.log("驗證失敗!!!");
}
}
});
}else{
console.log("用戶openid獲取失敗");
}
}
});
}else{
window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid='+appid+'&redirect_uri='+encodeURIComponent(href)+'&response_type=code&scope=snsapi_base&state=1#wechat_redirect'
}
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
$(function(){
$('#auth-btn').on('click',function(){
var accountVal = $('#account').val(),
pwdVal = $('#pwd').val();
if(fnCheckAccount(accountVal) && fnCheckPwd(pwdVal)){
if(openid){
$.ajax({
url: 'https://配置的授權域名/loginapp/authorization',
type:'post',
dataType:'json',
data:{account:accountVal,password:hex_md5(pwdVal),openId:openid},
success:function(datas){
if(datas.data>0){
console.log("授權成功!")
window.location.href = "success.html";
}else{
console.log("授權失敗!")
window.location.href = "fail.html";
}
}
});
} else{
wDialog.toast({
msg: "網絡慢,請重試。"
})
}
}
});
});
3.這也是一個讓我最蛋疼的坑,微信模板消息發送在access_token未失效的情況,發送接口間歇性出現40001錯誤,這個錯誤官方說明是access_token失效或者appsecert錯誤,經過排查。appsecert是沒錯的,那麼就一定是access_token失效咯,經過再一次排查,我發現模板消息推送失敗幾次之後又會成功,而失敗的時候和成功的時候access_token是一樣,那就說明access_token也沒有失效,那真是見鬼了哦,查了很多資料都沒找到解決方法,都說這是微信官方的bug,怎麼辦呢?問題總是要解決的,於是我就換一個思路。因爲是間接性發送失敗,所以我就加了重試機制,發送失敗就重新發送,並且記錄該條模板消息重發的次數,如果重發次數大於5次,就自動重新刷新一次access_token,因爲獲取access_token每天的次數有限,所以還會記錄自動刷新access_token的次數,如果大於500次就不再自動刷新access_token了(定時器任務刷新access_token還是會執行的)並且將失敗的消息記錄日誌。具體代碼也在上面有。如果你的消息可以非實時發送,可以將失敗的消息放入消息隊列進行異步處理。