微信第三方平台授权介绍

被微信开放文档绕的云里雾里,在这里踩了很多坑,浪费了很多事件,现在授权模块做完了,在此发散一下思维,整理一下代码

须知

你的项目里会收到微信推送的两种事件

  • 授权事件
  • 消息事件

授权事件分为

  • 取消授权事件
  • 更新授权事件
  • 授权成功事件(我另外写成了1个接口)
  • 第三方平台验证票据推送事件——获取component_verify_ticket的事件

微信会每10分钟一次的调用component_verify_ticket推送事件,授权成功之后还有一个授权成功回调事件,但是微信那边怎么知道调用哪个位置呢?因此,需要与微信约定,它来调用你们项目里的接口

因此,你要与负责人协商,约定项目部署好之后的请求URI

你要告诉负责人这两个值,别忘记要带上自己的项目访问前缀(我不知道填完URL之后能不能修改,所以尽量一次考虑到位)

  • 授权事件接收URL
  • 消息与事件接收URL

另外,你还要思考授权成功回调的URI

让负责人去做这个事情↓ 最后你能拿到下方配置项所空缺的东西

https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/how_to_apply.html

配置项

wechat:
  open: # 开放平台
    # 消息校验TOKEN(第三方平台填写)
    third-token: 
    # 消息加解密key(第三方平台填写)
    encoding-aes-key: 
    # 第三方平台 app id(第三方平台填写)
    component-app-id: 
    # 第三方平台 app secret(第三方平台填写)
    component-app-secret: 
    # 授权成功回调URi
    notify-auth-success-event-uri: (你填写)你的项目域名/回调接口URI
    # 启动ticket推送服务
    start-push-ticket-uri: https://api.weixin.qq.com/cgi-bin/component/api_start_push_ticket
    # 兑换令牌(component_access_token)URI
    paid-component-access-token-uri: https://api.weixin.qq.com/cgi-bin/component/api_component_token
    # 兑换预授权码URI
    paid-pre-auth-code-uri: https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=
    # 跳转到扫码授权页URI
    auth-page-uri: https://mp.weixin.qq.com/cgi-bin/componentloginpage
    # 授权码获取授权信息URI
    paid-authorization-info-uri: https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=
    # 获取/刷新接口调用令牌URI
    paid-authorizer-access-token-uri: https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=
    # 获取授权方的帐号基本信息URI
    paid-authorizer-info-uri: https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=

给你的业务类加上这些东西

	@Value("${wechat.open.third-token}")
    private String thirdToken;
    @Value("${wechat.open.encoding-aes-key}")
    private String encodingAesKey;
    @Value("${wechat.open.component-app-id}")
    private String componentAppId;
    @Value("${wechat.open.component-app-secret}")
    private String componentAppSecret;
    @Value("${wechat.open.start-push-ticket-uri}")
    private String startPushTicketUri;
    @Value("${wechat.open.notify-auth-success-event-uri}")
    private String notifyAuthSuccessEventUri;
    @Value("${wechat.open.paid-component-access-token-uri}")
    private String paidComponentAccessTokenUri;
    @Value("${wechat.open.paid-pre-auth-code-uri}")
    private String paidPreAuthCodeUri;
    @Value("${wechat.open.auth-page-uri}")
    private String authPageUri;
    @Value("${wechat.open.paid-authorization-info-uri}")
    private String paidAuthorizationInfoUri;
    @Value("${wechat.open.paid-authorizer-access-token-uri}")
    private String paidAuthorizerAccessTokenUri;
    @Value("${wechat.open.paid-authorizer-info-uri}")
    private String paidAuthorizerInfoUri;

解密加密

消息解密

无论是授权还是受到的消息,都是以加密消息的形式发过来的

消息解密

