被微信开放文档绕的云里雾里,在这里踩了很多坑,浪费了很多事件,现在授权模块做完了,在此发散一下思维,整理一下代码
微信第三方平台授权介绍
须知
你的项目里会收到微信推送的两种事件
- 授权事件
- 消息事件
授权事件分为
- 取消授权事件
- 更新授权事件
- 授权成功事件(我另外写成了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 "授权成功";
}