微信公众号开发之微信授权登录以及支付

2,958 阅读4分钟

一. 本地运行vue项目跨域问题的解决方案

新作一个微信h5充值页面,静态页面样式代码通过谷歌浏览器调试编写完毕后,在公众号中使用时是需要调用微信的相关接口;这就需要在微信的开发工具中运行,但是开发工具调试网址并不支持ip地址,因此必须想办法解决;

方案有三:

  1. 使用浏览器特性,关掉浏览器的同源策略限制;(此处不适用)
  2. 使用vue cli的配置
    • (1)配置运行脚本:"serve": "vue-cli-service serve --host h5.xxxx.com --port 443 --https",
    • (2) 此时host本来访问本地的环境127.0.0.1然后变成了h5.xxxx.com外网环境,这时肯定跑不起来,因此需要修改host文件,修改指向;(host文件在C盘system32中,127.0.0.1 test.com;)
    • (3)注意,https端口默认是443啊,http的默认端口是80,该端口是https的因此需要配置443;第二个,更改host文件时,要将修改处前面的#注释去掉,才能生效;如果执行npm run serve不能跑起来,请重启电脑;
    • (4)最后跑起来的地址链接就是https://h5.xxxx.com/lvy-pay,https://h5.xxxx.com:443/lvy-pay中443是默认端口,直接省略
  3. 使用vue代理服务解决跨域问题(这是最常用的,请百度文档)

二. 开发工具调试

复制https://h5.xxxx.com/lvy-pay到微信web开发工具上,显示redirect_uri参数错误?查找后说明这是因为要进行授权回调域名

  1. 这里就需要了解一下微信公众号的授权原理 微信公众号采用的是oauth2的登录授权方式; 简单来讲,就是

    • 1.1 用户用过微信确认登录之后,微信方会返回一个授权码code给回第三方(接入方),这个授权码是一次性的,且有效期时间比较短;
    • 1.2 第三方通过此code去调用微信接口获取token,token的有效期也比较短;
    • 1.3 通过token再去调用微信平台接口,获取微信个人信息(昵称,头像地址,openid,unionid,地区……)
  2. 第一个oauth2的登录授权方式如下:

    • 2.1 通过重新赋值location.href,并传入服务号的appId以及redirect_uri跳转微信授权链接

    • 2.2 经过用户同意获取code;这里redirect_uri授权的页面地址,里面的location.host必须要经过授权才行,否则官方不会承认这个地址,也就不会返回code。 示例如下(具体配置参考官方文档)

      export const requestWxCode = () => {
          const REDIRECT_URI = [location.protocol, "//", location.host, location.pathname].join('')
          location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=snsapi_userinfo&state=ok#wechat_redirect`
      }
      
  3. 授权回调域名过程:

    • 拿服务号的账号和密码登录微信管理后台,找到网页授权
    • ② 填写授权回调域
    • ③ 将MP_verify_NWxYqcEY0l6bdRam.txt下载下来上传至填写域名或路径指向的web服务器(或者虚拟主机)的目录;校验确保可以访问;
    • ④ 注意第三步,对于首次使用的域名,服务器根目录下是没有的,所以即便你在本地放置了,依旧是访问不到的;所以要么自己放置后部署,要么找运维手动放置;当可以点击“保存”时,就说明回调域名授权成功

三、封装请求

这里选用的是XMLHttpRequest,将有效token封装进入,具体看代码(此处略)

import { API_BASE_URL } from '../config'
import { $loginToken, $requestWxCode } from '../utils/utils'
import Toast from '@/components/Toast'
import { IS_DEV } from '../config'
import axios from 'axios'

import registerDialog from '@/components/Register'

const SUCCESS_CODE = 0
const NEED_REGISTER = 2
const TOKEN_FAIL = 100
const VERSION = '1.1.6'

axios.defaults.timeout = 5000
axios.defaults.baseURL = API_BASE_URL

export function $post(url, params) {
  axios.defaults.headers.common['AppVersion'] = VERSION
  if ($loginToken()) {
    axios.defaults.headers.common['Authorization'] = $loginToken()
  }
  return new Promise(resolve => {
    axios({ url, params, method: 'post' }).then(({ data: { data, code, message }}) => {
      if (IS_DEV) {
        console.warn('req => ', url, 'code = ',code, 'data = ',data)
      }
      if (code === SUCCESS_CODE) {
        resolve(data)
      } else if (code === NEED_REGISTER) {
        localStorage.removeItem('login_info')
        registerDialog()
      } else if (code === TOKEN_FAIL) {
        Toast(message)
        localStorage.removeItem('login_info')
        $requestWxCode()
      } else {
        console.error('Request fail: code = ' + code + ', message = ' + message)
        Toast(message || '服务器异常,请稍候重试')
      }
    })
  })
}




四、具体登录逻辑:

* 4.1 先判断是否在微信应用内打开,只有在应用内打开,才能调用微信的授权链接;
* 4.2  判断缓存中是否有有效token(token存在并且未过期),如果有则可直接去接口请求;
* 4.3  如果token不存在或者存在但已过期,则需要重新判断code,看链接上是否存在code或者code是不是重复的(重复的意味着一定不可用了,因为code是一次性的)
* 4.4  如果code不存在,则手动重定向路由,以调用微信的授权链接如下:
location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=snsapi_userinfo&state=ok#wechat_redirect`

用户同意授权后,就会发现路由地址变成了https://h5.xxxx.com/lvy-pay?code=0014sGyh2lXvIC05NYvh2B3qyh24sGyY&state=ok,上面含有code,取到code,向服务器换取token,并将此token及过期时间存到缓存中,方便下次打开时直接调用;这时就可以放开去发接口请求了

