微信公众号开发 - token获取(保证同一时间段内只请求一次)
微信公众号开发文章目录
1.微信公众号开发 - 环境搭建
2.微信公众号开发 - 配置表设计以及接入公众号接口开发
3.微信公众号开发 - token获取(保证同一时间段内只请求一次)
4.微信公众号开发 - 菜单按钮bean封装
5.微信公众号开发 - 创建菜单
6.微信公众号开发 - 事件处理和回复消息
7.微信公众号开发 - 发送Emoji表情
项目完整代码请访问github:https://github.com/liaozq0426/wx.git
获取微信公众号token的流程
由于微信公众号token在获取后需要过一段时间才会失效,且获取token的接口每日有调用次数限制,因此我们对获取的token需要做缓存处理,避免每次用到token时都访问微信远程服务器。我们获取token的逻辑流程图如下
1)当redis缓存和数据库不存在token时,或者token已经失效时,从微信远程服务器获取token,并将获取的token缓存至数据库中。
2)当redis缓存或数据库存在token时,且token未失效时,直接返回token。
如何保证同一时间段内只请求微信服务器一次?
在考虑高并发的情况下,同一时间段内可能会调用微信服务器接口多次,为了避免这种情况出现,首先我们想到的可能是对方法进行同步处理,也就是使用synchronized
关键字
public synchronized String readAccessToken() {}
但是对整个方法同步处理的话,效率较低。我们可以对token的类型(accessType)进行同步,保证获取同一种类型的token是同步行进的,并且同时结合AtomicInteger
类型变量的原子性,保证同步的情况下,同一时间段内只请求微信服务器一次。
上图中
1)首先需要先定义一个AtomicInteger类型的变量,初始值 0
2)当开始调用微信远程接口之前,首先需要判断AtomicInteger变量的值是否为1,如果不为1,则说明此时没有其他的线程在请求微信接口,这时我们可以调用微信接口,但调用之前需要设置AtomicInteger的值为1,表示已经有线程在请求微信接口了;如果AtomicInteger变量值为1,说明已经有线程在请求微信接口,此时就不要再次请求微信接口,而是循环读取缓存中的token
3)当有线程获取token成功后,需要将AtomicInteger变量重新设置为0,并将新token更新至缓存中。
4)对于accessType.intern()
的理解,accessType为token的类型,在平时微信公众号开发中,常用的有access_token
和jsapi_ticket
两种,前者为基础token,后者为使用jsapi时需要用到的token;intern()
方法解释起来比较复杂,可以查阅资料了解一下。
wx_token表设计,存放token
CREATE TABLE `wx_token` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',`platform` varchar(20) DEFAULT NULL COMMENT '公众号标识',`token_type` varchar(20) NOT NULL COMMENT 'token类型,access_token:基础token,jsapi_token:jsapi_ticket',`access_token` text NOT NULL COMMENT 'token值',`expires_in` int(11) NOT NULL COMMENT '失效时长',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`last_upd_time` timestamp NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最后一次更新时间',`refresh_count` int(11) DEFAULT NULL COMMENT '刷新次数',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
微信公众号开发一般会用到两个token
1)access_token :是公众号的全局唯一接口调用凭据
2)jsapi_token:jsapi_ticket,是公众号用于调用微信JS接口的临时票据
我们可以将两种不同的token存储在一张表,便于维护
编写获取token的核心代码
由于获取token的代码较多,这里只展示了部分核心代码,如果想看完成代码,请通过github获取
以下是wx_token业务接口和实现类代码,其中WxTokenService中有4个接口,代码如下
package com.gavin.service;import java.util.List;import com.gavin.pojo.AccessToken;
import com.gavin.pojo.WxToken;
public interface WxTokenService {public List<WxToken> select(WxToken token) throws Exception;public WxToken selectOne(WxToken token) throws Exception;public int save(WxToken token) throws Exception;public AccessToken readAccessToken(String accessType , String platform) throws Exception;
}
其中前三个接口为查询和保存wx_token记录,最后一个方法readAccessToken
比较复杂,会依次从redis缓存、数据库、微信服务器中获取token,只要任意一个步骤获取成功就返回token。
WxTokenServiceImpl实现类代码如下
package com.gavin.service.impl;import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.gavin.cfg.RedisService;
import com.gavin.mapper.WxTokenMapper;
import com.gavin.pojo.AccessToken;
import com.gavin.pojo.Wechat;
import com.gavin.pojo.WxToken;
import com.gavin.service.WxCfgService;
import com.gavin.service.WxTokenService;
import com.gavin.util.WxUtil;@Service
public class WxTokenServiceImpl implements WxTokenService , DisposableBean {private Logger logger = Logger.getLogger(this.getClass());public static final int DEFAULT_ACCESS_TOKEN_EXPIRESIN = 120;public static Map<String , AtomicInteger> tokenSyncMap = new ConcurrentHashMap<>();@Autowiredprivate WxTokenMapper wxTokenMapper;@Autowiredprivate RedisService redisService;@Autowiredprivate WxCfgService wxCfgService;/*** @title 查询token集合* @author gavin* @date 2019年11月27日*/@Overridepublic List<WxToken> select(WxToken token) throws Exception {return wxTokenMapper.select(token);}/*** @title 查询单个token* @author gavin* @date 2019年11月27日*/@Overridepublic WxToken selectOne(WxToken token) throws Exception {List<WxToken> tokenList = select(token);if(tokenList.size() == 1)return tokenList.get(0);logger.info("查询结果集不符合预期");return null;}/*** @title 保存token至数据库* @author gavin* @date 2019年11月27日*/@Overridepublic int save(WxToken token) throws Exception {Integer id = token.getId();if(id != null && id > 0) {// 更新return this.wxTokenMapper.update(token);}else {// 新增return this.wxTokenMapper.insert(token);}}/*** @title 读取微信token* @author gavin* @date 2019年11月27日*/@Overridepublic AccessToken readAccessToken(String accessType, String platform) throws Exception {// 1.尝试从redis中读取AccessToken token = null;try {token = readAccessTokenByRedisAndDb(accessType , platform);if(token != null && !StringUtils.isBlank(token.getAccess_token()))return token;if(tokenSyncMap.get(accessType) != null && tokenSyncMap.get(accessType).get() > 0) {while(tokenSyncMap.get(accessType).get() > 0) {// 此时正在向微信服务器请求token,阻塞等待Thread.sleep(100);logger.info("正在向微信服务器请求token,阻塞等待...");}token = readAccessTokenByRedisAndDb(accessType , platform);if(token != null && !StringUtils.isBlank(token.getAccess_token()))return token;elsereturn null;}else { // 3.尝试从微信服务器上获取// 同步intern,保证在同一时间段内仅访问远程服务器一次String intern = accessType.intern();synchronized (intern) { tokenSyncMap.put(accessType, new AtomicInteger(1));try {if(AccessToken.TYPE_ACCESS_TOKEN.equals(accessType) || AccessToken.TYPE_JSAPI_TOKEN.equals(accessType)) { Wechat wechat = wxCfgService.selectWechat(platform);if(AccessToken.TYPE_ACCESS_TOKEN.equals(accessType)) { logger.info("从微信服务器上获取access_token");token = WxUtil.getAccessToken(wechat.getBase64DecodeAppId(), wechat.getBase64DecodeAppSecret());}if(AccessToken.TYPE_JSAPI_TOKEN.equals(accessType)) {logger.info("从微信服务器上获取js_ticket");AccessToken Atoken = readAccessToken(AccessToken.TYPE_ACCESS_TOKEN , platform);token = WxUtil.getJSTicket(Atoken.getAccess_token());}}if(token != null && !StringUtils.isBlank(token.getAccess_token())) { token.setAccess_type(accessType);// 缓存tokencacheAccessToken(token , platform);}else {logger.error("从微信服务器上获取access_token失败");}} catch (Exception e) {logger.error("从微信服务器上获取access_token失败");logger.error(e.getMessage() , e);} finally {tokenSyncMap.get(accessType).decrementAndGet();logger.info("tokenSyncMap." + accessType + " count:" + tokenSyncMap.get(accessType).get());}}return token;}} catch (Exception e) {logger.error(e.getMessage(), e);throw new Exception("读取access_token失败");}}/*** @title 从redis和数据库中读取accessToken* @param accessType* @return*/private AccessToken readAccessTokenByRedisAndDb(String accessType , String platform) {if(StringUtils.isBlank(accessType)) return null;String redisKey = null;if(!StringUtils.isBlank(accessType)) {// 生产redisKeyredisKey = makeAccessTokenRedisKey(accessType , platform);}AccessToken token = null;try {Object obj = null;// 1.尝试从redis中读取logger.info("尝试从redis中读取...");obj = redisService.get(redisKey);if(obj != null) token = (AccessToken) obj;if(token != null && !StringUtils.isBlank(token.getAccess_token())) {long tokenCreateTime = token.getCreate_time();logger.info("tokenCreateTime:" + tokenCreateTime);long interval = (System.currentTimeMillis() - tokenCreateTime) / 1000;logger.info("interval:" + interval);if(interval <= (token.getExpires_in() - DEFAULT_ACCESS_TOKEN_EXPIRESIN)) { return token;}else {logger.info("redis中的accessToken已经失效");redisService.del(redisKey);}}} catch (Exception e) {logger.error("尝试从redis中读取access_token失败");logger.error(e.getMessage() , e);}// 2.尝试从数据库中获取try {logger.info("尝试从数据库中读取...");WxToken wxTokenParam = new WxToken();wxTokenParam.setTokenType(accessType);wxTokenParam.setPlatform(platform);WxToken wxToken = this.selectOne(wxTokenParam); if(wxToken != null) {// 判断token是否失效int expiresIn = wxToken.getExpiresIn();Date lastUpdTime = wxToken.getLastUpdTime();logger.info("System.currentTimeMillis:" + System.currentTimeMillis());logger.info("lastUpdTime:" + lastUpdTime.getTime() + ",format:" + lastUpdTime);long interval = (System.currentTimeMillis() - lastUpdTime.getTime()) / 1000;logger.info("interval:" + interval);if(interval <= (expiresIn - DEFAULT_ACCESS_TOKEN_EXPIRESIN)) {token = new AccessToken();token.setAccess_token(wxToken.getAccessToken());token.setAccess_type(wxToken.getTokenType());token.setExpires_in(wxToken.getExpiresIn());token.setCreate_time(wxToken.getLastUpdTime().getTime());// 同步至redis// long redisExpires = System.currentTimeMillis() - wxToken.getLastUpdTime().getTime();long redisExpires = expiresIn - interval;redisService.set(redisKey, token , redisExpires);return token; }}} catch (Exception e) {logger.error("尝试从数据库中读取access_token失败");logger.error(e.getMessage() , e);}return null;}/*** @title 缓存access token,1.缓存至redis 2.缓存至数据库* @author gavin* @date 2019年5月23日* @param accessToken* @param platform* @throws Exception*/public void cacheAccessToken(AccessToken accessToken , String platform) throws Exception {// 如果token的创建时间为空,则必须设置(从微信服务器获取到token时create_time为空)if(accessToken.getCreate_time() == 0) { accessToken.setCreate_time(new Date().getTime());logger.info("设置token创建时间");}logger.info("accessToken_createTime:" + accessToken.getCreate_time());logger.info("System.currentTimeMillis:" + System.currentTimeMillis());// 1.缓存至redisString redisKey = null;String accessType = accessToken.getAccess_type();if(!StringUtils.isBlank(accessType)) { redisKey = makeAccessTokenRedisKey(accessType , platform);redisService.set(redisKey, accessToken, accessToken.getExpires_in());logger.info("缓存" + platform + " " + accessType + "至redis成功");// 2.缓存至数据库WxToken tokenParam = new WxToken();tokenParam.setTokenType(accessType);tokenParam.setAccessToken(accessToken.getAccess_token());tokenParam.setExpiresIn(accessToken.getExpires_in());tokenParam.setPlatform(platform);// 1.先查询数据库中是否存在记录int result = 0;WxToken wxAccessToken = this.selectOne(tokenParam);if(wxAccessToken == null) {// 首次插入tokenParam.setRefreshCount(0);result = this.wxTokenMapper.insert(tokenParam);}else {// 更新if(wxAccessToken.getRefreshCount() == null) {tokenParam.setRefreshCount(1);}else { tokenParam.setRefreshCount(wxAccessToken.getRefreshCount() + 1);}tokenParam.setId(wxAccessToken.getId());result = this.wxTokenMapper.update(tokenParam);}if(result == 1) logger.info("缓存" + platform + " " + tokenParam.getTokenType() + "至数据库成功");elselogger.info("缓存" + platform + " " + tokenParam.getTokenType() + "至数据库失败");}}/*** @title access_token redis 缓存 key规则* @author gavin* @date 2019年5月23日* @param accessType* @param platform* @return*/private String makeAccessTokenRedisKey(String accessType , String platform) {String redisKey = platform + "_" + accessType;return redisKey;}/*** @title 销毁时清空缓存* @author gavin* @date 2019年11月27日*/@Overridepublic void destroy() throws Exception {tokenSyncMap.clear();System.out.println("tokenSyncMap清空了,size:" + tokenSyncMap.size());}}
微信公众号开发 - token获取(保证同一时间段内只请求一次)相关推荐
- java微信公众号开发token验证失败的问题及解决办法
java微信公众号开发token验证失败的问题及解决办法 参考文章: (1)java微信公众号开发token验证失败的问题及解决办法 (2)https://www.cnblogs.com/beardu ...
- 【微信公众号开发】获取并保存access_token、jsapi_ticket票据(可用于微信分享、语音识别等等)...
步骤一:首先得开通公众号(目的是 获得appid.AppSecret.设置安全域名)~ [公众号设置]→[功能设置] 设置相应的域名 步骤二:编写帮助类WeixinLuyinHelper中的代码 #r ...
- 微信公众号开发之获取用户地理位置
使用微信的用户地理位置接口就要配置这里. 前端代码: function configWx() {var thisPageUrl = location.href.split('#')[0];$.ajax ...
- 微信公众号开发:获取openId和用户信息(完整版)
注:之前总结怎么进行本地公众号开发调试,时间一长忘记开发配置却忘了,所以这里记录一下公众号开发配置,方便快速上手. 目录 开发前服务器配置 网页授权获取用户基本信息 snsapi_base snsap ...
- 微信公众号开发系列-获取微信OpenID
在微信开发时候在做消息接口交互的时候需要使用带微信加密ID(OpenId),下面讲讲述2中类型方式获取微信OpenID,接收事件推送方式和网页授权获取用户基本信息方式获取. 1.通过接收被动消息方式获 ...
- php 公众号token认证,微信公众号开发——Token认证
公众号开发第一步就是绑定Token,Token认证相当于把我们的公众号和服务器关联起来,只有Token认证成功了我们的服务器才能接收到来自公众号的消息.微信官方回调的地址必须能在公网上访问,后端服务的 ...
- Node微信公众号开发 - 定时获取最新文章同步到MySQL数据库
0.介绍 本文源码:https://github.com/Jameswain/... 最近有一个需求:把5个公众号的所有文章定时同步到小程序的数据库里,10分钟同步一次.实现这个需求当时我 ...
- 微信公众号开发之获取用户信息
微信获取用户信息的方式有两种,静默授权(无需用户同意)和非静默授权(需要用户" 手动点击 "拉取授权,可以用户无需关注公众号即可获取用户信息) 整体的代码请查看最后,前边为原理介绍 ...
- 微信公众号开发之获取oppenid和用户基本信息
前言: 在微信公众号请求用户网页授权之前,开发者需要先在自己的公众平台配置好基本配置,修改授权回调域名JS安全域名.并且需要先获取到全局access_token,这里不对全局access_token的 ...
最新文章
- docker安装kibana7.6.1
- list clear 2 python,python中怎么将列表的数据清空
- 左值、右值、左值引用、右值引用
- 推荐 10 个有趣的 Python 项目
- 基于Android 虹软人脸、人证对比,活体检测
- 二层交换机、三层交换机和路由器的基本工作原理和三者之间的主要区别
- [Octotree] 树形展示GitHub项目
- 74系列芯片引脚图资料大全
- 计算机四级(网络工程师)内容,计算机四级《网络工程师》考试内容
- CodeBook 可以自定义字符集的密码本
- 有赞搜索系统的技术内幕
- BZOJ 1127: [POI2008]KUP 最大子矩阵
- 表达式的LenB(123程序设计ABC)的值是
- .net ref java_Java URL.getRef方法代碼示例
- 大数据剖析 | 北京VS上海: 活着为了工作还是工作为了生活?
- linux之pmap命令查看进程的地址空间和占用的内存
- 对言语上的自律和真正的自律的一些想法
- 西瓜直播弹幕阅读器 python
- win10设置微信双开电脑登录多个微信,超级详细教程,小白也可轻松设置
- 美团美食板块的token加密