这里使用了微信官方提供的demo

	@Override
    public String decrypt(String msgSignature, String timeStamp, String nonce, String receiveMsg, int type) {
        // 消息加密解密对象
        WXBizMsgCrypt pc = getWxBizMsgCrypt();
        // 明文
        String msg = null;
        try {
            assert pc != null;
            msg = pc.decryptMsg(msgSignature, timeStamp, nonce, receiveMsg,type);
        } catch (AesException e) {
            log.error("信息解码失误,有可能是msgSignature和signature没对上");
        }
        return msg;
    }

形参里的type是个人自己加的,加在了XMLParse的extract方法里

	switch (type) {
		case 1: // 用户给公众号发消息,我们(第三方平台)代收,代处理
			NodeList nodelist1_c1 = root.getElementsByTagName("Encrypt");
			NodeList nodelist2_c1 = root.getElementsByTagName("ToUserName");
			result[0] = nodelist1_c1.item(0).getTextContent();
			result[1] = nodelist2_c1.item(0).getTextContent();
			break;
		case 2: // 比如微信发的component_verify_ticket
			NodeList nodelist1_c2 = root.getElementsByTagName("Encrypt");
			NodeList nodelist2_c2 = root.getElementsByTagName("AppId");
			result[0] = nodelist1_c2.item(0).getTextContent();
			result[1] = nodelist2_c2.item(0).getTextContent();
			break;
	}

还有一个值得注意的是,我了用postman(模拟发送ticket),postman好像不能同时发送xml 和 timestamp、nonce、msgSignature数据,所以我在后续流程展示的(获取ticket)接口参数那里加上了默认值

流程

假设你已经拿到所有需要的配置了,那么你现在可以开始获取第三方平台验证票据了。

观看文档,可以发现,需要先启动ticket推送服务,只要启动一次就会一直开始推送了。

当然,是微信推送授权事件→你信息解密后发现是ticket推送事件
以及,我将授权成功回调事件单独写成了一个接口,没有放在这里↓

	@Override
    public String authorizedEvent(Map<String,Object> authorizedMap) {
        // 授权事件类型
        String infoType = (String) authorizedMap.get("InfoType");
        if(infoType.equals(WxOpenInfoTypeEnum.UN_AUTHORIZED.getValue())) { // 取消授权

        } else if(infoType.equals(WxOpenInfoTypeEnum.UPDATE_AUTHORIZED.getValue())) { // 更新授权

        } else if (infoType.equals(WxOpenInfoTypeEnum.COMPONENT_VERIFY_TICKET.getValue())) {
            log.info("开始获取第三方平台验证票据");
            return getComponentVerifyTicket(authorizedMap);
        }
        return "failure";
    }

启动ticket推送服务

业务类
	@Override
    public void startPushTicket() {
        String baseUri = startPushTicketUri;

        JSONObject params = new JSONObject();
        params.put("component_appid",componentAppId);
        params.put("component_secret", componentAppSecret);
        // 获取开启ticket推送的返回结果
        String res =  HttpUtil.post(baseUri, params);
        WxOpenStartPushTicketResultDto wxOpenStartPushTicketResultDto = null;
        if(!StringUtils.isEmpty(res)) {
            wxOpenStartPushTicketResultDto = JSONUtil.toBean(res, WxOpenStartPushTicketResultDto.class);
        }
        if (wxOpenStartPushTicketResultDto == null) {
            log.error("开启ticket推送无返回值");
            return;
        }
        if(wxOpenStartPushTicketResultDto.getErrcode() == 0) {
            log.info("启动ticket推送成功");
        }
    }

获取component_verify_ticket

