微信开发——网页授权

  • 前期准备
  • 前端
  • 后端


前期准备

①微信客户端中访问第三方页面,公众号可以通过网页登陆授权,获取微信用户的基本信息(头像、昵称等),实现业务逻辑。一切按照官方文档说明开发。

②安装微信开发者工具

③一个官方认证的公众号,或者使用微信公众平台的测试公众号。

微信开发者工具中弹出手机号码授权 微信开发者平台 授权_微信


④如果使用微信公众平台测试号,需要配置JS接口安全域名网页授权获取用户基本信息,填写正确的域名和端口号。

微信开发者工具中弹出手机号码授权 微信开发者平台 授权_前端_02


微信开发者工具中弹出手机号码授权 微信开发者平台 授权_微信平台_03


⑤本文使用测试号来演示网页授权。前端页面使用VSCode的Live Server本地开启服务器,端口号是5500。后端使用Node的express本地开启服务器,端口号是3000

前端

页面介绍
回调页面: 你自己写的网页,即http://127.0.0.1:5500/,与之前配置的JS接口安全域名网页授权获取用户基本信息一致,可以打开在微信平台与非微信平台。
静默授权链接: 将你的回调页用微信规定的静默授权格式包裹起来。在微信中打开时,会返回展示你的回调页,并且在URL上附加额外的codestate参数。静默授权只能获取到微信用户的openid
主动授权链接: 将你的回调页用微信规定的主动授权格式包裹起来。在微信中打开时,弹出主动授权框。用户点击确定授权后,会返回展示你的回调页,并且在URL上附加额外的codestate参数。若用户近期主动授权过,则不会弹出主动授权框,自动刷新跳转到回调页。主动授权能进一步获取到微信用户的头像、昵称等信息。
④在主动授权链接中带上state=auth参数,能够在用户主动授权后的回调页的URL上,知道用户已经主动授权了。

// 回调页
const cbURL = encodeURIComponent(`http://127.0.0.1:5500/`);
// 静默授权链接
const shareURL = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${cbURL}&response_type=code&scope=snsapi_base#wechat_redirect`;
// 主动授权链接
const authURL = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${cbURL}&response_type=code&scope=snsapi_userinfo&state=auth#wechat_redirect`;

二次分享与code参数
code参数是微信平台附加在回调页URL上的随机码,用于获取当前微信用户的access_token。随机码只能用一次,5分钟未使用自动过期。原地刷新页面也会导致code失效,需要重定向静默授权链接,重新获取随机码。
②非微信端打开的回调页分享到微信为首次分享,首次分享的分享链接即为静默授权链接。
③微信端打开的回调页,通过微信浏览器右上角的···继续进行分享为二次分享,需要调用微信的SDK,参见我的另一篇博客《微信开发——开放标签》。
④二次分享的链接应该设置为回调页链接http://127.0.0.1:5500/。若其他用户在微信平台打开二次分享的链接,那么就无法进行主动授权。所以需要在回调页判断微信平台,并在无code参数时,重定向至静默授权链接。
记录并判断用户的主动授权
①从URL的state参数或localStorage中判断用户曾经是否主动授权过。
②若用户完成主动授权,则在localStorage中持久化存入标志信息,建议设置一个过期时间,可以借助storejs第三方库。

// 判断微信平台
const ua = navigator.userAgent.toLowerCase();
const isWechat = /micromessenger/.test(ua);
// 解析URL的query参数
function query() {
  const map = {};
  const params = window.location.search.substring(1).split("&");
  params.forEach((item) => {
    const temp = item.split("=");
    map[temp[0]] = temp[1];
  });
  return map;
}
// 判断用户是否曾经主动授权过
const auth = localStorage.getItem("Auth") || query().state || "";
const code = query().code || "";
const lastCode = sessionStorage.getItem("vercode");
// 在微信平台中
if (isWechat) {
  // 储存本次code
  sessionStorage.setItem("vercode", code);
  // 二次分享无code,保证每次页面刷新,均存在有效的code,
  if (code === lastCode || !code) {
    window.location.replace(shareURL);
  } else {
    // 发起请求(下一节)
  }
}

