微信开发文档关注公众号

2017-09-09 15:07:55 csdn_Info 阅读数 6490

需求

  • 用户通过微信扫码关注公众号获取红包

解决方案


步骤

  • 生成带参数的二维码
  • 扫码

    • 是否关注公众号

      • 已关注,则微信会将带场景值扫描事件推送给开发者,根据场景判断当前二维码是否失效

        • 失效,公众号推送消息:当前二维码已失效
        • 未失效,获取当前扫码用户openid查询是否已经领取过红包
          1. 已领取,则公众号推送消息到微信用户通知今日已领取
          2. 未领取
            • 则调用发送红包接口,发送红包,发送成功,则存储当前微信用户信息,包含唯一 openid。
            • 设置当前扫描二维码失效
      • 未关注,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者

        • 根据场景值查询二维码是否失效
          • 失效,公众号推送消息:当前二维码已失效
          • 未失效,获取当前扫码用户openid查询是否已经领取过红包
            1. 已领取,则公众号推送消息到微信用户通知今日已领取
            2. 未领取
              • 则调用发送红包接口,发送红包,发送成功,则存储当前微信用户信息,包含唯一 openid。
              • 设置当前扫描二维码失效

  • 生成带参数的二维码
    • 目前有2种类型的二维码:
      • 临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
      • 永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。
    • 用户扫描带场景值二维码时,可能推送以下两种事件:
      1. 用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
      2. 用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。
    • 获取带参数的二维码的过程包括两步
      1. 首先创建二维码ticket
      2. 然后凭借ticket到指定URL换取二维码

这里我们选择永久二维码


  • 然后,将TOKEN添加到请求ticket地址,进行请求
    • 成功返回说明
      {
      “ticket”:”xxx”,
      “expire_seconds”:60,
      “url”:”http:\/\/weixin.qq.com\/q\/xxx”
      }
    • 参数说明
      1. ticket :二维码ticket,凭借此ticket可以在有效时间内换取二维码
      2. expire_seconds:二维码有效时间,以秒为单位。 最大不超过2592000(即30天)
      3. url:二维码图片解析后的地址


  • 用户扫描带参数的二维码
    • 用户未关注时,进行关注后的事件推送,xml数据包
<xml>
    <ToUserName>接收人</ToUserName>
    <FromUserName>发送人</FromUserName>
    <CreateTime>创建时间,时间戳</CreateTime>
    <MsgType>消息类型【event】</MsgType>
    <Event>事件类型【subscribe】</Event>
    <EventKey>事件KEY值,qrscene_为前缀【scan事件无此前缀,】</EventKey>
    <Ticket>二维码的ticket</Ticket>
