公众号权限集: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左右(线上可能会更低一点)