接口
	/**
     * 接收微信推送的的授权事件(授权事件接收URL标记处)
     * 包括取消授权、授权更新、接受ticket事件(不包括授权成功回调)
     *
     * @param postData      微信发送过来的加密的xml格式数据
     * @param timestamp     时间戳
     * @param nonce         随机数
     * @param msgSignature  前文描述密文消息体
     * @return              API返回结果
     */
    @PostMapping("/notify/authorize")
    public String authorizedEvent(@RequestBody String postData,
                                  @RequestParam(value = "timestamp",required = false, defaultValue = "1610343671158") String timestamp,
                                  @RequestParam(value = "nonce",required = false, defaultValue = "5") String nonce,
                                  @RequestParam(value = "msg_signature",required = false, defaultValue = "ds45a5d42s") String msgSignature) {
        log.info("接收到了授权相关事件");
        Map<String,Object> authorizedMap = getMap(msgSignature,timestamp,nonce,postData,2);
        if(authorizedMap != null) {
            return wxOpenService.authorizedEvent(authorizedMap);
        }
        return "fail";
    }
    
     /**
     * 获取事件集合
     *
     * @param msgSignature      前文描述密文消息体
     * @param timestamp         时间戳
     * @param nonce             随机数
     * @param postData          加密消息
     * @param type              消息加密类型(1:消息事件,2:授权相关事件)
     * @return                  解密后的xml文件
     */
    private Map<String,Object> getMap(String msgSignature, String timestamp, String nonce, String postData, int type) {
        log.info("获取到的参数:msgSignature为{},timestamp为{},nonce为{},postData为{}",msgSignature,timestamp,nonce,postData);
        // 解密后的xml文件
        String decryptXml;
        // xml文件对应的map
        Map<String,Object> map;
        try {
            decryptXml = wxOpenService.decrypt(msgSignature,timestamp,nonce,postData,type);
        } catch (Exception e) {
            log.error("信息解码异常");
            return null;
        }
        try {
            map = XmlUtil.xmlToMap(decryptXml);
        } catch (Exception e) {
            log.error("xml转化map异常");
            return null;
        }
        return map;
    }
业务类
	@Override
    public String getComponentVerifyTicket(Map<String,Object> authorizedMap) {
        try {
            // 验证票据
            String componentVerifyTicket = (String) authorizedMap.get("ComponentVerifyTicket");
            log.info("authorizedMap的值为:{}",authorizedMap.toString());
            if(StrUtil.isEmpty(componentVerifyTicket)) {
                log.error("没有正确获取到票据,请调试程序,检查官方给的key名是否已经改变");
                return "failure";
            }
            // 更新写入ticket 是包装了RedisUtil的redis工具类
            WxOpenUtil.addOrRefreshKey(WxOpenConstant.TICKET_KEY,componentVerifyTicket, WxOpenConstant.TICKET_EXPIRES_IN);
            log.info("component_verify_ticket已经保存好了!保存的ticket为:{},过期时间为:{}",componentVerifyTicket,WxOpenConstant.TICKET_EXPIRES_IN);
            // 微信要求的返回值
            return "success";
        } catch (Exception e) {
            log.error("请检查与redis服务器的连接情况");
        }
        return "fail";
    }

扫二维码授权流程

之前收到的ticket事件属于授权事件,授权事件是由微信官方调用我们这边的接口,而授权流程,也就是授权,是我们这边主动调用的,像是微信给出的扫码和点击按钮。我这里选择的是扫二维码。

接口
	/**
     * 进行授权流程,返回授权页重定向链接
     *
     * @param tenantCode    租户码
     * @return              授权页重定向链接
     */
    @PostMapping("/authorize")
    public String authorize(@RequestHeader("tenantCode") String tenantCode) {
        log.info("开始进行授权流程");
        return wxOpenService.authorize(tenantCode);
    }
业务类
	@Override
    public String authorize(String tenantCode) {
        String componentAccessTicket = WxOpenUtil.get(WxOpenConstant.TICKET_KEY);
        log.info("从redis里拿出来的ticket为:{}",componentAccessTicket);
        // 获取token
        WxOpenComponentAccessTokenDto tokenDto = paidComponentAccessToken(componentAccessTicket);
        if(tokenDto == null) {
            log.error("转换授权dto ->{}<- 失败","tokenDto");
            return "";
        }
        WxOpenUtil.addOrRefreshKey(
                WxOpenConstant.TOKEN_KEY,
                tokenDto.getComponentAccessToken(),
                tokenDto.getExpiresIn() - WxOpenConstant.MINUTE * 3
        );
        log.info("成功保存了component_access_token:{},设置其过期时间为:{}",tokenDto.getComponentAccessToken(),tokenDto.getExpiresIn() - WxOpenConstant.MINUTE * 3);

        // 获取预授权码
        WxOpenPreAuthDto preAuthCodeDto = paidPreAuthCode(tokenDto.getComponentAccessToken());
        if(preAuthCodeDto == null) {
            log.error("转换授权dto ->{}<- 失败","preAuthCodeDto");
            return "";
        }

        // 授权
        return getAuthorizePage(tenantCode,preAuthCodeDto.getPreAuthCode(),null,null);
    }
