公众号权限集:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/product/offical_account_authority.html

小程序权限集:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/product/miniprogram_authority.html

0、价值分析

0.0、数据

0.0.1、日志

周错误日志数量:1475

接口 错误日志数量 URL
获取微信用户基础信息 1238 https://api.weixin.qq.com/cgi-bin/user/info
发送模板消息 141 https://api.weixin.qq.com/cgi-bin/message/template/send
获取帐号关注者列表 40 https://api.weixin.qq.com/cgi-bin/user/get
创建公众号二维码 29 https://api.weixin.qq.com/cgi-bin/qrcode/create
获取小程序/公众号绑定的开放平台 27 https://api.weixin.qq.com/cgi-bin/open/get

0.0.1、接口使用情况(近3个月)

0.0.1.1、获取微信用户基础信息

影响接口 响应时间 请求数
com.f6car.ranger.api.wx.user.WxUserInfoEngineerApi#getUserInfo 204.2ms 544.8K
接口所需微信公众号权限 funcscope_id 备注
用户管理权限 2 指公众号第三方平台方在获得该权限后,可以获得被授权公众号的用户管理权限。具体包括:1) 获取用户基本信息接口 2) 获取用户列表接口 3) 用户分组管理接口4) 用户备注名设置接口 5) 设置用户地理位置的开关及上报方式 6) 获取用户地理位置上报的事件推送 7) 关注/取消关注公众号事件推送
使用场景 工程 备注
SCAN(普通扫码)\subscribe(订阅)参数解析 outback 必须调用
关注公众号参与推荐有礼活动 outback 可以读取参数解析后的数据,不必调用,可优化

0.0.1.2、发送模板消息

影响接口 响应时间 请求数
com.f6car.ranger.api.wx.message.WxMpTemplateMsgEngineerApi#sendWxTemplateMsg 281.5ms 3893.2K

0.0.1.3、获取帐号关注者列表

影响接口 响应时间 请求数
com.f6car.ranger.api.wx.user.WxUserInfoEngineerApi#getUserOpenIdList 278.2ms 403.1K

0.0.1.4、创建公众号二维码

影响接口 响应时间 请求数
com.f6car.ranger.api.wx.qrcode.WechatQrCodeBusinessApi#createOfficialAccountQrCode
com.f6car.ranger.api.wx.qrcode.WechatQrCodeConsumerApi#createOfficialAccountQrCode
com.f6car.ranger.api.wx.qrcode.WechatQrCodeEngineerApi#createOfficialAccountQrCode
com.f6car.ranger.api.wx.qrcode.WechatQrCodeBusinessApi#createOfficialAccountQrCodeBase64 229.4ms 980.7K
com.f6car.ranger.api.wx.qrcode.WechatQrCodeConsumerApi#createOfficialAccountQrCodeBase64 223.7ms 42.2K
com.f6car.ranger.api.wx.qrcode.WechatQrCodeEngineerApi#createOfficialAccountQrCodeBase64
接口所需微信权限 funcscope_id 备注
(公众号)帐号服务权限 3 该权限主要进行公众帐号的帐号管理,包括获取和设置公众帐号信息,由于可供多方获取和设置,故授权不互斥。具体包括:1) 生成带参数二维码接口 2) 扫描带参数二维码时(而不是关注后)的事件接收(请注意,带参数二维码扫码后关注的事件推送,是会推送给具备消息与菜单管理权限集的第三方) 3) 获取公众帐号基础信息(头像昵称/帐号类型等,此接口暂为服务方独有) 4)上传下载多媒体文件接口
(小程序)帐号管理权限 17 该权限主要进行小程序的帐号管理,包括生成带参数二维码接口、生成小程序码等,由于可供多方获取和设置,故授权不互斥。具体包括:获取小程序二维码获取小程序码,适用于需要的码数量较少的业务场景获取小程序码,适用于需要的码数量极多的业务场景将二维码长链接转成短链接

0.0.1.5、获取小程序/公众号绑定的开放平台

