上篇我们介绍了关于注册并认证开放平台和创建第三方平台的一些资料介绍与说明,本篇我们将如何处理微信应用(公众号和小程序)授权到第三方平台的逻辑。如未特殊说明,后续的第三方平台统一代指「平台型服务商第三方平台」,定制化开发服务商的第三方平台没有这些流程。
授权事件处理
前文我们讲到了当第三方平台创建并且审核通过了,那么这时候微信服务器会每隔10
分钟POST
推送第三方平台验证票据到我们的授权事件接收URL
。同时后续的取消授权通知、授权成功通知、授权更新通知、复用公众号资质快速注册小程序通知、法人身份认证方式创建小程序通知等授权相关事件也将通过该URL
推送过来。
消息与验证票据事件
我们需要在接收到component_verify_ticket
事件时将消息体中的验证票据存储起来(验证票据默认有效期为12
个小时),建议直接缓存到redis
中或者以其他方式存储起来,每次事件到达时直接刷新该票据。消息默认都是加密的,解密规则与公众号消息基本一致,具体可以参考微信第三方平台加解密技术方案。
推送过来的消息格式(所有的消息基本都这样)长这样:
{
"nonce" : "1111111111",
"timestamp" : "1600000000",
"signature" : "767d********************************0e76",
"msgSignature" : "93dd********************************e53b",
"encryptType" : "aes",
"postData" : "<xml>n <AppId><![CDATA[wx**************4a]]></AppId>n <Encrypt><![CDATA[RlLGbIBoa97x9VtaV2GYEPKTi6+YFSv4YVh4TZSr6IkBMVpYmZdVzXYoZ6EiAs3v299mARD9IheeC07VYiXLF1YY+HzjqLKrSvajydNBZV/FRWsqm4dn4xxjZoNw+a***********************************************************************************PAIEL+WewrZr4IbRQWZCnP1PmzlGZq6T34rxLQtQ5oMZBoeYDcrDRCIPd4y8BFtPMnhQzEHOU3gWQFkLMZQni/pJhF3c+cljHOBo0******************************++igqj8SGZ99qTT42lGiXTeHs+ii4aUHfdJP/ZlLSRqpR53RSxJVT8LB/Z/Via5VmJYrsonD5lDR1t7jdCE7Vnm+3LgLOR+1*************************k3zTPTDVA==]]></Encrypt>n</xml>n"
}
*
表示打码了,并不是推送*
字符过来的意思。
解密后的消息格式为xml
(以component_verify_ticket
事件的消息解密数据为例),这里我就直接以json
形式展示,字段可能会比官方文档定义的多,但一般情况下不会少。
{
"appId": "wx**************e3",
"createTime": 1600000008,
"infoType": "component_verify_ticket",
"componentVerifyTicket": "ticket@@@**************-********************************************_******************ua3FHkuQ",
"status": 0,
"info": {
"codeType": 0
}
}
我们要做的就是验证appId
是否合法(确认是己方申请的第三方平台appId
),合法的话就直接将对应的componentVerifyTicket
存储起来,后续的操作将基于这个票据展开。
处理授权事件设计
这里顺便介绍一下目前我们对授权事件的处理设计,由于授权事件接收URL
会接收到各种各样的事件,所以第一反应是考虑策略模式,结合我们之前介绍的如何在Spring环境中优雅地使用策略模式。很容易地,我们能够得到如下的设计:
定义授权事件处理器
首先自然是我们的授权事件处理器接口
了,抽象接口onEvent
表示处理器能处理的事件名称,handle
方法表示处理逻辑,其中的WxOpenXmlMessage
我们直接使用了WxJava
提供的解密后的消息类。
import me.chanjar.weixin.open.bean.message.WxOpenXmlMessage;
/**
* 微信第三方平台授权事件处理器
*/
public interface WxAuthEventHandler {
/**
* 处理事件
*
* @param authMessage 授权事件
*/
void handle(WxOpenXmlMessage authMessage);
/**
* 事件处理器监听/支持的事件名称
*
* @return 监听/支持的事件名称
*/
String onEvent();
/**
* 处理器是否支持处理特定事件
*
* @param event 事件名称
* @return {@code true} 表示支持,{@code false} 表示不支持
*/
default boolean support(String event) {
return StringUtils.equalsIgnoreCase(this.onEvent(), event);
}
}
针对各种授权事件实现对应的处理器
接着我们根据所需要监听处理的授权事件,实现对应的处理器,比如验证票据事件的处理器逻辑如下:
/**
* 第三方平台刷新认证票据事件处理器
*/
@Slf4j
@Service
@AllArgsConstructor
public class ComponentVerifyTicketEventHandler implements WxAuthEventHandler {
public static final String EVENT = "component_verify_ticket";
@Override
public void handle(WxOpenXmlMessage authMessage) {
// 首先通过 authMessage.getComponentVerifyTicket() 获取得到推送过来的票据
// 然后直接存储到`redis`和其他存储介质中
}
@Override
public String onEvent() {
return EVENT;
}
}
授权事件管理器与配置类
最后就是我们的管理器与配置类:
/**
* 微信第三方平台授权事件管理器
*/
@Slf4j
@AllArgsConstructor
public class WxAuthEventManager {
private final Map<String, WxAuthEventHandler> eventHandlerMap;
public WxAuthEventHandler get(String event) {
return eventHandlerMap.get(event);
}
public void handle(WxOpenXmlMessage authMessage) {
String event = authMessage.getInfoType();
WxAuthEventHandler handler = eventHandlerMap.get(event);
if (Objects.nonNull(handler)) {
handler.handle(authMessage);
}
}
}
/**
* 微信第三方平台授权事件管理器配置
*/
@Slf4j
@Configuration
public class WxAuthEventManagerConfiguration {
@Bean
public WxAuthEventManager wxAuthEventManager(ObjectProvider<List<WxAuthEventHandler>> authEventHandlerList) {
List<WxAuthEventHandler> handlerList = authEventHandlerList.getIfAvailable();
if (CollectionUtils.isEmpty(handlerList)) {
log.warn("None WxAuthEventHandler found! WxAuthEventManager#eventHandlerMap is empty");
return new WxAuthEventManager(Collections.emptyMap());
}
Map<String, WxAuthEventHandler> handlerMap = Maps.newHashMapWithExpectedSize(handlerList.size());
for (WxAuthEventHandler handler : handlerList) {
String event = handler.onEvent();
WxAuthEventHandler existHandler = handlerMap.get(event);
log.info("Append Handler[{}, listen on {}] to handlerMap", handler.getClass().getName(), event);
handlerMap.put(event, handler);
}
return new WxAuthEventManager(Collections.unmodifiableMap(handlerMap));
}
}
到了这一步就可以直接通过注入WxAuthEventManager
进行事件处理了。后续有新的事件需要监听处理,只需要直接实现WxAuthEventHandler
接口并且将其注册到Spring
容器中即可被自动添加到WxAuthEventManager
中。
SDK推荐
在这里推荐一款非常好用而且较为全面的微信Java
开发工具包——WxJava,这款SDK
对微信生态圈的小程序
、公众号
、企业微信
、微信支付
、微信开放平台
等都提供了较为友好的封装,同时有持续维护并且更新(当前最新版本是2020年11月29日
发布的4.0.0
版本,JDK
最低版本要求1.8
),我司便是用这款SDK
来辅助微信第三方平台开发的,目前使用状况良好,能帮助提高微信开发效率。
授权流程
参考微信官方的授权流程技术说明,我们知道基本流程分为5
个流程(实际上就4
步,最后是实际应用,并不算授权流程),分别是:
-
获取第三方平台的预授权码(前置条件是有第三方平台的令牌 component_access_token
) -
引导用户访问根据预授权码生成的授权链接 -
用户确认授权 -
通过回调 URI
获取用户的授权码(并缓存) -
通过授权码调用公众号/小程序的 API
维护第三方平台令牌
空格在获取预授权码之前,我们需要先拿到第三方平台的令牌component_access_token
。这个令牌与公众号、小程序的令牌一样存在有效期的限制,有效期为2
个小时,需要结合第三方平台的appId
和appSecret
以及微信推送过来的第三方平台票据进行获取。我们通常结合redis
缓存该令牌。WxJava
中的WxOpenComponentService#getComponentAccessToken
默认使用Lock
进行加锁获取令牌,由于加锁操作是针对单实例的,所以在多实例部署时最好覆写该方法,通过分布式锁机制实现获取令牌。
官方文档
获取预授权码并生成授权链接
预授权码的获取非常简单,直接使用第三方平台的appId
和component_access_token
即可获取得到,参考官方文档。预授权码根据下述规则可以生成对应的授权链接:
https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=xxxx&pre_auth_code=xxxxx&redirect_uri=xxxx&auth_type=xxx
,其中component_appid
是第三方平台的appId
、pre_auth_code
是我们请求微信得到的预授权码,redirect_uri
是授权完成后的回调地址(注意域名必须是授权发起页域名),auth_type
表示授权账号类型,1
表示仅展示可授权的公众号、2
表示仅展示可授权的小程序、3
表示同时展示可授权的公众号和小程序,默认为3
,biz_appid
用来指定唯一授权的公众号或者小程序的appId
,这个参数与auth_type
互斥。
需要特别注意的是:这条授权链接是不能直接访问的,必须经过我们先前创建第三方平台指定的授权发起页域名跳转过来才能正常访问。
微信会判断referer
是否是我们配置的域名,如果不是则会报错:
因此授权链接必须先通过访问我们的授权发起页域名所在的服务,然后通过该服务中转访问授权连接,这时候才能够正常显示授权地址。
这里简单介绍一下如何让授权链接能够被正常有效访问,第一种方式是后端直接返回授权链接,前端将授权链接展示在授权发起页域名对应服务下,这时候就能够正常访问该授权链接。另一种是后端直接返回一串html
代码,代码里让整个页面在访问时直接重定向到授权链接:
<!DOCTYPE html>
<html>
<script>window.location.href='${授权链接}'</script>
</html>
这种的好处是不用将授权发起页域名和服务强制绑定在一起,任何授信服务都能够正常发起授权请求。
还有一个是redirect_uri
授权完成后的回调地址域名问题,一样跟授权发起页域名做了强绑定。如果想跳转到其他授信域名,解决方法也是很简单的,就是在请求生成授权链接时,将回调地址和其他业务参数先用唯一key
关联并存储起来,同时将该key
作为回调地址的组成部分生成授权链接。
@GetMapping(value = "/createAuthPage", produces = MediaType.TEXT_HTML_VALUE)
public String createAuthPage(@RequestParam @Valid @RedirectUrl String redirectUri,
@RequestParam @NotEmpty String anyOtherBizParam,
@RequestParam(defaultValue = "3") Integer authType) {
// 生成唯一的key
String callbackKey = UUIDHelper.uuid();
// 将 redirectUri 和 anyOtherBizParam 等数据关联存储起来(比如redis定时缓存或者数据库持久化)
String key = Constants.AUTH_INFO_KEY_PREFIX + callbackKey;
// redis.set(key, ...);
// 处理授权回调链接
String authRedirectUri = "${授权发起页域名}/auth/callback/" + callbackKey;
// 生成授权链接
String authUri = "...";
// 处理referer域名问题
return MessageFormat.format("<!DOCTYPE html><html><script>window.location.href='{0}'</script></html>", authUri);
}
然后我们在授权完成回调的时候,就能够通过这个唯一key
获取到原先定义的回调地址,最后拼装并再次重定向即可:
@GetMapping("/auth/callback/{callbackKey}")
public ModelAndView userAuthCallback(@RequestParam("auth_code") String authorizationCode,
@RequestParam("expires_in") Integer expireIn,
@PathVariable("callbackKey") String callbackKey) {
String key = Constants.AUTH_INFO_KEY_PREFIX + callbackKey;
// 根据唯一key从存储介质中拿到对应的重定向地址
String redirectUri = "";
// 处理授权事件
// ...
// 重定向到原先定义的回调地址
ModelAndView mv = new ModelAndView();
mv.setView(new RedirectView(redirectUri));
return mv;
}
到这里授权发起域名和回调域名的问题基本都解决了。
用户访问授权链接并确认授权
通过各种形式让用户能够正常访问到我们的授权链接,这时候根据auth_type
或者biz_appid
会有不同的扫码效果。
用户确认要授权或者更新授权的应用公众号/小程序后(如果是已经授权过的,那么可以修改权限集),将进入授权确认页,在这个页面确认要授权到该第三方平台的权限集,这些权限集就是我们先前定义需要用户授权的,注意部分权限只能唯一授权到某个第三方平台,因此我们在授权后需要自己确认用户授权是否完整并做出相应的逻辑调整。
处理授权回调,获取授权码
当用户最终确定后,微信将会触发授权回调,就是我们配置的redirect_uri
,并且携带auth_code
授权码和expires_in
过期时间。我们需要在这时候根据授权码获取得到授权者(公众号、小程序)的信息,将其存储起来并与用户挂钩,这样后续内部业务才能正常使用。参考使用授权码获取授权信息接口,我们可以拿到授权者的accessToken
和refreshToken
,实际上就是公众号/小程序的访问令牌和刷新令牌(有点OAuth2
的味道),这个accessToken
和公众号/小程序根据appId
和appSecret
生成的accessToken
是独立的,两者不等效、不互斥,前者的权限受用户授权约束,后者则是支持什么功能就能用什么功能。并且前者在过期前必须通过refreshToken
进行刷新,而后者则是通过重新调用API
直接生成。有效期都是2
个小时。
因此,我们在得到了授权码并且获得访问令牌和刷新令牌后,必须存储起来。「尤其是刷新令牌,一旦丢失,只能让用户重新授权才能得到(这期间访问令牌要是过期了,那所有业务都将被阻塞)」。实际上就是用户不给你appSecret
,你自己通过appId
、刷新令牌以及我们的第三方平台appId
生成用户应用的访问令牌。
实际上授权完成后,微信服务器除了会触发回调地址(实际上是客户端不停地轮询是否授权完成,完成后页面会跳转到该回调地址),同时也会触发authorized
授权事件通知。根据我们先前对授权事件的处理设计,我们只需要简单添加对应的授权处理器即可:
@Slf4j
@Service
@AllArgsConstructor
public class AuthorizedEventHandler implements WxAuthEventHandler {
public static final String EVENT = "authorized";
@Override
public void handle(WxOpenXmlMessage authMessage) {
// 根据授权码获取授权信息和授权者信息
String authorizationCode = authMessage.getAuthorizationCode();
WxOpenQueryAuthResult authResult = wxOpenService.getWxOpenComponentService().getQueryAuth(authorizationCode);
WxOpenAuthorizationInfo authorizationInfo = authResult.getAuthorizationInfo();
WxOpenAuthorizerInfoResult authorizerInfo = wxOpenService.getWxOpenComponentService()
.getAuthorizerInfo(authorizationInfo.getAuthorizerAppid());
// 判断是公众号还是小程序授权
WxOpenAuthorizerInfo info = authorizerInfo.getAuthorizerInfo();
WxAppType appType = Objects.nonNull(info.getMiniProgramInfo()) ? WxAppType.MINI_PROGRAM : WxAppType.MP;
// 获取访问令牌和刷新令牌
String accessToken = authorizationInfo.getAuthorizerAccessToken();
String refreshToken = authorizationInfo.getAuthorizerRefreshToken();
// 将授权信息存储起来
}
@Override
public String onEvent() {
return EVENT;
}
}
事件处理由于拿不到用户数据,因此授权应用与用户关联只能通过授权回调进行处理。当然如果授权链接是通过biz_appid
明确指定微信应用appId
,并且内部已经维护了用户与appId
的关联关系,那么事件处理也能拿到对应的用户做进一步关联。
通过授权码调用公众号/小程序的API
当我们拿到了授权应用的访问令牌后,根据已经授权的权限集,我们就能够通过WxJava
调用相应的API
了。
结束语
以上就是本篇关于第三方平台处理微信应用(公众号/小程序)授权的介绍,下一篇我们将会继续介绍如何复用公众号资质快速注册小程序以及微信应用消息与事件处理。敬请期待!
原文始发于微信公众号(三维家技术实践):如何开发一个微信第三方平台?(授权篇)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30611.html