Java開發公衆號系列教程(二):公衆號開發全局緩存access_token和jsapi_ticket

上篇文章給大家分享了Java實現微信公衆號調用微信拍照接口和打開本地相冊上傳圖片的實戰案例詳解,近期收到很多開發者朋友通過筆者微信的諮詢和反饋,表示很專業,很全面,很詳細,十足的乾貨,足金足兩,很受益。廣大開發者朋友的持續支持和好評,讓筆者有了更飽滿的技術創作精神,那麼今天就再次給大家分享一篇今天精心整理的乾貨《獲取公衆號的access_token和jsapi_ticket以及全局緩存公衆號accessToken和jsapi_ticket》

        首先先了解一下微信access_token和jsapi_ticket是什麼,啥時用,怎麼用等問題。

       爲了使第三方開發者能夠爲用戶提供更多更有價值的個性化服務,微信公衆平臺開放了許多接口,包括自定義菜單接口、客服接口、獲取用戶信息接口、用戶分組接口、羣發接口,調用攝像頭接口等,

開發者在調用這些接口時,都需要傳入一個相同的參數 access_token,它是公衆衆號的全局唯一票據,它是接口訪問憑證。

能看到這裏相信讀者已經知道在調用微信JS-SDK的所有場景下,都需要一個很重要的參數簽名(signature) ,而這個參數要使用權限簽名算法生成,生成簽名要用到access_token和jsapi_ticket。所以簽名之前必須先了解一下jsapi_ticket,jsapi_ticket是公衆號用於調用微信JS接口的臨時票據。正常情況下,jsapi_ticket的有效期爲7200秒,通過access_token來獲取。由於獲取jsapi_ticket的api調用次數非常有限,頻繁刷新jsapi_ticket會導致api調用受限,影響自身業務,開發者必須在自己的服務全局緩存jsapi_ticket

一、獲取access_token(通過下面2種方式都可以獲取)

1、開發者擁有認證過的微信公衆號通過下面方式獲取

登陸微信公衆平臺點選左側的開發者中心,申請成爲開發者。

成功後可以看到開發者中心界面,其中有AppId與AppSecret。目前AppSecret是部分隱藏的,如果要查看完整的版本需要綁定手機並掃二維碼才能看到。

爲了不暴露自己的AppId和AppSecret,我們要去向微信服務器獲取一個access_token 使用GET方法訪問下面的接口獲取,詳細代碼見下文工具類: 

https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

首先封裝一個AccessToken實體類,包含2個屬性具體代碼如下:

package com.huaqi.payment.domain;
import java.io.Serializable;

/**
 * create by guo bin hui in 2018-09-30
 * AccessToken實體類用於組裝返回結果
 */
public class AccessToken  implements Serializable {
    private String token;
    private Integer expiresIn;

    public String getToken() {return token;}

    public void setToken(String token) {this.token = token;}

    public Integer getExpiresIn() {return expiresIn;}

    public void setExpiresIn(Integer expiresIn) {this.expiresIn = expiresIn;}
}
public static AccessToken getToken()throws IOException{
        AccessToken token =  new AccessToken();
        String url = Constants.GET_ACCESS_TOKEN.replace("APPID",Constants.appID).replace("APPSECRET",Constants.secret);
        JSONObject jsonObj = doGetStr(url);
        if(!StringUtils.isEmpty(jsonObj)){
            token.setToken(jsonObj.getString("access_token"));
            token.setExpiresIn(jsonObj.getInt("expires_in"));
        }
        return token;
    }

調用上述微信接口所需參數說明

正常情況下,訪問這個接口微信會返回下述JSON數據包給公衆號:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

2、開發者如果沒有微信公衆號通過下面方式獲取

(1)、首先註冊一個微信公衆號測試賬號,具體步驟自行百度,此處省略

(2)、使用網頁調試工具生成:地址 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013

上圖填寫完後點擊檢查問題,則可以生成access_token

二、獲取jsapi_ticket

首先封裝一個JSTicket實體類,用於組裝微信服務器返回的JSON數據包