影响接口 响应时间 请求数
com.f6car.ranger.api.wx.WechatOpenAccountApi#getWechatOpenAccount 139.9ms 77
com.f6car.ranger.api.app.PAppStationBusinessApiImpl#getPAppStationInfoWithOpenAccount 241.4ms 5764
com.f6car.ranger.api.app.PAppStationBusinessApi#queryAuthAndBindingInfo 436.4ms 5415
com.f6car.ranger.api.wx.AppAndFuncscopeApi#queryAppAndFuncscopeList 748.8ms 11.7K
com.f6car.ranger.api.wx.WechatOpenAccountApi#createWechatOpenAccount
com.f6car.ranger.api.wx.WechatOpenAccountApi#bindWechatOpenAccount 1.003s 903
com.f6car.ranger.api.wx.WechatOpenAccountApi#unBindWechatOpenAccount

0.1、客户价值

  • 发生48001错误后,能及时刷新客户微信app信息,如果微信认证失败,可以联系客户去认证,免得影响客户微信app使用。
  • 发送模板消息时,能直观看到因为权限认证等发送失败原因。
  • 即使解决权限问题可以避免每晚微信公众号粉丝数据的更新。
  • 可以在生成公众号台卡二维码时,直观展示未生成原因,不必报错,优化用户体验。
  • 绑定开放平台,错误提示,优化用户体验。

0.2、技术价值

  • 解决因为权限不足调用接口产生的48001错误日志而引发的告警问题。
  • 在接口调用前,进行权限判断,如果权限不足,则不必调用接口,节省计算机资源。
  • 节省开发人力,避免每次因为48001错误日志,定位问题产生的花销。

1、如何避免发生48001(API 功能未授权)

前提:p_app_station中的微信认证状态与微信官方同步(通过标题2解决)

  • 判断p_app_station:删除状态、授权状态
  • 判断公众号类型 :0订阅号,1由历史老帐号升级后的订阅号,2服务号
  • 判断微信认证状态
  • 判断wx_app_funcscope权限

2、刷新p_app_station思路

  • 发生48001错误,获取appid
  • 放到redis

每个appid每天只执行一次

  • 监听redis,刷新公众号或者小程序
  • 如果是微信未认证,则当天此公众号或者小程序不打印48001 error日志
  • 对接钉钉,将微信认证过期的公司发出来,让运营处理

2.2、redis结构

2.2.1、pub/sub

发生48001错误时,直接推送appId到redis,订阅主题的机器刷新此appId。

不能控制刷新appId速度,会发生一个appId重复推送情况。

2.2.2、hash

结构:48001-appid:20210628 ===== appid===== 0/1/2(标记)

  • 发生48001错误时,如果此appId在redis中不存在,则将数据存储到redis的hash结构中(默认值为0),如果存在,则不处理。
  • xxlJob,轮询扫描redis

​ 1)如果此appId的值未0,刷新appId:

​ 1.1)如果此appId对应的绑定及认证状态正常将redis对应的值改为1;

​ 1.2) 如果此appId对应的绑定及认证状态不正常将redis对应的值改为2;

​ 2)如果此appId的值为1,删除key;

​ 3)如果此appId的值为2,钉钉告警;

3、调用微信接口统一权限校验及异常处理

3.1、思路

  • 提供自定义注解,可配置需要的参数
  • aop拦截自定义注解,方法执行前校验权限,拦截并过滤方法抛出的异常
  • 抛出异常如果是48001,则刷新p_app_station

3.2、自定义注解设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.lang.annotation.*;

/**
* <Description> 微信方法权限<br>
*
* @author LiZhiMing<br>
* @create 2021-06-28 15:05<br>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WxFuncAuth {
/**
* 方法权限列表
*
* @return 方法权限列表
*/
String[] funcs() default {};

/**
* 微信认证状态,默认:认证
*
* @return 微信认证状态
*/
boolean isVerify() default true;

/**
* appId
*
* @return appId
*/
String appId() default "";
}

3.3、注解AOP拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.f6car.ranger.aop;