</xml>

    • 用户已关注时的事件推送
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[SCAN]]></Event>
<EventKey><![CDATA[SCENE_VALUE]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>

*:EventKey也就是生成二维码时的场景ID,比如生成二维码时传的场景ID为 A,那么此处EventKey 则为 A


  • 知道了这个EventKEY值,
    • 查询当前二维码是否失效
  • 知道了ToUserName也就是接收人openid

    • 查询当前微信唯一openid是否存在
  • 如果当前二维码未失效,并且当前 openid 不存在数据库中,那么则可以调用发送红包接口进行红包发送。



<xml>
    <sign><![CDATA[红包签名key]]></sign>
    <mch_billno><![CDATA[订单号]]></mch_billno>
    <mch_id><![CDATA[商户号]]></mch_id>
    <wxappid><![CDATA[公众号APPID]]></wxappid>
    <send_name><![CDATA[发送方名称]]><send_name>
    <re_openid><![CDATA[接收人openid]]></re_openid>
    <total_amount><![CDATA[红包金额]]></total_amount>
    <total_num><![CDATA[红包数量]]></total_num>
    <wishing><![CDATA[恭喜发财]]></wishing>
    <client_ip><![CDATA[调用接口的机器Ip地址]]></client_ip>
    <act_name><![CDATA[活动名称]]></act_name>
    <remark><![CDATA[备注]]></remark>
    <scene_id><![CDATA[场景值,非必须]]></scene_id>
    <consume_mch_id><![CDATA[资金授权商户号,非必须]]></consume_mch_id>
    <nonce_str><![CDATA[随机字符串]]></nonce_str>
    <risk_info>活动信息,非必须</risk_info>
</xml>
  • 红包签名算法
  • 这里请求的时候要带上请求证书
    • 证书说明地址
    • 证书下载地址
      • 微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>证书下载
  • 使用证书

    • 使用商户证书
      ◆ apiclient_cert.p12是商户证书文件,除PHP外的开发均使用此证书文件
      ◆ 商户如果使用.NET环境开发,请确认Framework版本大于2.0,必须在操作系统上双击安装证书apiclient_cert.p12后才能被正常调用。
      ◆ 商户证书调用或安装都需要使用到密码,该密码的值为微信商户号(mch_id)
  • 成功返回说明,xml

<xml>
    <return_code><![CDATA[SUCCESS]]></return_code>
    <return_msg><![CDATA[发放成功.]]></return_msg>
    <result_code><![CDATA[SUCCESS]]></result_code>
    <err_code><![CDATA[0]]></err_code>
    <err_code_des><![CDATA[发放成功.]]></err_code_des>
    <mch_billno><![CDATA[00100104xxx]]></mch_billno>
    <mch_id>10010404</mch_id>
    <wxappid><![CDATA[wx6fa7e3bab7e15415]]></wxappid>
    <re_openid><![CDATA[onqOjjmM1tad-3ROpncN-yUfa6uI]]></re_openid>
    <total_amount>1</total_amount>
</xml>

代码见第二章


2020-03-01 00:11:57 LiuAustin 阅读数 558

在这里插入图片描述
互联网时代,不管是以哪种形式存在的应用,移动端或者PC网站,注册登录功能是用户访问应用的第一步,可以说,注册登录用的方不方便在一定程度上能决定用户的去留。对于用户来说,能够越简单,不用动手做过多操作就能达到同样效果的功能是最好不过的。今天就来介绍一下PC网站如何通过扫描微信二维码关注公众号,直接完成注册登录。

1、思考

最近在刷乐观数据的时候,发现网站注册登录流程有点不一样,都没怎么操作,只是用手机扫了一个二维码进入到关注公众号的页面,然后关注公众号,就收到登入成功的消息提醒,电脑上也直接进入到平台首页,而且还显示了我的微信头像、昵称,很是快速。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这一看就不是用扫描微信开放平台的那种二维码做的,这种还更轻巧些,那这是怎么实现的呢?于是就去翻阅微信开发文档理了一下,就明白了。
在这里插入图片描述

2、思路

其实这个功能实现原理很简单,如果按公众号开发文档的步骤来就两步:

账号管理 -> 生成带参数二维码接口-> 创建临时二维码

消息管理 -> 接收事件推送 -> 扫描带参数二维码事件

接下来 来看开发过程,注意,已微信认证的服务号才有生成带参数二维码的功能

3、开发

关于二维码,微信根据实际业务不同提供了两种创建方法,临时和永久的,其实也什么差别,这里就举例说明创建临时二维码。

3.1 生成调用凭据access_token(这个要做缓存,第一次先存缓存,之后都从缓存取,过期了再重新获取,这里没有做缓存)

/**
 * @author liuqh
 * @date 2019/8/6 下午1:49
 * @description
 */
@Component
public class GetToken {

    private Logger logger = LoggerFactory.getLogger(GetToken.class);

    public AccessToken getToken(String appid, String appSecrect) {
        AccessToken token;
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid
                + "&secret=" + appSecrect;
        String result = CommonUtil.httpsRequest(url, "GET", null);
        JSONObject jsonObject = JSONObject.fromObject(result);
        if (jsonObject != null) {
            try {
                token = new AccessToken();
                token.setAccess_token(jsonObject.getString("access_token"));
                token.setExpires_in(jsonObject.getLong("expires_in"));
            } catch (Exception e) {
                token = null;
                e.printStackTrace();
                logger.error("系统出错了!");
            }
        } else {
            token = null;
            // 获取token失败
            logger.error("jsonObject为空,获取token失败");
        }
        return token;
    }
}

工具类 CommonUtil

/**
 * 
 * @Title: 请求CommonUtil.java
 * @Description: 通用工具类
 * @author
 *
 */
public class CommonUtil {
  private static Logger log = LoggerFactory.getLogger(CommonUtil.class); 

  /**
   * 
   * @Title: httpsRequestJson
   * @Description: 发送https请求 ---返回JSONObject
   * @param requestUrl 请求地址
   * @param requestMethod 请求方式(GET、POST)
   * @param outputStr 提交的数据
   * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
   * @return JSONObject 返回类型
   * @throws  
   */
  public static JSONObject httpsRequestJson(String requestUrl, String requestMethod,
                        String outputStr) {
    JSONObject jsonObject = null;
    try {
      // 创建SSLContext对象,并使用我们指定的信任管理器初始化
      TrustManager[] tm = { new MyX509TrustManager() };
      SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
      sslContext.init(null, tm, new java.security.SecureRandom());
      // 从上述SSLContext对象中得到SSLSocketFactory对象
      SSLSocketFactory ssf = sslContext.getSocketFactory();
      URL url = new URL(requestUrl);
      HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
      conn.setSSLSocketFactory(ssf);
      conn.setDoOutput(true);
      conn.setDoInput(true);
      conn.setUseCaches(false);
      // 设置请求方式(GET/POST)
      conn.setRequestMethod(requestMethod);
      // 当outputStr不为null时向输出流写数据
      if (null != outputStr) {
        OutputStream outputStream = conn.getOutputStream();
        // 注意编码格式
        outputStream.write(outputStr.getBytes("UTF-8"));
        outputStream.close();
      }
      // 从输入流读取返回内容
      InputStream inputStream = conn.getInputStream();
      InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
      BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
      String str = null;
      StringBuffer buffer = new StringBuffer();
      while ((str = bufferedReader.readLine()) != null) {
        buffer.append(str);
      }
      // 释放资源
      bufferedReader.close();
      inputStreamReader.close();
      inputStream.close();
      inputStream = null;
      conn.disconnect();
      jsonObject = JSONObject.parseObject(buffer.toString());
    } catch (ConnectException ce) {
    } catch (Exception e) {
    }
    return jsonObject;
  }
}

生成二维码,

scene_id这个字段是自定义的场景值,比如我们这个扫码场景是注册登录,

在处理事件推送那边可以取到,用来判断场景。

/**
 * @ClassName WxPublicQrcode
 * @Author liuqh
 * @Date 2020-02-29 20:15
 * @Description
 */
public class WxPublicQrcode {

    private static final String createUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=";
    private static final String showqrcodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";

    @Autowired
    private GetToken getToken;
    
    /**
     * 1、创建二维码ticket
     * @return
     * {"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm
     * 3sUw==","expire_seconds":60,"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"     }
     */        
    public static JSONObject create () {
        String token = getToken.getToken("appid","appSecrect").getAccess_token();
        Map<String, Object> map = new HashMap<>();
        map.put("expire_seconds",60);
        map.put("action_name","QR_SCENE");
        Map<String,Object> sceneId = new HashMap<>();
        sceneId.put("scene_id",123);
        Map<String, Object> scene = new HashMap<>();
        scene.put("scene",sceneId);
        map.put("action_info",scene);
        String param = JSON.toJSONString(map);
        System.out.println(param);
        JSONObject jsonObject = CommonUtil.httpsRequestJson(createUrl+token, "POST", param);
        System.out.println(jsonObject);
        return jsonObject;
    }

    /**
     * 2、通过ticket换取二维码
     * @return ticket正确情况下,http 返回码是200,是一张图片,可以直接展示或者下载
     */
    public static JSONObject showqrcode () {
        JSONObject create = create();
        String ticket = create.getString("ticket");
        try {
            ticket =  URLEncoder.encode(ticket,"utf-8");
            JSONObject jsonObject = CommonUtil.httpsRequestJson(showqrcodeUrl+ ticket, "GET", null);

            System.out.println(jsonObject);
            return jsonObject;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

3.2 处理事件推送

关于如何处理接收事件,参考微信公众号关注或取关后再处理我们自己的业务逻辑这篇文章

判断Event值是subscribe且EventKey值是qrscene_为前缀,后面为二维码的参数值,那么就是用户还未关注公众号,则用户可以关注公众号,做各种操作,存数据库什么,FromUserName就是用户的openid,Event值是SCAN且EventKey值是是一个32位无符号整数,即创建二维码时的二维码scene_id,则用户已经关注公众号,再做相对应的操作。
在这里插入图片描述

2019-07-17 13:27:37 weixin_45418036 阅读数 1451


  上周二开晨会分配任务的时候,分配到了一个微信扫码关注公众号的需求。刚开始以为只要截个公众号二维码的图,然后按照UI出的设计稿把二维码放到指定位置,再加上一波加边框加阴影的操作提交就完事了。所以当部门大佬问多久能做完的时候,我毫不犹豫地说:小Case啦,两天妥妥的!

  回到座位上本想着时间还早,先刷会微博。刷着刷着看到广州东站的宜家要搬迁的消息,活动大减价全场5折起!!忍不住点进去宜家的网上商城,看了两圈好想剁手买买买。这时,部门大佬站了起来,好像正往我这边看。算了算了,先关注宜家公众号干活去吧,拿出手机准备扫码的时候整个人都懵了…

  诶?诶诶?!早上的需求好像是要实现扫码关注公众号并登陆的,但浏览器怎么会知道我扫码了,而且扫的还是登陆用的二维码…这怎么跟想象的不一样,我还打包票说两天内做完,真的完了完了。

  当然,两天后这扫码的功能还是经过测试按时提交,并在线上稳定运行了一周。如何才能快速实现,并尽量少踩坑,我想,有些思路和和代码写下来,以后可能会用得着。

  项目主要以Nodejs进行开发,优先选用Koa2 + ioredis等一些比较轻量级的模块实现,配合Alpine Linux制作Docker Image,最后得到的一个开箱即用的Docker镜像也仅仅只有33M

  浏览项目的完整代码可以点击这里github,如果对你有帮助欢迎Star。

先整理一下需求:

  • 登录入口实现扫描二维码关注公众号并登录网站。已关注的直接跳转登陆,未关注的等待用户关注再跳转;
  • 新的公众平台扫码登录机制代替原有的微信开放平台的扫码登录;
  • 扫码关注后需要根据情况返回不同的提示(欢迎)信息。

  但目前已上线的网站同时提供微信扫码和手机/邮箱注册登录,新需求实际是想让更多的用户关注公众号。完全按照需求上做的话就会变成强制用户必须关注公众号,否则无法完成登录。考虑到市场上有不少同类型产品,这种强制的行为可能会导致用户反感,从而选择其他产品。

经讨论需求改为:

  • 保留现有的微信扫码和手机/邮箱注册登录,完成注册/登录流程后,新增一个扫码关注公众号的页面;
  • 用户扫码关注,关注后利用**unionid机制**绑定账户,让手机/邮箱注册的用户以后可以直接微信扫码登录;
  • 点击关注后,网站自动跳转进入控制台,或点击暂不关注直接跳转;
  • 扫码关注后需要根据情况返回不同的提示(欢迎)信息。

实现思路和步骤:

  1. 实现一个与微信公众号平台交互的API,接收并处理公众号推送的事件(关注、扫码和文字消息等);
  2. 实现一个生成二维码的API供浏览器调用,API可通过参数声明需要返回的格式;
  3. 请求公众平台 →【生成带参数的二维码】接口生成带有场景值的二维码,生成成功后记录到数据库并返回;
  4. 浏览器获取二维码信息后轮询二维码的扫描状态,扫描成功后自动跳转;
  5. 用户扫码后,公众平台会向1实现的API推送事件,如果是关注就获取用户信息,然后记录到数据库。

第一步,搭建Koa的环境并接入微信公众平台

  提供的源码里包含删减过的 Koa2和 koa-router的代码,也可以使用原版的代码。建议使用Nodejs10以上版本,特别是Nodejs12,换了新的HTTP解析器(llhttp)性能直接提高了一倍。

安装依赖

package.json

"dependencies": {
    "debug": "^4.1.1",
    "got": "^9.6.0",
    "ioredis": "^4.10.0",
    "mime-types": "^2.1.24",
    "negotiator": "^0.6.2",
    "xml2js": "^0.4.19",
    "ylru": "^1.2.1"
 }

如果是直接用官方的 Koa,mime-types,negotiator,ylru都不用安装

目录结构

dir.png

Koa APP的代码结构跟官方的栗子差不多,就直接看吧
app.js

const http = require('http')
const Koa = require('./vendor/koa2/application')
const XMLParser = require('./middlewares/XMLParser')
const router = require('./routes/wechat')
const app = new Koa()
​
app.use(XMLParser) // 解析xml的中间件,用于预处理微信公众号推送的事件
​
app.use(router.routes())
app.use(router.allowedMethods())
​
http.createServer(app.callback()).listen(3000)

middlewares/XMLParser.js

const parseXML = require('xml2js').parseString
const debug = require('debug')('xml-parse')const parse = (req, options = {}) => {
    return new Promise((resolve, reject) => {
        let xml = ''
        req.on('data', chunk => { xml += chunk.toString('utf-8') })
           .on('error', reject)
           .on('end', () => parseXML(xml, options, (err, res) => {
                if (err) reject(err)
                resolve(res)
        }))
    })
}
​
module.exports = async (ctx, next) => {
    // 这里先尝试直接匹配,匹配失败再到mime库里查询
    if (ctx.request.type === 'text/xml' || ctx.is('xml')) {
        try {
            ctx.request.body = await parse(ctx.req)
        } catch (e) {
            debug(e.message)
        }
    }
    await next()
}

routes/wechat.js

const Router = require('../vendor/koa-router')
const wechatController = require('../controllers/wechat')
​
​
const router = new Router({
    prefix: '/wechat'
})// 测试号配置接口信息时需要校验,但传输的数据跟推送消息一样,所以放在同一个controller里处理
// conntroller的完整path是/wechat/event,这个后面配置测试号URL的时候会用到
router.get('/', ctx => ctx.body = 'hello wechat')
      .get('/event', wechatController)
      .post('/event', wechatController)
​
module.exports = router

先新建一个配置文件,与app.js同目录
config.js
WXMP的信息暂时留空,到配置微信公众平台的时候再填写

const CACHE = {
    host: 'localhost',
    port: 6379
}// WeChat Media Platform
const WXMP = {
    appID: '',
    appSecret: '',
    token: ''
}
​
module.exports = {
    CACHE,
    WXMP
}

/controllers/wechat.js

const { WXMP } = require('../config');
const { SHA1 } = require('../utils/mUtils')
​
module.exports = async (ctx, next) => {
    const token = WXMP.token
    const { signature, nonce, timestamp, echostr } = ctx.query
​
    /**
    * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319
    * 1)将token、timestamp、nonce三个参数进行字典序排序
    * 2)将三个参数字符串拼接成一个字符串进行sha1加密
    * 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
    */
    const str = [token, timestamp, nonce].sort().join('')const signVerified = SHA1(str) === signature
​
    if (!signVerified) {
        ctx.status = 404 // 可以不设为404,koa默认的状态值就是404
        return
    }if (ctx.method === 'GET') ctx.body = echostr
    else if (ctx.method === 'POST') {
        // 实现思路里的第一步
        // 推送的消息会以POST的方式进到这里,暂时用不着,先放着
    }
}

  来到这里,先测试一下Web服务是否能正常跑起来。这里使用Postman直接发请求,也可以用浏览器访问http://localhost:3000/wechat/

hello_wechat.png

接下来配置一下微信公众平台。

  线上的环境经不起折腾,还是用公众号测试号进行调试吧,如果你是刚开始接触微信公众号开发,推荐使用测试号。

扫码登陆后会看到这样一个界面:

mp-sandbox.png

  appID和appsecret是系统生成的,只需要填写URL和Token。把appID和appsecret贴到之前创建的config.js中,token自己随便输入,保证两个token一致即可。配置URL之前,我们需要一个域名和一个内网穿透的环境。旧版本的微信web开发工具提供一个类似的方式,让微信服务器可以向我们在内网的机器推送消息,新版没有这功能我们就自己百度一个吧。

  粗略比较了一下,发现续断内网穿透很大方,只要9.9就有两条8M永久使用的隧道(只比KFC会员价的原味鸡贵4毛,尊贵的VIP吃鸡怎么还这么贵(╯‵□′)╯︵┻━┻)。注册交了9.9入会费,装上客户端后进行简单配置就可以了,体验挺棒的。设置过程中发现他家支持好多系统,像群晖、OpenWRT那些都有,还有树莓派,看来吃尘多年的B+可以拿出来发挥余热了。好像还有一些有趣的功能,可惜体验隧道不支持。不过只要9.9,还要啥自行车呢,果断开干吧!

xd.png

  点击保存稍等一会会得到一个外网访问地址,类似http://sd8xxxxxxxxs.gzhttp.cn

tunnel.png

  接着把外网访问地址+之前定义的path(http://sd8xxxxxxxxs.gzhttp.cn/wechat/event)填写到测试号接口配置的URL中,然后点击提交。这时我们已经成功接入到微信公众平台了。

第二步,实现一个生成二维码的API并完善与微信公众号平台交互的API

  开始第二步之前,先来了解一下创建二维码及用户扫码后公众号给web服务推送消息的流程:

sequence.png

  首先在utils/wechat/文件夹中新建一个helper.js,负责提供公众号配置(用于下面创建wechat对象)和get/set access_token的两个方法。

utils/wechat/helper.js

const { WXMP } = require('../../config')
const { redis } = require('../dbHelper')const config = {
    MP: {
        appID: WXMP.appID,
        appSecret: WXMP.appSecret,
        token: WXMP.token,
        getAccessToken: async () => {
            let token = await redis.get('access_token')
            return token
        },
        saveAccessToken: async (data = {}) => {
            await redis.set('access_token', data.access_token ,'EX', data.expires_in)
        }
    }
}
​
module.exports = {
    ...config
}

  在utils/wechat/文件夹中新建一个wxmp.js,定义一个Wechat类

...
const api = {
    accessToken: 'token?grant_type=client_credential',
    user: {
        info: 'user/info?',
    },
    QRCodeTicket: 'qrcode/create?',
    QRCode: 'showqrcode?'
}class Wechat {
    constructor (opts) {
        // 这里的opts传入的是上面定义的config
        ...
        this.fetchAccessToken(true)
    }
    // 获取access_token
    async fetchAccessToken (init = false) {
        let token = await this.getAccessToken()if (!token) {
            token = await this.updateAccessToken()
            await this.saveAccessToken(token)
            token = token.access_token
        }
        return token
    }async updateAccessToken () {
        const url = api.accessToken + '&appid=' + this.appID + '&secret=' + this.appSecret
        return await got(url)
    }// 提供一个统一操作的入口,第一个参数传入操作函数名就可以拿到对应的配置
    async handle (operation, ...args) {
        const token = await this.fetchAccessToken()
        if (!token) return nullconst options = this[operation](token, ...args)
        let res = await wxGot(options)

        return res
    }// 获取用户信息
    getUserInfo (token, openID, lang) {
        const url = `${api.user.info}access_token=${token}&openid=${openID}&lang=${lang || 'zh_CN'}`return { url: url }
    }// 申请二维码Ticket
    getQRCodeTicket (token, sceneStr, timeout) {
        return {
            url: `${api.QRCodeTicket}access_token=${token}`,
            method: 'post',
            body: {
                "expire_seconds": timeout || 60,
                "action_name": "QR_STR_SCENE", // 临时二维码
                "action_info": {
                "scene": {
                    "scene_str": sceneStr
                }
            }
        }
    }
}
​
module.exports = Wechat

  我们继续回到刚才的/routes/wechat.js,增加 “+” 标识的代码(新增获取二维码的路由)

const Router = require('../vendor/koa-router')
const wechatController = require('../controllers/wechat')
+ const { createQRCodeMB } = require('../controllers/wechat')const router = new Router({
    prefix: '/wechat'
})// 测试号配置接口信息时需要校验,但传输的数据跟推送消息一样,所以放在同一个controller里处理
// conntroller的完整path是/wechat/event,这个后面配置测试号URL的时候会用到
router.get('/', ctx => ctx.body = 'hello wechat')
      .get('/event', wechatController)
      .post('/event', wechatController)
+     .get('/qrcode', createQRCodeMB)
​
module.exports = router

然后打开/controllers/wechat.js,公众号推送事件类型可以参考这里

const { WXMP } = require('../config');
const { SHA1, fmtNormalXML, streamToBuffer, createTimestamp } = require('../utils/mUtils')
const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
const MPConfig = require('../utils/wechat/helper').MP
const got = require('got')
const qr = require('../vendor/qr')
const fs = require('fs')
const pathResolve = require('path').resolve
​
const MP = new Wechat(MPConfig)
​
module.exports = async (ctx, next) => {
    ...
    if (ctx.method === 'GET') ctx.body = echostr
    else if (ctx.method === 'POST') {
        // 把数组形态的xmlObject转换可读性更高的结构
        const message = fmtNormalXML(ctx.request.body.xml)const msgType = message.MsgType
        const msgEvent = message.Event
        const userID = message.FromUserName
        let eventKey = message.EventKey
        let body = nullif (msgType === 'event') {
            switch (msgEvent) {
                // 关注&取关
                case 'subscribe':
                case 'unsubscribe':
                    body = await subscribe(message)
                    break
                // 关注后扫码
                case 'SCAN':
                    body = '扫码成功'
                    break
            }

            if (!!eventKey) {
                // 有场景值(扫了我们生成的二维码)
                let user = await MP.handle('getUserInfo', userID)
                let userInfo = `${user.nickname}${user.sex ? '男' : '女'}, ${user.province}${user.city})`
                if (eventKey.slice(0, 8) === 'qrscene_') {
                    // 扫码并关注
                    // 关注就创建帐号的话可以在这里把用户信息写入数据库完成用户注册
                    eventKey = eventKey.slice(8)
                    console.log(userInfo + '扫码并关注了公众号')
                } else {
                    // 已关注
                    console.log(userInfo + '扫码进入了公众号')
                }// 更新扫码记录,供浏览器扫码状态轮询
                await redis.pipeline()
                           .hset(eventKey, 'unionID', user.unionid || '') // 仅unionid机制下有效
                           .hset(eventKey, 'openID', user.openid)
                           .exec()
            }
        }
    }
}async function subscribe (message) {
    let userID = message.FromUserName
    if (message.Event === 'subscribe') {
        return '感谢您的关注'
    } else {
        // 用户取消关注后我们不能再通过微信的接口拿到用户信息,
        // 如果要记录用户信息,需要从我们自己的用户记录里获取该信息。
        // 所以建议创建用户时除了unionid,最好把openid也保存起来。
        console.log(userID + '取关了')
    }
}const templetData = fs.readFileSync(pathResolve(__dirname, '../vendor/qrcode-templet.html'))// 创建二维码
async function createQRCodeMB (ctx, next) {
    let userID = ctx.query.userID
    let type = +ctx.query.type
    let errno = 0
    let responseDate = {}
    let id = createTimestamp()let res = await MP.handle('getQRCodeTicket', id)if (res === null) errno = 1
    else {
        responseDate = {
            expiresIn: res.expire_seconds,
            id
        }let imgBuffer = await streamToBuffer(qr.image(res.url))
        let imgSrc = imgBuffer.toString('base64')if (type === 1) {
            // 返回图片
            ctx.body = `<img src="data:image/png;base64,${imgSrc}" />`
        } else if (type === 2) {
            // 返回一个自带查询状态和跳转的网页
            let templetValue = `
                <script>var imgSrc='${imgSrc}',id='${responseDate.id}',
                timeout=${responseDate.expiresIn},width=100,height=100</script>`
​
            ctx.body = templetValue + templetData.toString('utf-8')
        } else {
            // 返回图片内容
            responseDate.imgSrc = imgSrc
        }
     }if (!ctx.body) {
        ctx.body = {
            errno,
            ...responseDate
        }
    }
}
​
module.exports.createQRCodeMB = createQRCodeMB

  到这里应该是可以接收到公众号推送的扫码事件和生成二维码。

  保存后我们先测试一下,首先不带参数访问http://localhost:3000/wechat/qrcode

normal.png

  接着尝试获取二维码图片(使用参数type=1)并使用微信扫描二维码:

scan.png

  首次扫描二维码会提示关注,点击关注后数据库就会更新,控制台也会打印出类似 “XXX扫码并关注了公众号“ 的日志。但这时候公众号里应该会提示 ”该公众号提供的服务出现故障,请稍后再试“ 的提示,因为程序并没有把提示信息正确得返回。下一步我们需要格式化返回的信息(即ctx.body的内容)。

新增一个生成模板的文件/utils/tmpl.js
格式化给公众号返回的消息,这里只简单使用util.format来格式化消息。

const util = require('util')const msgTemplet = `
<xml>
    <ToUserName><![CDATA[%s]]></ToUserName>
    <FromUserName><![CDATA[%s]]></FromUserName>
    <CreateTime>%d</CreateTime>
    <MsgType><![CDATA[%s]]></MsgType>
    $msgBody$
</xml>
`const textMsg = `<Content><![CDATA[%s]]></Content>`
const imageMsg = `<Image><MediaId><![CDATA[%s]]></MediaId></Image>`
​
module.exports = (ctx, originMsg) => {
    let type = (ctx && ctx.type) || 'text'
    let msgTmpl = util.format(msgTemplet,
        originMsg.FromUserName,
        originMsg.ToUserName,
        Math.floor(new Date().getTime() / 1000),
        type
    )let body = ''switch (type) {
        case 'text':
            body = util.format(textMsg, ctx)
            break
    case 'image':
        break
    default:
        body = util.format(textMsg, '操作无效')
    }return msgTmpl.replace(/\$msgBody\$/, body)
}

接着我们在controllers/wechat.js增加一下 ”+“ 标记的代码

const { WXMP } = require('../config');
const { SHA1, fmtNormalXML, streamToBuffer, createTimestamp } = require('../utils/mUtils')
+ const { tmpl } = require('../utils/wechat')
const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
...
module.exports = async (ctx, next) => {
    const token = WXMP.token
    const { signature, nonce, timestamp, echostr } = ctx.query
​
    const str = [token, timestamp, nonce].sort().join('')
    ...
                // 更新扫码记录,供浏览器扫码状态轮询
                await redis.pipeline()
                           .hset(eventKey, 'unionID', user.unionid || '') // 仅unionid机制下有效
                           .hset(eventKey, 'openID', user.openid)
                           .exec()
            }
        }+       ctx.type = 'application/xml'
+       ctx.body = tmpl(body || ctx.body, message)
    }
}async function subscribe (message) {
    let userID = message.FromUserName
    if (message.Event === 'subscribe') {
        return '感谢您的关注'
    } else {
        console.log(userID + '取关了')
    }
}

  保存后再获取一次二维码并扫描,微信上就能正确显示提示信息了:

message.png

第三步,浏览器增加扫码状态轮询

  这块跟业务代码关系比较密切,所以不做详细介绍。共通点就是通过二维码返回id获取unionid(openid)的记录,然后按需处理,最后以cookies或其他方式更新登录状态。

轮询的代码可以参考vendor/qrcode-templet.html

async function waitToSubscribe(id, timeout) {
    let countdown = Math.ceil(timeout / 3);
    return new Promise((resolve, reject) => {
        const loop = async function() {
            let res = await ky.default.get("/wechat/check", {
                searchParams: { id }
            }).json();

            if (!res) return;
            if (res.errno === 0) resolve("subscribe");
            else if (res.errno === 2) reject("timeout");
            else if (countdown-- > 0) self.QRCodeTimer = setTimeout(loop, 3000);
        };
        loop();
    });
};

(async () => {
    try {
        await waitToSubscribe(id, timeout);
        window.location.href = "/wechat/";
    } catch (e) {
        history.go(0);
    }
})();

我们可以尝试获取集成好获取状态的二维码网页(使用参数type=2,实际使用时可以用iframe嵌套):

auto.png

总结:

  到这里,我们已经实现了:

1.  与微信公众号平台交互的API,能够接收并处理公众号推送的事件;
2.  生成二维码的API,并能分别以三种常用方式返回二维码;
3.  扫描二维码后,微信上能正常显示服务返回的提示信息,并成功记录在数据库中;
4.  当浏览器轮询二维码的扫描状态并获取到扫描结果后,自动跳转。

  以上几乎包含了公众号开发的完整流程,其他的功能可以参照公众号开发文档上的说明按需增加。这里有一点需要注意的,文中提到的unionid机制需要以公司身份申请正式的公众号和微信开放平台,并在开放平台上完成公众号绑定。同一个用户在已绑定公众号、小程序、网站应用等程序里会使用同一个unionid来确定用户的唯一性。

  像公众号网页授权、开放平台的网站应用授权(类似京东的扫码登录)和小程序的开发,等有空的时候再更新。码了这么多字,差点忘了要去宜家扫货,广告上说促销商品数量有限,万一卖完了岂不是错过了几个亿::>_<::,周末要找个时间过去看看才行。

最后附上Dockerfile 和源码地址

  预先拷贝文件到/build目录,便于生成更小的Docker Image

cp -rf vendor docker/build/vendor
cp -rf utils docker/build/utils
cp -rf routes docker/build/routes
cp -rf middlewares docker/build/middlewares
cp -rf controllers docker/build/controllers
cp app.js docker/build/app.js
cp config.js docker/build/config.js
FROM alpine
COPY package.json /var/www/wechat-mp/
WORKDIR /var/www/wechat-mp
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
 && apk add nodejs npm \
 && npm install --production --registry=https://registry.npm.taobao.org \
 && npm cache clean -f \
 && rm package-lock.json \
 && apk del npm \
 && rm -rf ~/.npm \
 && rm -rf /var/cache/apk/* \
 && rm -rf /root/.cache \
 && rm -rf /tmp/*
COPY build/ /var/www/wechat-mp/
CMD node app.js</pre>

github: https://github.com/lym0r9/wechat-mp

文章转载自快速实现微信扫码关注公众号/用户注册并登陆

2019-02-27 14:11:54 tom9238 阅读数 887

微信获取,操作公众号文章

项目地址:https://gitee.com/wrzhxy/wx_article

  • 我以为微信支付和微信企业付款文档就够坑爹了,直到我遇到了微信获取公众号文章。。。
  • 微信获取公众号文章就是指在第三方平台获取公众号作者写的文章。

一. 第三方开发需要到微信开放平台注册并且创建一个第三方平台应用

创建第三方平台时要填写很多资料,其中填写的回调地址需要记下来,等下要用到

第三方)

二. 获取文章

微信公众号的文章在微信里面叫做素材,获取素材列表和素材详细内容的文档在这里:

获取素材列表

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738734

素材列表里有media_id,用这个可以继续查素材详情以及各种操作

https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738730

三. 获取access_token
获取公众号文章前需要获取access_token

access_token有两种获取方式

  1. 公众号开发配置时根据APPsecret,appid获取
    但是这种获取方式只适合公众号自己获取自己的文章,不适合第三方平台获取别的作者的文章

  2. 作者授权给第三方平台,然后获取access_token

四. 获取authorizer_access_token

第三方平台按步骤获取用户微信授权时获取到的authorizer_access_token就是获取公众号文章需要的access_token

第三方授权登录接口文档

https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=58d58ee7c21059e4b719afa1ce71b76b5df38370&lang=zh_CN

  1. 预授权码获取
    1.1 推送ticket协议

1.2 更新component_access_token
1.3 更新预授权码
2. 引导用户进入授权页(扫码授权)

  • 这里的回调地址必须由网页进入,否则微信会提示域名入口页配置不正确
  1. 用户同意授权
  2. 授权后回调URI,得到授权码和过期时间
  3. 利用授权码调用公众号api 获取authorizer_access_token

文档这里不够详细,具体发送请求获取数据的操作可以参考我的源码,业务逻辑实现都在com.routz.wxarticlelist.demo.service.WxArticleServiceImp类中

2017-12-13 21:02:47 qq_26245325 阅读数 17462

用户关注公众号回调

参考资料

  1. 微信公众平台
  2. 微信公众号开发文档

基本信息

  1. AppID:开发者ID,微信公众号的唯一标识
  2. AppSecret:开发者密码,操作微信公众号的验证
  3. IP白名单:获取access_token时,需要IP白名单才可以获取
  4. OpenID:微信用户在当前公众号的唯一标识
  5. UnionID:微信用户在当前开放平台账号的所有公众号和应用情景下的唯一标识
  6. 服务器配置:
    (1)地址:配置以后,用户对公众号的操作(关注、发消息),微信会转到该链接
    (2)令牌:用作生成签名
    (3)密钥:消息加密
    开发-配置
    官方文档教程

7.验证服务器地址,6(1) 配置时要在链接下,原样输出微信发送GET请求的参数echostr,验证成功过后,才能配置完成。


公众号关注后的推送

  1. 在上一步的服务器地址中,加入以下代码
    (1) 接口加密,官方PHP-DEMO
include_once "wxBizMsgCrypt.php";
// 第三方发送消息给公众平台
$encodingAesKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
$token = "pamtest";
$timeStamp = "1409304348";
$nonce = "xxxxxx";
$appId = "wxb11529c136998cb6";
$text = "<xml><ToUserName><![CDATA[oia2Tj我是中文jewbmiOUlr6X-1crbLOvLw]]></ToUserName><FromUserName><![CDATA[gh_7f083739789a]]></FromUserName><CreateTime>1407743423</CreateTime><MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[eYJ1MbwPRJtOvIEabaxHs7TX2D-HV71s79GUxqdUkjm6Gs2Ed1KF3ulAOA9H1xG0]]></MediaId><Title><![CDATA[testCallBackReplyVideo]]></Title><Description><![CDATA[testCallBackReplyVideo]]></Description></Video></xml>";


$pc = new WXBizMsgCrypt($token, $encodingAesKey, $appId);
$encryptMsg = '';
$errCode = $pc->encryptMsg($text, $timeStamp, $nonce, $encryptMsg);
if ($errCode == 0) {
    print("加密后: " . $encryptMsg . "\n");
} else {
    print($errCode . "\n");
}

$xml_tree = new DOMDocument();
$xml_tree->loadXML($encryptMsg);
$array_e = $xml_tree->getElementsByTagName('Encrypt');
$array_s = $xml_tree->getElementsByTagName('MsgSignature');
$encrypt = $array_e->item(0)->nodeValue;
$msg_sign = $array_s->item(0)->nodeValue;

$format = "<xml><ToUserName><![CDATA[toUser]]></ToUserName><Encrypt><![CDATA[%s]]></Encrypt></xml>";
$from_xml = sprintf($format, $encrypt);

// 第三方收到公众号平台发送的消息
$msg = '';
$errCode = $pc->decryptMsg($msg_sign, $timeStamp, $nonce, $from_xml, $msg);
if ($errCode == 0) {
    print("解密后: " . $msg . "\n");
} else {
    print($errCode . "\n");
}

(2) 关注、发送消息的推送 官方文档

$xmlStr = $GLOBALS['HTTP_RAW_POST_DATA'];
if(!empty($xmlStr )){
   // 解析该xml字符串,利用simpleXML
   libxml_disable_entity_loader(true);
   //禁止xml实体解析,防止xml注入
   $requestXml = simplexml_load_string($xmlStr , 'SimpleXMLElement', LIBXML_NOCDATA);
   //判断该消息的类型,通过元素MsgType
   switch ($requestXml ->MsgType){
       case 'event':
           //判断具体的时间类型(关注、取消、点击)
           $event = $requestXml->Event;
           if ($event=='subscribe') { // 关注事件
               //查看用户是否首次关注
               $content = '欢迎关注!';
               if(isset($requestXml->EventKey)) {
                       // 扫特定二维码关注会携带相对应的参数,具体见下一篇
               }

           }elseif ($event=='CLICK') {//菜单点击事件
           }elseif ($event=='VIEW') {//连接跳转事件
           }
           break;
     case 'text'://文本消息
        break;
     case 'image'://图片消息
        break;
     case 'voice'://语音消息
        break;
     case 'video'://视频消息
        break;
     case 'shortvideo'://短视频消息
        break;
     case 'location'://位置消息
        break;
     case 'link'://链接消息
        break;
   }
}

(3)接受关注的数据格式
这里写图片描述

(4) 回复消息

//基本消息模板
private $mMsgTemplate = [
        'text' => '<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>',//文本回复XML模板
        'image' => '<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>',//图片回复XML模板
        'music' => '<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[music]]></MsgType><Music><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><MusicUrl><![CDATA[%s]]></MusicUrl><HQMusicUrl><![CDATA[%s]]></HQMusicUrl><ThumbMediaId><![CDATA[%s]]></ThumbMediaId></Music></xml>',//音乐模板
        'news' => '<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[news]]></MsgType><ArticleCount>%s</ArticleCount><Articles>%s</Articles></xml>',// 新闻主体
        'news_item' => '<item><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>',//某个新闻模板
    ];
//回复
echo sprintf($this->mMsgTemplate ['text'], $requestXml->FromUserName, $requestXml ->ToUserName, time(), $msgContent);