文档资料:
微信官方文档获取ACCESS_TOKEN:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html微信开发者平台文档https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html 微信官方文档根据code及ACCESS_TOKEN获取手机号:
1:获取流程
1.1:预先申请好的小程序APPID及SECRET(在小程序管理员中可以看到)
1.2:通过APPID及SECRET获取到微信服务器上该小程序的ACCESS_TOKEN
1.3:前端获取到微信用户的code
1.4:后端接收到前端传输的code,使用code及ACCESS_TOKEN获取到微信用户的手机号
2:代码片段
/**
* 微信相关业务层
*/
@Service
public class WxServiceImpl implements IWxService {
private static Logger logger = LoggerFactory.getLogger(WxServiceImpl.class);
// APPID
@Value("${wechat.appid}")
private String appId;
// SECRET
@Value("${wechat.secret}")
private String appSecret;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/** 微信小程序获取Access_Token地址 */
public static String WX_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=SECRET";
/** 微信小程序获取手机号地址 */
public static String WX_SJH_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN";
/** access_token 缓存Key */
public static String REDIS_ACCESS_TOKEN_KEY = "qdfp:wx:accesstoken";
/** access_token 缓存击穿Lock */
public static String REDIS_ACCESS_TOKEN_LOCK = "qdfp:lock:accesstokenlock";
/**
* 获取ACCESS_TOKEN 及 ACCESS_TOKEN刷新策略
* */
@Override
public String getAccessToken() {
// 判断缓存中是否有数据
String token = stringRedisTemplate.opsForValue().get(REDIS_ACCESS_TOKEN_KEY);
if(StrUtil.isNotBlank(token)){ // 缓存命中直接返回ACCESS_TOKEN
return token;
}
try {
// 缓存失效、重建缓存
if(!tryLock(REDIS_ACCESS_TOKEN_LOCK)){ // 此处没有获取到锁,表示其他线程获取到锁在执行缓存重建
Thread.currentThread().sleep(1000); // 休眠1000ms,避免一直重试获取锁
return getAccessToken(); // 递归尝试获取锁
}
// 获取锁成功、重建缓存
token = stringRedisTemplate.opsForValue().get(REDIS_ACCESS_TOKEN_KEY);
if(StrUtil.isBlank(token)){ // 缓存中没有数据、重建缓存
// 缓存中没有ACCESS_TONKEN信息,去调用微信接口获取ACCESS_TOKEN
HttpResponse response = HttpRequest.get(
WX_TOKEN_URL
.replaceAll("APPID", appId)
.replaceAll("SECRET", appSecret)
).execute();
if(response.getStatus() == 200){
Map repMap = JSONUtil.toBean(response.body(), Map.class);
token = (String) repMap.get("access_token");
// 将获取到的ACCESS_TOKEN写入缓存,并设置110分钟的过期时间(官方文档ACCESS_TOKEN失效时间为120分钟,程序将在110分钟的时间进行刷新)
stringRedisTemplate.opsForValue().set(REDIS_ACCESS_TOKEN_KEY,token,110L, TimeUnit.MINUTES);
}
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
// 释放锁
unLock(REDIS_ACCESS_TOKEN_LOCK);
}
return token;
}
/**
* 通过code换取微信手机号
*/
@Override
public Map getWxsjh(Map map) {
// 参数校验
AssertUtils.isNotBlank((String)map.get("code"),"code不能为空");
// 用于封装响应结果
Map<String, Object> resultMap = new HashMap<>();
// 获取access_token
String accessToken = getAccessToken();
// 重新构建code参数
Map param = new HashMap<>();
param.put("code",map.get("code"));
// 构建获取微信手机号请求
HttpResponse response = HttpRequest.post(
WX_SJH_URL.replaceAll("ACCESS_TOKEN", accessToken)
).body(JSONUtil.toJsonStr(param)).execute();
// 返回响应
if(response.getStatus() != 200){
throw new IMsgEx("获取手机号异常,请稍后再试");
}
JSONObject repObj = JSONUtil.parseObj(response.body());
// 获取微信响应错误码
Integer errcode = repObj.getInt("errcode");
if(errcode != 0){ // 响应异常
throw new IMsgEx(repObj.getStr("errmsg"));
}
Map phoneInfoMap = repObj.get("phone_info", Map.class);
// 获取没有区号的手机号
String purePhoneNumber = (String) phoneInfoMap.get("purePhoneNumber");
AssertUtils.isNotBlank(purePhoneNumber,"获取手机号失败,请稍后再试");
resultMap.put("purePhoneNumber",purePhoneNumber);
return resultMap;
}
/**
* 获取锁方法 使用Redis的SETNX命令实现锁
* SETNX命令解释:
* 往Redis中添加 K - V 键值数据,当添加的Key在Redis中不存在时
* 才添加成功,返回0/1代表添加失败/成功。
* */
private boolean tryLock(String key){
// 为锁设置的过期时间是需要大于缓存重建的时间,设置过期时间的目的是防止程序异常出现“死锁”问题
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag); // 使用Hutool工具类将Bolean类型转bolean
}
/** 释放锁方法 即删除锁 */
private void unLock(String key){
stringRedisTemplate.delete(key);
}
}
说明:
对于ACCESS_TOKEN的刷新没有采用轮询程序定时去更新的方式刷新ACCESS_TOKEN,通过Redis的SET NX命令实现分布式锁 + 为Key设置过期时间 让用户调用获取手机号码接口时自动刷新ACCESS_TOKEN。
微信官方对ACCESS_TOKEN刷新的部分说明:
ACCESS_TOKEN的有效期通过返回的 ACCESS_TOKEN来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。在刷新过程中,中控服务器可对外继续输出的老 access_token
,此时公众平台后台会保证在5分钟内,新老 access_token
都可用,这保证了第三方业务的平滑过渡;