import com.alibaba.fastjson.JSON;
import com.f6car.ranger.enums.app.ServiceTypeInfoEnum;
import com.f6car.ranger.enums.app.VerifyTypeInfoEnum;
import com.f6car.ranger.exception.WxServiceException;
import com.f6car.ranger.manager.app.AppAndFuncscopeCheckManager;
import com.f6car.ranger.so.app.CheckAppAndFuncscopeSo;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxMaErrorMsgEnum;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
* <Description> 微信方法权限注解拦截<br>
*
* @author LiZhiMing<br>
* @create 2021-06-28 15:11<br>
*/
@Aspect
@Component
@Slf4j
public class WxFuncAuthAspect {
/**
* 用于SpEL表达式解析.
*/
private SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Autowired
private AppAndFuncscopeCheckManager appAndFuncscopeCheckManager;
@Autowired
private WxCode48001Processor wxCode48001Processor;

@AfterThrowing(throwing = "ex", value = "@annotation(wxFuncAuth)")
public void wxFuncAuth(JoinPoint joinPoint, Throwable ex, WxFuncAuth wxFuncAuth) {
//获取appId
String appId = generateKeyBySpringEL(wxFuncAuth.appId(), joinPoint);
log.info("appId:{}", appId);
if (ex instanceof WxServiceException) {
if (((WxServiceException) ex).getErrorCode() == WxMaErrorMsgEnum.CODE_48001.getCode()) {
wxCode48001Processor.push(appId);
}
}

}

@Before(value = "@annotation(wxFuncAuth)")
public void wxFuncAuth(JoinPoint joinPoint, WxFuncAuth wxFuncAuth) {
//获取appId
String appId = generateKeyBySpringEL(wxFuncAuth.appId(), joinPoint);
log.info("appId:{}", appId);
if (StringUtils.isBlank(appId)) {
throw new WxServiceException("appId不存在");
}
log.info("funcs:{}", JSON.toJSONString(wxFuncAuth.funcs()));
//权限判断
CheckAppAndFuncscopeSo build = CheckAppAndFuncscopeSo.builder()
//服务号
.needServiceTypeList(Lists.newArrayList((byte) ServiceTypeInfoEnum.FWH.getType()))
//微信认证
.needVerifyTypeList(VerifyTypeInfoEnum.getAllType())
//方法权限
.funcscopeIdList(Lists.newArrayList(wxFuncAuth.funcs()))
.build();
build.setAppId(appId);
boolean isLegal = appAndFuncscopeCheckManager.checkAppAndFuncscope(build);
if (!isLegal) {
throw new WxServiceException("接口无权调用");
}
}

/**
* 根据el表达式字符串生成key
*
* @param elString el表达式字符串
* @param joinPoint joinPoint
* @return key
*/
private String generateKeyBySpringEL(String elString, JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
Expression expression = parser.parseExpression(elString);
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(Objects.requireNonNull(paramNames)[i], args[i]);
}
return Objects.requireNonNull(expression.getValue(context)).toString();
}
}

推送48001异常的appId到redis:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
* <Description> 微信48001异常处理<br>
*
* @author LiZhiMing<br>
* @create 2021-06-28 18:29<br>
*/
@Component
@Slf4j
public class WxCode48001Processor {
@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* 将发生48001异常的appId推送到redis
*
* @param appId appId
*/
public void push(String appId) {
try {
//如果key-field不存在
if (!stringRedisTemplate.opsForHash().hasKey(get48001Key(), appId)) {
log.info("48001异常,需刷新p_app_station,appId:{}", appId);
stringRedisTemplate.opsForHash().put(get48001Key(), appId, "0");
}
} catch (RuntimeException e) {
log.warn("48001appId推送到redis异常,appId:{}", appId, e);
}

}

/**
* 获取48001redis key
*
* @return 48001redis key
*/
private static String get48001Key() {
return "wx_48001:" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
}
}

3.4、使用及测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* <Description> 微信方法权限测试<br>
*
* @author LiZhiMing<br>
* @create 2021-06-28 16:50<br>
*/
@Component
public class WxFuncAuthAnnotationCaller {
@WxFuncAuth(appId = "#appId", funcs = {"1", "2"})
public String callNormal(String appId) {
return appId + ": I'm very good";
}
@WxFuncAuth(appId = "#appId", funcs = {"1", "2"})
public String callError(String appId) {
throw new WxServiceException(appId+"权限不足",48001);
}
}

3.5、刷新p_app_station xxlJob

48001异常微信APP权限信息刷新

wxAppAuthRefreshJobHandler

1
0 0/5 * * * ?

参数:发生48001异常的wxAppId(字符串)

4、微信app及权限校验优化