package com.huaqi.payment.domain;
import java.io.Serializable;

public class JSTicket implements Serializable {

    private String ticket;
    private Integer expiresIn;
    private Integer errCode;
    private String errMsg;

    public String getTicket() {
        return ticket;
    }

    public void setTicket(String ticket) {
        this.ticket = ticket;
    }

    public Integer getExpiresIn() {
        return expiresIn;
    }

    public void setExpiresIn(Integer expiresIn) {
        this.expiresIn = expiresIn;
    }

    public Integer getErrCode() {
        return errCode;
    }

    public void setErrCode(Integer errCode) {
        this.errCode = errCode;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }
}

獲取到access_token後通過下面方法獲取ticket(然後纔會有微信JS-SDK接口簽名權限)

    public static JSTicket getJsTicket(String access_token) throws IOException{
        JSTicket ticketObj = new JSTicket();
        String url = Constants.TICKET_CREATE_URL.replace("ACCESS_TOKEN",access_token);
        JSONObject jsonObj = doGetStr(url);
        if(!StringUtils.isEmpty(jsonObj)){
            ticketObj.setTicket(jsonObj.getString("ticket"));
            ticketObj.setErrCode(jsonObj.getInt("errcode"));
            ticketObj.setErrMsg(jsonObj.getString("errmsg"));
            ticketObj.setExpiresIn(jsonObj.getInt("expires_in"));
        }
        return ticketObj;
    }

下面是上述用到的具體的工具類:包括其他參數的獲取,SHA1加密,文件流讀寫操作等等。

package com.huaqi.payment.util;

import com.huaqi.payment.domain.AccessToken;
import com.huaqi.payment.domain.JSTicket;
import net.sf.json.JSONObject;
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.springframework.util.StringUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import java.text.SimpleDateFormat;
import java.util.Date;

public class WeiXinUtil {

    private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=APPSECRET&code=CODE&grant_type=authorization_code";


    public static AccessToken getToken()throws IOException{
        AccessToken token =  new AccessToken();
        String url = Constants.GET_ACCESS_TOKEN.replace("APPID",Constants.appID).replace("APPSECRET",Constants.secret);
        JSONObject jsonObj = doGetStr(url);
        if(!StringUtils.isEmpty(jsonObj)){
            token.setToken(jsonObj.getString("access_token"));
            token.setExpiresIn(jsonObj.getInt("expires_in"));
        }
        return token;
    }

    public static JSTicket getJsTicket(String access_token) throws IOException{
        JSTicket ticketObj = new JSTicket();
        String url = Constants.TICKET_CREATE_URL.replace("ACCESS_TOKEN",access_token);
        JSONObject jsonObj = doGetStr(url);
        if(!StringUtils.isEmpty(jsonObj)){
            ticketObj.setTicket(jsonObj.getString("ticket"));
            ticketObj.setErrCode(jsonObj.getInt("errcode"));
            ticketObj.setErrMsg(jsonObj.getString("errmsg"));
            ticketObj.setExpiresIn(jsonObj.getInt("expires_in"));
        }
        return ticketObj;
    }

    /**
     * 獲取時間戳(秒)
     */
    public static String getTimestamp() {
        return String.valueOf(System.currentTimeMillis() / 1000);
    }

    /**
     * 獲取當前時間 yyyyMMddHHmmss
     */
    public static String getCurrTime() {
        Date now = new Date();
        SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String s = outFormat.format(now);
        return s;
    }

    /**
     * 生成隨機字符串
     */
    public static String getNonceStr() {
        String currTime = getCurrTime();
        String strTime = currTime.substring(8, currTime.length());
        String strRandom = buildRandom(4) + "";
        return strTime + strRandom;
    }

    /**
     * 取出一個指定長度大小的隨機正整數.
     * @param length int設定所取出隨機數的長度。length小於11
     * @return int 返回生成的隨機數。
     */
    public static int buildRandom(int length) {
        int num = 1;
        double random = Math.random();
        if (random < 0.1) {
            random = random + 0.1;
        }
        for (int i = 0; i < length; i++) {
            num = num * 10;
        }
        return (int) ((random * num));
    }


