公众号权限集: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
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.*;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WxFuncAuth {
String[] funcs() default {};
boolean isVerify() default true;
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;
@Aspect @Component @Slf4j public class WxFuncAuthAspect {
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) { 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) { 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("接口无权调用"); } }
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;
@Component @Slf4j public class WxCode48001Processor { @Autowired private StringRedisTemplate stringRedisTemplate;
public void push(String appId) { try { 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); }
}
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
|
@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
参数:发生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) { checkResult = checkApp(checkAppAndFuncscopeSo, checkResult, pAppStationVo); 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
|
void checkAppStation(CheckAppAndFuncscopeSo checkAppAndFuncscopeSo);
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左右(线上可能会更低一点)