4.1、现有逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public Boolean checkAppAndFuncscope(CheckAppAndFuncscopeSo checkAppAndFuncscopeSo) {
Preconditions.checkArgument(StringUtils.isNotEmpty(checkAppAndFuncscopeSo.getAppId()), "checkAppAndFuncscope中wxAppId不能为空");
Preconditions.checkArgument(CollectionUtils.isNotEmpty(checkAppAndFuncscopeSo.getNeedServiceTypeList()) || CollectionUtils.isNotEmpty(checkAppAndFuncscopeSo.getNeedVerifyTypeList()) || CollectionUtils.isNotEmpty(checkAppAndFuncscopeSo.getFuncscopeIdList()), "checkAppAndFuncscope中校验内容不能为空");
PAppStationEngineerSo pAppStationEngineerSo = new PAppStationEngineerSo();
pAppStationEngineerSo.setWxAppId(checkAppAndFuncscopeSo.getAppId());
pAppStationEngineerSo.setIsAuthorized(AuthorizationType.BE_AUTHORIZED.getValue().intValue());
Boolean checkResult = Boolean.TRUE;
PAppStationVo pAppStationVo = pAppStationEngineerService.getPAppStationInfo(pAppStationEngineerSo);
if (pAppStationVo != null) {
//1.校验对应的App
checkResult = checkApp(checkAppAndFuncscopeSo, checkResult, pAppStationVo);
//2.校验对应的funscope
if (checkResult) {
WxAppFuncscopeSo build = WxAppFuncscopeSo.builder().funcscopeIdList(checkAppAndFuncscopeSo.getFuncscopeIdList()).wxAppId(checkAppAndFuncscopeSo.getAppId()).permissionGroupList(Sets.newHashSet(pAppStationVo.getGroupId())).build();
checkResult = wxAppFuncscopeService.checkFuncscopeIdListByWxAppId(build);
}
} else {
log.warn("校验AppAndFunscope失败,没有查询到对应pAppStation,入参={}", JSON.toJSONString(checkAppAndFuncscopeSo));
checkResult = Boolean.FALSE;
}
return checkResult;
}

会查询mysql数据库,表p_app_station和表wx_app_funcscope,如果所有调用微信的接口都需要走校验,那么这一步的性能消耗是巨大的。

4.2、优化思路

使用J2Cache缓存。

4.2.1、缓存结构

  • p_app_station缓存对象:WX_APP_ID、WX_APP_NAME、is_authorized、type、service_type_info、verify_type_info;
  • wx_app_funcscope缓存对象:wx_app_id、funcscope_id(多个)

4.2.2、缓存查询及添加

1
2
3
4
5
6
7
8
  @Override
@Cacheable(value = "appStation", key = "'queryPAppStationFromCache'+#appId", condition = "#appId!=null", unless = "#result==null")
public PAppStationCacheVo queryPAppStationFromCache(String appId) {
}
@Override
@Cacheable(value = "appFuncscope", key = "'listFuncscopeIdFromCache'+#appId", condition = "#appId!=null", unless = "#result==null")
public List<String> listFuncscopeIdFromCache(String appId) {
}

4.2.3、清除缓存

在同步微信基础信息及权限信息的时候清缓存

1
2
3
4
5
6
7
@Caching(evict = {
@CacheEvict(value = "appStation", key = "'queryPAppStationFromCache'+#result.wxAppId"),
@CacheEvict(value = "appFuncscope", key = "'listFuncscopeIdFromCache'+#result.wxAppId")
})
public PAppStationVo refreshAuthInfo(PAppStationVo pAppStationVo) {

}

4.2.4、微信接口调用权限校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 校验微信app基础信息
*
* @param checkAppAndFuncscopeSo needServiceTypeList 帐号类型
* needVerifyTypeList 认证类型
* appId appId
*/
void checkAppStation(CheckAppAndFuncscopeSo checkAppAndFuncscopeSo);

/**
* 校验微信权限信息
*
* @param checkAppAndFuncscopeSo funcscopeIdList 方法权限集
* appId appId
*/
void checkAppFuncscope(CheckAppAndFuncscopeSo checkAppAndFuncscopeSo);

4.3、本地执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 第一次调用
校验微信app基础信息,耗时:3010
校验微信权限信息,耗时:285

-- 存在缓存后调用
校验微信app基础信息,耗时:73
校验微信权限信息,耗时:75
校验微信app基础信息,耗时:71
校验微信权限信息,耗时:64
校验微信app基础信息,耗时:63
校验微信权限信息,耗时:84
校验微信app基础信息,耗时:70
校验微信权限信息,耗时:65
校验微信app基础信息,耗时:65
校验微信权限信息,耗时:65

校验逻辑耗时需要150ms左右(线上可能会更低一点)