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

 

 

 

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