五、 调用微信支付

createOrder(goodsId).then(({ prepay_id }) => {
  const timeStamp = String(Date.now())
  const nonceStr = `qingteng_${timeStamp}_${Math.floor(Math.random() * 10000)}`
  const $$package = `prepay_id=${prepay_id}`
  const paramStr = `appId=${APP_ID}&nonceStr=${nonceStr}&package=${$$package}&signType=MD5&timeStamp=${timeStamp}&key=${API_KEY}`

  window.WeixinJSBridge.invoke('getBrandWCPayRequest',{
    appId: APP_ID,
    timeStamp,
    nonceStr,
    package: $$package,
    signType: 'MD5',
    paySign: md5(paramStr).toUpperCase(),
  },({ err_msg }) => {
    if (err_msg === "get_brand_wcpay_request:ok") {
      Toast('购买成功')
      this.getUserProperty()
      this.currentType = ''
    } else if (err_msg === "get_brand_wcpay_request:fail") {
      Toast('支付失败,请稍候重试')
    } else if (err_msg === "get_brand_wcpay_request:cancel") {
      Toast('支付已取消')
    }
  }
  )
})

过程:

  • ①:创建订单,从服务器换取相应的prepay_id,这是支付package必须要的参数;核心是调用微信浏览器环境提供的window.WeixinJSBridge.invoke('getBrandWCPayRequest',options,() =>{}};
  • ② 这时一样涉及到权限问题,不是任何域名下的页面都可以随意调用该支付接口,那样是很危险的;必须要有公司资质担保的,授权该域名下可调用才行;
  • ③ 具体就是要登录商务号,增加h5支付域名授权;

以上就是开发过程中关于配置和授权问题的记录与总结

六、知识扩展

  1. HTTP、HTTPS常用的默认端口号(自行百度)
  2. XMLHttpRequest学习:axios学习
  3. OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户再某一网站上存储的私密的资源(如照片,视频,联系人列表),而无须将用户名和密码提供给第三方应用。 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样OAuth允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,二部需要分享他们的访问徐厚或者他们数据的所有内容; 代码示例:
export default {
  created() {
    // 解决首次授权需要回退2次的问题
    window.addEventListener("popstate", () => {
      window.WeixinJSBridge.invoke("closeWindow");
    });
    this.init();
  },

  methods: {
    $requestWxCode2 = (code) => {
      if (!IS_WECHAT) {
        Toast('请在微信内使用')
      } else {
        const { protocol, host, pathname, search } = location
        const REDIRECT_URI = `${protocol}//${host}${pathname}${search}`
        const wxAuthorizeAddress = 'https://open.weixin.qq.com/connect/oauth2/authorize'
        window.location.href = `${wxAuthorizeAddress}?appid=${APP_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=snsapi_userinfo&state=ok&connect_redirect=1#wechat_redirect`
      }
    }

    init() {
      // 测试环境或者正式环境设置强制开启登录过期
      // this.setLoginExpirationTimeInDev(false);
      if (!$loginToken()) {
        this.userLogin();
        return;
      }
      
      this.initPageInfo();
    },

    userLogin() {
      let code = location.search.match(/code=(\w+)[&#]?/)
        ? location.search.match(/code=(\w+)#?/)[1]
        : "";
      // 1. 授权拿code
      if (!code) {
        // auth2授权后会打开新的页面重新走created声明周期函数
        $requestWxCode2()
        return
      }
      
      // 2. 拿code换token并缓存token以及其过期时间
      h5UserLogin(code).then(({ token, expires_in }) => {
        Storage.setItem("wechat_code", code);
        Storage.setItem("login_info", {
          token,
          expiredTime: Date.now() + expires_in * 1000,
        });
        // 初始化页面其它信息每个接口都需要处理token
        this.initPageInfo();
      });

      history.pushState({ page: 1 }, null, window.location.href)
    },

    setLoginExpirationTimeInDev(forcedOpen = false) {
      if (!IS_DEV || !forcedOpen) {
        return;
      }
      const { expiredTime = "" } = Storage.getItem("login_valid") || {};
      if (!expiredTime || expiredTime < Date.now()) {
        Storage.setItem("login_valid", { expiredTime: Date.now() + 5000 });
        $requestWxCode2("");
      } else {
        history.pushState({ page: 1 }, null, window.location.href);
      }
    },

    initPageInfo() {
    },


    Pay(goodsId) {
      if (!$checkIsWeChat()) {
        return;
      }

      createOrder(goodsId).then(({ prepay_id }) => {
        const timeStamp = String(Date.now());
        const nonceStr = `qingteng_${timeStamp}_${Math.floor(
          Math.random() * 10000
        )}`;
        const $$package = `prepay_id=${prepay_id}`;
        const paramStr = `appId=${APP_ID}&nonceStr=${nonceStr}&package=${$$package}&signType=MD5&timeStamp=${timeStamp}&key=${API_KEY}`;

        window.WeixinJSBridge.invoke(
          "getBrandWCPayRequest",
          {
            appId: APP_ID,
            timeStamp,
            nonceStr,
            package: $$package,
            signType: "MD5",
            paySign: md5(paramStr).toUpperCase(),
          },
          ({ err_msg }) => {
            if (err_msg === "get_brand_wcpay_request:ok") {
              Toast("购买成功");
              this.getUserProperty();
              this.currentType = "";
            } else if (err_msg === "get_brand_wcpay_request:fail") {
              Toast("支付失败,请稍候重试");
            } else if (err_msg === "get_brand_wcpay_request:cancel") {
              Toast("支付已取消");
            }
          }
        );
      });
    },
  },
};