发起请求
①当回调页在微信浏览器中记载完毕时,需要做的第一件事就是发起一个请求,向后端发送code参数,并判断用户是否主动授权过,从后端获取用户的基本信息。
②若用户未曾主动授权则只能获取到openid,主动授权过才能获取到更多基本信息。

// AJAX请求
const ajax = {
  get(url) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", url);
      xhr.send();
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          if (xhr.status >= 200 && xhr.status < 300) {
            const res = JSON.parse(xhr.response);
            if (!res.code) resolve(res.data);
            else reject(res);
          }
        }
      };
    });
  },
};
// 1是静默授权,0是主动授权
ajax
  .get(`${location.protocol}//${location.hostname}:3000/auth?code=${code}&type=${auth ? 0 : 1}`)
  .then((res) => {
    UserInfo.openId = res.openId;
    if (res.UserName && res.UserPhoto) {
      UserInfo.UserName = res.UserName;
      UserInfo.UserPhoto = res.UserPhoto;
      // 主动授权后,储存记录
      localStorage.setItem("Auth", 1);
    }
  })
  .catch((e) => console.log(e.message));

引导用户主动主动授权
①在页面中放置一个按钮用于交互,绑定事件,出发主动授权,弹出主动授权框。已主动授权过的用户则点击无效。

const authBtn = document.getElementById("authBtn");
// 按钮点击事件,获取主动授权
const handleAuth = () => {
  if (!auth) {
    window.location.replace(authURL);
  }
};
authBtn.addEventListener("click", handleAuth);

后端

express搭建一个服务器
①由于前后端不在一个服务器上,所以需要解决跨域的问题。

const express = require("express");
const app = express();
// CORS跨域设置
app.all("*", function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By", " 3.2.1");
  res.header("Content-Type", "application/json;charset=utf-8");
  next();
});
app.listen(3000, () => console.log("服务器已开启"));

通过code换取access_token和openid
①前端会将code传给后端,后端需要用该code向微信获取access_tokenopenid等信息。
②每个用户对于同一公众号的openid都是唯一固定不变的。
③静默授权的流程到此结束。

const https = require("https");
const getAccessToken = (code) => {
  return new Promise((resolve, reject) => {
    const URL = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`;
    https
      .get(URL, (res) => {
        let rawData = "";
        res.on("data", (data) => (rawData += data));
        res.on("end", () => resolve(JSON.parse(rawData)));
      })
      .on("error", (e) => console.log(e.message));
  });
 };

通过access_token和openid获取UserInfo
①用户主动授权后,通过上一步获取的access_tokenopenid可以进一步获取用户的基本信息。

const getUserInfo = (accessToken, openId) => {
  return new Promise((resolve, reject) => {
    const URL = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`;
    https
      .get(URL, (res) => {
        let rawData = "";
        res.on("data", (data) => (rawData += data));
        res.on("end", () => resolve(JSON.parse(rawData)));
      })
      .on("error", (e) => console.log(e.message));
  });
};

部署GET接口
①根据前端传的type参数确定本次获取的是静默授权信息还是主动授权信息,返回用户基本信息。

app.get("/auth", function (req, res) {
  const wxAuth = async () => {
    // 解析query参数,字符串类型
    const { code, type } = req.query;
    const token = await getAccessToken(code);
    // 请求报错
    if (!token.access_token) {
      return res.send({ code: token.errcode, message: token.errmsg });
    }
    // 静默授权
    if (Number(type)) {
      return res.send({
        code: 0,
        message: "success",
        data: { openId: token.openid },
      });
      // 主动授权
    } else {
      const UserInfo = await getUserInfo(token.access_token, token.openid);
      // 请求报错
      if (!UserInfo.openid) {
        return res.send({ code: UserInfo.errcode, message: UserInfo.errmsg });
      }
      return res.send({
        code: 0,
        message: "success",
        data: {
          openId: UserInfo.openid,
          UserName: UserInfo.nickname,
          UserPhoto: UserInfo.headimgurl,
        },
      });
    }
  };
  wxAuth();
});