blog icon indicating copy to clipboard operation
blog copied to clipboard

微信公众号网页开发之授权、支付、分享功能

Open Pasoul opened this issue 4 years ago • 2 comments

微信公众号网页和移动端wap的区别

微信公众号网页开发和移动web开发无本质区别,只是在移动web开发的基础上增加了一些微信提供的扩展能力,如:支付、分享、地理定位等,可以理解为:移动web + 微信能力 = 微信公众号网页开发

什么是JS-SDK

微信JS-SDK:是开发者在网页上通过JavaScript代码调用微信原生功能的工具包,开发者可以用它在网页上实现录制和播放语音、扫一扫、分享等能力

微信的用户机制

为了识别用户,每个用户在每个公众号下都有一个OpenId,如果需要在多公众号、移动应用之间做用户共通,则需前往微信开放平台,将这些公众号和应用绑定到一个开放平台账号下,绑定后,一个用户虽然对多个公众号和应用有多个不同的OpenID,但他对所有这些同一开放平台账号下的公众号和应用,只有一个UnionID,此UnionID是在拉取用户信息(需scope为 snsapi_userinfo)时返回的(后续授权功能会提到)

网页授权

如果用户在微信端访问网页,公众号可以通过微信网页授权来访问用户的基本信息。

网页授权回调域名说明:

  1. 在公众号请求网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头; image

  2. 授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com 无法进行OAuth2.0鉴权

  3. 如果公众号登录授权给了第三方开发者来进行管理,则不必做任何设置,由第三方代替公众号实现网页授权即可

网页授权的两种scope区别:

  1. 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
  2. 以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。 关于UnionID机制: 如果开发者有在多个公众号,或在公众号、移动应用之间统一用户帐号的需求,需要前往微信开放平台(open.weixin.qq.com)绑定公众号,因为同一用户,对同一个微信开放平台下的不同应用(移动应用、网站应用和公众帐号),unionid是相同的,此unionid是在拉取用户信息(需scope为 snsapi_userinfo)时返回的

关于静默授权的场景:

  1. 上面已经提到,对于以snsapi_base为scope的网页授权,就静默授权的,用户无感知;
  2. 对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知。

网页授权的流程

image

前端代码实现

假设我们有这样的需求:用户进入网页通过静默授权拿到用户openId,在指定场景下(如设置用户信息)需要手动授权拿到用户的userInfo

  1. 创建login页面,用于网站的授权及重定向操作,我们可以通过mode指定授权模式,通过redirectUrl指定授权成功后重定向的页面,关键代码如下:
export default function() {
  // mode: base 静默授权  userInfo 手动授权
  const query = useLocation().query
  const mode =  query.mode === 'userInfo'
                  ? 'zuc/wx/authorizeUserInfo'
                  : 'zuc/wx/authorizeBase';
  window.location.replace(
    `${config.h5LoginDomain}${mode}?redirectUrl=${encodeURIComponent(query.fromPath)}`,
  );
  return null;
}
  1. 和后端约定好错误码,在全局响应拦截器里检测到该错误码后跳转到登录页进行授权,关键代码:
request.interceptors.response.use(async response => {
  if (response) {
    const data = await response.clone().json();
    if (  data.code === 30000118 ) {
      // 30000118:您的登录已过期,请重新登录
      history.push(
        `/login?fromPath=${encodeURIComponent(location.href)}`,
      );
    }
  }
  return response;
});
  1. 在设置个人资料时进行手动授权,关键代码
handleClick() {
    history.push(
      `/micro-store/h5/login?fromPath=${encodeURIComponent(location.href)}&mode=userInfo`,
    );
}

注意

  1. 确保公众号已认证,只有认证的公众号才有获取用户信息接口的权限 image
  2. 授权成功后,服务端会在浏览器埋下cookie,因为一般服务端会把cookie的domain设置成网站的域名,而通常我们本地开发环境是通过ip访问项目的,导致请求的时候,无法带上cookie,可以通过修改本地host解决此问题,如:192.168.30.77 xxx.abc.com,我们本地访问地址由原来的:192.168.30.77:8080/index改成xxx.abc.com:8080/index
  3. 常见的请求库如axiosfetchumi-request请求时默认不带cookie的,需要设置credentials: ‘include’。但此属性要求后端设置的Access-Control-Allow-Origin不能为*,此问题在通过CROS解决跨域问题时尤为常见,报错如下: image 服务端常见的解决方案是获取浏览器的请求头,然后设置到Access-Control-Allow-Origin中,当然为了安全,可以把允许跨域访问的域名放在一个集合里,然后判断浏览器的请求头是不是在这个集合中 image

Pasoul avatar May 25 '20 15:05 Pasoul

微信分享

我们可以通过jssdk配置微信分享出去的标题、描述、图片和链接 image

微信分享流程

  1. 在公众号设置里面配置JS安全域名 image 注意安全域名一个月只能修改三次,并且一个公众号只能配三个安全域名 image
  2. 在JS安全域名下,前端初始化页面时配置wx.config进行签名验证,然后在wx.ready回调里配置分享信息

前端代码实现

  1. 封装initJssdk方法,调用config接口注入权限验证配置