获取预授权码

微信开放文档给的请求地址需要注意一下

POST https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=COMPONENT_ACCESS_TOKEN

问号后面的那部分直接拼接在uri后面才可以,请参考下方代码

	/**
     * 获取预授权码
     *
     * @param componentAccessToken  第三方平台 令牌
     * @return                      预授权码
     */
    private WxOpenPreAuthDto paidPreAuthCode(String componentAccessToken) {
        String baseUri = paidPreAuthCodeUri + componentAccessToken;

        JSONObject params = new JSONObject();
        params.put("component_appid", componentAppId);

        String res =  HttpUtil.post(baseUri, params.toJSONString());
        WxOpenPreAuthDto wxOpenPreAuthDto = null;
        if(!StringUtils.isEmpty(res)) {
            wxOpenPreAuthDto = JSONUtil.toBean(res, WxOpenPreAuthDto.class);
        }
        log.info("返回preAuthCode的json为:{}",res);
        assert wxOpenPreAuthDto != null;
        if (wxOpenPreAuthDto.getErrcode() != 0) {
            log.error("预授权码无返回值,错误码是:{}", wxOpenPreAuthDto.getErrcode());
            return null;
        }
        log.info("接收preAuthCode的dto为:{}",wxOpenPreAuthDto.toString());
        return wxOpenPreAuthDto;
    }
跳转到授权页
	/**
     * 返回授权页重定向链接(扫码授权)
     *
     * @param tenantCode            租户码
     * @param preAuthCode           预授权码
     * @param authType (非必填)     要授权的帐号类型:1 则商户点击链接后,手机端仅展示公众号、2 表示仅展示小程序,3 表示公众号和小程序都展示。如果为未指定,则默认小程序和公众号都展示。第三方平台开发者可以使用本字段来控制授权的帐号类型。
     * @param bizAppId (非必填)     指定授权唯一的小程序或公众号
     * @return                      授权页重定向链接
     */
    private String getAuthorizePage(String tenantCode, String preAuthCode, String authType, String bizAppId) {
        log.info("-->进入正式授权环节,重定向预备");
        // 要跳转的授权页url
        String baseUri = authPageUri;
        // 回调 URI (授权流程完成后,授权页会自动跳转进入回调 URI)
        String redirectUri = notifyAuthSuccessEventUri + tenantCode;

        return baseUri + "?component_appid=" + componentAppId+ "&pre_auth_code=" + preAuthCode + "&redirect_uri=" + redirectUri;
    }

如果程序没有异常,到此为止,应该能够打开二维码页面了,扫码之后会收到1个授权成功回调事件

授权回调

回调的时候你能拿到授权码、过期时间,当然也能从自己的项目header里拿一些参数,比如我这里就拿了tenantCode

接口
	/**
     * 授权成功回调事件
     *
     * @param authCode      授权码
     * @param expiresIn     过期时间
     * @return              返回给前端的回调提升字符串
     */
    @AnonymousAccess
    @RequestMapping("/notify/authorize/success")
    public String authorizedSuccessEvent(@RequestParam("auth_code") String authCode,
                                         @RequestParam("tenantCode") String tenantCode,
                                       @RequestParam("expires_in") int expiresIn) {
        log.info("成功进入授权成功回调事件,租户码是:{}",tenantCode);
        return wxOpenService.authorizedSuccessEvent(tenantCode,authCode,expiresIn);
    }