    /**
     * 保存圖片至服務器
     * @param mediaId
     * @return 文件名
     */
    public static String saveImageToDisk(String mediaId)throws IOException{
        String filename = "";
        InputStream inputStream = getMediaStream(mediaId);
        byte[] data = new byte[1024];
        int len ;
        FileOutputStream fileOutputStream = null;
        try {
            //服務器存圖路徑
            String path = Constants.UPLOAD_PATH;
            filename = System.currentTimeMillis() + getNonceStr() + ".jpg";
            fileOutputStream = new FileOutputStream(path + File.separator+ filename);
            while ((len = inputStream.read(data)) != -1) {
                fileOutputStream.write(data, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return filename;
    }


    /**
     * 獲取臨時素材
     */
    private static InputStream getMediaStream(String mediaId)throws IOException {
        String url = "https://api.weixin.qq.com/cgi-bin/media/get";
        String access_token = getAccessToken();
        String params = "access_token=" + access_token + "&media_id=" + mediaId;
        InputStream is = null;
        try {
            String urlNameString = url + "?" + params;
            URL urlGet = new URL(urlNameString);
            HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
            http.setRequestMethod("GET"); // 必須是get方式請求
            http.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            http.setDoOutput(true);
            http.setDoInput(true);
            http.connect();
            // 獲取文件轉化爲byte流
            is = http.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return is;
    }

    public static JSONObject doGetStr(String url) throws IOException{
        HttpClient httpClient = new DefaultHttpClient();
        HttpGet  httpGet = new HttpGet(url);//HttpGet使用Get方式發送請求URL
        JSONObject jsonObj = null;
        HttpResponse  res = httpClient.execute(httpGet);//使用httpClient從Client執行httpGet的請求
        HttpEntity  entity = res.getEntity();//從HttpResponse中獲取結果
        if(!StringUtils.isEmpty(entity)){
          String result =   EntityUtils.toString(entity,"utf-8");
            jsonObj = JSONObject.fromObject(result);//字符串類型轉換爲JSON對象
        }
        return jsonObj;
    }

    public static JSONObject doPostStr(String url,String outStr) throws IOException{
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost(url);//HttpGet使用Post方式發送請求URL
        JSONObject jsonObj = null;
        httpPost.setEntity(new StringEntity(outStr,"utf-8"));//使用setEntity方法,將傳進來的參數放進請求
        HttpResponse  res = httpClient.execute(httpPost);
        HttpEntity  entity = res.getEntity();//從HttpResponse中獲取結果
        if(!StringUtils.isEmpty(entity)){
            String result =   EntityUtils.toString(entity,"utf-8");
            jsonObj = JSONObject.fromObject(result);//字符串類型轉換爲JSON對象
        }
        return jsonObj;
    }
}

通過以上方法我們已經獲取到了access_token和jsapi_ticket,這2個參數的有限期只有7200秒,接下來筆者爲大家詳解在項目中如何全局緩存這2個參數

三、全局緩存公衆號accessToken和jsapi_ticket

1、目前常規的實現思路有三種方式

(1)、通過數據庫保存
      做法是獲取access_token的時候把當前系統時間和access_token保存到數據表中,當再次獲取時,查詢上次獲取的時間與當前系統時間比較,看看時間是否大於2個小時(7200s)。如果超過這個時間限制,再獲取一個access_token,然後更新數據表的accessToken和getTime。
       表名:t_access_token
       票據:access_token
       獲取時間:getTime
(2)、通過物理磁盤創建txt文件保存
      1、創建access_token.txt文件
      2、讀取get_time
      3、讀取txt文件判斷時間是否超過2個小時
      4、超過則覆蓋重寫access_token.txt文件內容

(3)、通過servlet啓動線程,讓線程定時執行獲取

2、具體的實現方式

這裏我們主要講一下上述1中的(2)和(3)這二種方式

方式一、通過物理磁盤創建txt文件保存。這裏依賴google的Gson jar包

dependencies {
    compile(
            "org.springframework:spring-webmvc:${springVersion}",
            "javax.servlet:servlet-api:2.5",
            "com.alipay.sdk:alipay-sdk-java:3.1.0",
            "org.apache.commons:commons-collections4:4.1",
            "dom4j:dom4j:1.6.1",
            "commons-codec:commons-codec:1.9",
            "commons-httpclient:commons-httpclient:3.0.1",
            "net.sf.json-lib:json-lib:2.4:jdk15",

            'mysql:mysql-connector-java:5.1.38',
            'org.mybatis:mybatis-spring:1.2.2',
            'org.mybatis:mybatis:3.2.8',
            "com.alibaba:druid:1.1.9",
            "net.sf.ehcache:ehcache-core:2.6.11",
            "org.springframework:spring-context-support:4.2.3.RELEASE",
            "org.springframework:spring-jdbc:3.0.5.RELEASE",
            "com.alibaba:fastjson:1.2.4",
            "javax.servlet:jstl:1.2",
            "org.apache.httpcomponents:httpclient:4.3.6",
            "org.apache.httpcomponents:httpcore:4.4.6",
            "com.google.code.gson:gson:2.7"
    )
}

如果是maven項目安裝下面方式依賴

<dependency>  
    <groupId>com.google.code.gson</groupId>  
    <artifactId>gson</artifactId>  
    <version>2.7</version>  
</dependency> 

 具體的代碼實現如下:主要思路就是通過IO流的讀寫操作,爲了測試,寫一個main方法可以測試一下

package com.huaqi.payment.util;
import com.google.gson.Gson;
import com.huaqi.payment.domain.AccessToken;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 *
 * 微信獲取AccessToken並本地保存
 */
public class WxAccessToken {
    private static final long MAX_TIME = 7000 * 1000;// 微信允許最長Access_token有效時間爲7200秒,這裏設置爲7000秒

    public static AccessToken access_token_obj=null;
    /**
     * 獲取Access_token 保存並且只保存2小時Access_token。如果超過兩個小時重新獲取;如果沒有超過兩個小時,直接獲取
     * 思路:將獲取到的Access_token和當前時間存儲到file裏,
     * 取出時判斷當前時間和存儲裏面的記錄的時間的時間差,如果大於MAX_TIME,重新獲取,並且將獲取到的存儲到file替換原來的內容
     * 如果小於MAX_TIME,直接獲取。
     */
    public static String getSavedAccessToken() throws IOException {
        Gson gson = new Gson();
        String mAccess_token = null;// 需要獲取的Access_token;
        System.out.println(System.getProperty("user.di"));
        File file = new File(Constants.LOCAL_ACCESS_TOKEN_path);// Access_token保存的位置
        // 如果文件不存在,創建
        if (!file.exists())
            file.createNewFile();
        // 如果文件大小等於0,說明第一次使用,存入Access_token

        if (file.length() == 0) {
            access_token_obj = WeiXinUtil.getToken();
            String token = access_token_obj.getToken();
            FileOutputStream fos = new FileOutputStream(Constants.LOCAL_ACCESS_TOKEN_path, false);// 不允許追加
            AccessToken at = new AccessToken();
            at.setToken(token);
            at.setExpiresIn(Integer.valueOf(System.currentTimeMillis()/1000+""));
            String json = gson.toJson(at);
            fos.write(json.getBytes());
            fos.close();
        } else {
            // 讀取文件內容
            FileInputStream fis = new FileInputStream(file);
            byte[] b = new byte[2048];
            int len = fis.read(b);
            String mJsonAccess_token = new String(b, 0, len);// 讀取到的文件內容

            AccessToken access_token = gson.fromJson(mJsonAccess_token, new AccessToken().getClass());
            if (access_token.getExpiresIn() != null) {
                long lastSaveTime = Long.parseLong(access_token.getExpiresIn()*1000+"");
                long nowTime = System.currentTimeMillis();
                long remianTime = nowTime - lastSaveTime;
                if (remianTime < MAX_TIME) {
                    AccessToken access = gson.fromJson(mJsonAccess_token, new AccessToken().getClass());
                    mAccess_token = access.getToken();
                } else {
                    access_token_obj = WeiXinUtil.getToken();
                    FileOutputStream fos = new FileOutputStream(file, false);// 不允許追加
                    AccessToken newToken = new AccessToken();
                    newToken.setToken(access_token_obj.getToken());
                    newToken.setExpiresIn(Integer.valueOf(System.currentTimeMillis()/1000+""));
                    String json = gson.toJson(newToken);
                    fos.write((json).getBytes());
                    fos.close();
                }
            }
        }
        return mAccess_token;
    }

    public static void main(String args[]) throws IOException {
        WxAccessToken.getSavedAccessToken();
    }
}

最終緩存到本地磁盤的access_token.txt內容格式如下:

{"token":"14__rU4rUKeTPSqv6Y4l1r1gnR9M3rl07RG-5Q_D0os6TmOP8204OlwSjmGSKP7KuRVffopvrY9t3vi78GWUsC2wd8jG1CdfTrZQbUirxiMp3KJxh3zhYjVABD_ixd6pqd8v5GNEkWPM6tFzYxTGEOhABAVTD","expiresIn":1538225055}

在項目中要用到access_token的地方就可以引用這個工具類,動態獲取access_token,根據動態獲取的access_token生成的jsapi_ticket自然也是動態的,有效期2個小時。

方式二、通過servlet啓動線程,讓線程定時執行獲取

(1).寫一個線程,在線程中實現定時獲取accessToken。代碼如下:

package com.huaqi.payment.util;

import com.huaqi.payment.domain.AccessToken;

import java.io.*;

public class DynamicTokenThread  implements Runnable {

    private static AccessToken access_token = null;
    @Override
    public void run() {
        while(true){
            try {
                //調用工具類獲取access_token(每日最多獲取2000次,每次獲取的有效期爲7200秒)
                access_token = WeiXinUtil.getToken();
                System.out.println(System.getProperty("user.di"));
                OutputStream  os = new FileOutputStream(Constants.LOCAL_ACCESS_TOKEN_path,false);
                byte[] data = access_token.getToken().getBytes();
//              byte[] data  = access_token.getToken().getBytes();
                os.write(data, 0, data.length);    //3、寫入文件
                os.flush();    //將存儲在管道中的數據強制刷新出去
                if (null != access_token) {
                    //7000秒之後重新進行獲取
                   Thread.sleep((access_token.getExpiresIn() - 200) * 1000);
                } else {//獲取失敗時,60秒之後嘗試重新獲取
                    Thread.sleep(60 * 1000);
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }catch (FileNotFoundException e) {
                e.printStackTrace();
                System.out.println("文件沒有找到!");
            }catch (IOException e){
                e.printStackTrace();
                System.out.println("寫入文件失敗!");
            }
        }
    }
}

  (2). 寫一個servlet去開啓這個線程。

package com.huaqi.payment.server;

import com.huaqi.payment.util.DynamicTokenThread;

import javax.servlet.http.HttpServlet;
/**
 * 編寫servlet並在servlet初始化時啓動該線程
 *
 */
public class GetAccessTokenServlet extends HttpServlet {
    @Override
    public void init(){
        new Thread(new DynamicTokenThread()).start();//啓動動態獲取access_token的線程
    }
}

3. 在web.xml中去配置servlet,設置成啓動tomcat時就啓動該線程,然後每隔一定時間就會去執行一次該線程(自動獲取一次請求動態得到access_token)

<servlet>
    <servlet-name>getAccessTokenServlet</servlet-name>
    <servlet-class>com.huaqi.payment.server.GetAccessTokenServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>getAccessTokenServlet</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

以上內堪稱最全面的Java獲取公衆號的access_token和jsapi_ticket以及全局緩存公衆號accessToken和jsapi_ticket案例詳解,更多技術乾貨敬請持續關注博主。歡迎廣大開發者朋友一起學習交流,聯繫筆者電話(同微信):18629374628

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章