export const initJssdk = function(storeCode, debug = false) {
  return new Promise(async (resolve, reject) => {
    // 调用后端接口,获取微信签名配置
    let res = await getWxConfig({
      url: isIphone()
        ? encodeURIComponent(window._entryUrl.split('#')[0])
        : encodeURIComponent(location.href.split('#')[0]),
      bizCode: storeCode,
    });
    if (res.code === 0) {
      let data = res.data;
     // 调用wx.config,注入配置信息
      wx.config({
        debug,
        appId: data.appId,
        timestamp: data.timestamp,
        nonceStr: data.nonceStr,
        signature: data.signature,
        jsApiList: [
          // 这里是所有你需要调用的wx api
          'updateAppMessageShareData',
          'updateTimelineShareData',
          'hideMenuItems',
          'showMenuItems',
          'chooseWXPay',
        ],
      });
      wx.ready(function() {
        resolve(data);
      });
      wx.error(function(res) {
         reject(res);
      });
    } else {
      reject(res);
    }
  });
};
  1. 封装shareMessage方法(看个人喜好,也可以封装成promise),包含分享给朋友、朋友圈功能:
export const shareMessage = (
  { title, desc, link, imgUrl, storeCode, storeInfo = {} },
  sucCb = () => {},
  cancelCB = () => {},
) => {
  wx.ready(function() {
    let shareTitle = title || storeInfo.name;
    let shareDesc = desc || '上精彩课程,看缤纷世界';
    let shareLink = link || getFullPath(`home?storeCode=${storeCode}`);
    let shareImageUrl = imgUrl || storeInfo.logo;
    // 自定义“分享给朋友”及“分享到QQ”
    wx.updateAppMessageShareData({
      title: shareTitle, // 分享标题
      desc: shareDesc, // 分享描述
      link: shareLink, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
      imgUrl: shareImageUrl, // 分享图标
      success: sucCb,
    });
    // 自定义“分享到朋友圈”及“分享到QQ空间”
    wx.updateTimelineShareData({
      title: shareTitle, // 分享标题
      link: shareLink, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
      imgUrl: shareImageUrl, // 分享图标
      success: sucCb,
    });
  });
};
  1. 在初始化页面的时候调用:
initJssdk(this.storeCode).then(res => {
      // 配置微信分享
      shareMessage({
        storeInfo: this.props.storeInfo,
        storeCode: this.storeCode,
      });
});

常见的问题

  1. 微信分享配置的JS安全域名是不带端口号的,所以我们配置host访问本地页面进行wx.config通常都会报invalid signature,但是每次改动后把前端代码发到对应的域名下又很繁琐,好在微信开发者工具提供测试号,关注该测试号,把js安全域名设置成本地的ip或host即可,同时服务端验证的微信appid和appsecret也要改成测试号对应的id和secret。 image image
  2. 所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用),在单页应用里,如果我们是hash模式路由,可以在入口js里调用initJssdk,而在history模式下,需要在每个页面(或者是vue-router的afterEnter里面调用initJssdk)
  3. 可能注意到我在wx.config验签的时候有这样一段代码:
url: isIphone()
        ? encodeURIComponent(window._entryUrl.split('#')[0])
        : encodeURIComponent(location.href.split('#')[0]),

这是ios和安卓的差异,在ios中,微信验签的的url必须是引起页面刷新(window.onload或者location.href跳转)的地址,比如用户打开页面A,此时window._entryUrl设置成A,然后通过单页面路由跳转到B,此时如果B页面需要验签,我们传给微信的url必须是A页面的url,如果我们通过location.href跳转到B页面,此时验签传给微信的url必须是B页面的url。而在安卓中,验签的url是当前页面的url。 4.验签的url是页面完整的url(包括'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。

Pasoul avatar May 28 '20 05:05 Pasoul

微信支付

微信支付流程

  1. 在公众号开通微信支付 image
  2. 设置支付目录 支付授权目录说明: 1、商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:https://www.weixin.com/pay.php。 2、商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册:” 支付授权目录设置说明: 登录微信支付商户平台(pay.weixin.qq.com)-->产品中心-->开发配置,设置后一般5分钟内生效。 支付授权目录校验规则说明: 1、如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀; 2、如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/ image
  3. 前端网页请求生成支付订单,后端会返回支付参数prepay_id、paySign等,前端传入支付参数,调用wx.chooseWXPay方法,微信会在choosePay回调函数内告知支付结果。

关键代码

  1. 封装通用wxPay方法
/**
 * 功能: 微信支付功能  https://pay.weixin.qq.com/wiki/doc/api/index.html
 * @param { timestamp } { Number } { 支付签名时间戳 }
 * @param { nonceStr } { Number } { 支付签名随机串,不长于 32 位 }
 * @param { packages } { Number } {  统一支付接口返回的prepay_id参数值 }
 * @param { signType } { Number } { 签名方式,默认为'SHA1',使用新版支付需传入'MD5' }
 * @param { paySign } { Number } { 支付签名 }
 */
export const wxPay = function(payData) {
  return new Promise((resolve, reject) => {
    wx.chooseWXPay({
      ...payData,
      success: function(res) {
        resolve(res);
      },
      cancel: function(res) {
        resolve(res);
      },
      fail: function(res) {
        reject(res);
      },
    });
    wx.error(function(res) {
      reject(res);
    });
  });
};
  1. 获取支付签名,调用微信支付api
handlePay = async () => {
  // 报名,生成订单
  const applyRes = await apply({
      goodsId: this.goodsId
  });
  // 根据订单号,获取微信支付参数
  const payRes = await pay({
    orderNo: applyRes.data.orderNo
  });
  // 调用微信支付方法
   wxPay(payRes.data.payData)
    .then(res => {
      if (res.errMsg.indexOf('ok') > 0) {
        Toast.info('支付成功');
      }
    })
}

Pasoul avatar May 28 '20 05:05 Pasoul