业务类
	@Override
    public String authorizedSuccessEvent(String tenantCode, String authCode, int expiresIn) {
        String componentAccessToken = WxOpenUtil.get(WxOpenConstant.TOKEN_KEY);
        // 获取授权信息
        WxOpenAuthorizationInfoDto authorizationInfo = paidAuthorizationInfo(componentAccessToken,authCode);
        if(authorizationInfo == null) {
            log.error("转换授权dto ->{}<- 失败","authorizationInfo");
            return "fail";
        }
        String authorizerAppId = authorizationInfo.getAuthorizerAppid();

        // 获取授权方账号信息
        WxOpenAuthorizerInfoDto authorizerInfo = paidAuthorizerInfo(componentAccessToken,authorizerAppId);
        if(authorizerInfo == null) {
            log.error("转换授权dto ->{}<- 失败","authorizerInfo");
            return "fail";
        }
        // 授权方应用类型——公众号/小程序
        String appType = getAppType(authorizerInfo.getAlias());
        // 保存授权账号基本信息
        WxOpenUtil.addOrRefreshKey(
                WxOpenConstant.REFRESH_TOKEN_KEY, authorizationInfo.getAuthorizerRefreshToken(),tenantCode,appType
        );
        WxOpenUtil.addOrRefreshKey(
                WxOpenConstant.AUTHORIZER_ACCESS_TOKEN_KEY,authorizationInfo.getAuthorizerAccessToken(), authorizationInfo.getExpiresIn() - WxOpenConstant.TOKEN_EXPIRES_IN, tenantCode,appType
        );

        WxOpenAuthorizerInfo saveEntity = new WxOpenAuthorizerInfo();
        {
            WxOpenBusinessInfoDto businessInfo = authorizerInfo.getBusinessInfo();
            WxOpenMiniProgramInfoDto miniProgramInfo = authorizerInfo.getMiniProgramInfo();
            // FIXME 有点复杂不知道怎么处理比较好 先转json保存了
            String miniProgramInfoStr;
            try {
                miniProgramInfoStr = JSON.toJSONString(miniProgramInfo);
            } catch (Exception e) {
                log.error("转换miniProgramInfoStr出错啦");
                return "fail";
            }

            String serviceType = getServiceTypeString(appType,authorizerInfo.getServiceTypeInfo().getId());
            String veryfyType = getVerifyTypeString(appType,authorizerInfo.getVerifyTypeInfo().getId());
            List<Object> funcInfoList = authorizationInfo.getFuncInfo();

            saveEntity.setTenantCode(tenantCode);
            saveEntity.setNickName(authorizerInfo.getNickName());
            saveEntity.setAuthorizerAppId(authorizerAppId);
            saveEntity.setHeadImg(authorizerInfo.getHeadImg());
            saveEntity.setAlias(authorizerInfo.getAlias());
            saveEntity.setPrincipalName(authorizerInfo.getPrincipalName());
            saveEntity.setQrcodeUrl(authorizerInfo.getQrcodeUrl());
            saveEntity.setSignature(authorizerInfo.getSignature());
            saveEntity.setUserName(authorizerInfo.getUserName());
            saveEntity.setBusinessInfo(JSONUtil.toJsonStr(businessInfo));
            saveEntity.setFunc(JSONUtil.toJsonStr(funcInfoList));
            saveEntity.setMiniProgramInfo(miniProgramInfoStr);
            saveEntity.setServiceType(serviceType);
            saveEntity.setVerifyType(veryfyType);
        }
        log.info("回调方法保存的实体为:{}",saveEntity.toString());

        // 获取数据库已有的授权信息
        WxOpenAuthorizerInfo existsInfo = getAuthorizerInfo(tenantCode,appType);
        if(existsInfo != null) {
            saveEntity.setId(existsInfo.getId());
        }
        // 保存授权方账号信息到数据库
        wxOpenAuthorizerService.saveOrUpdate(saveEntity);
        return "授权成功";
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值