egg搭建微信授权

微信授权采用的是oauth2.0的授权机制,这个与微博,支付宝等授权机制都是通用的。授权会产生access_token这个票据只是为了做授权的认证票据。可以根据这个票据换取用户的详细信息。微信授权机制有一个刷新机制,这个暂时可以不用管,现在基本上都是每次都去授权,都去获取一次access_token。值得注意的一点:订阅号不管认不认证,授权后都没办法拿到用户信息。只有认证过的服务号才有权限拿用户信息。

授权流程

  • 访问服务器的获取收取地址接口,一般我们把这个称为重定向,拿到这个地址,服务器会重定向到授权页面,这就是所谓的一跳。如下图。


    授权图
  • 用户点击允许,微信回调我们配置的回调地址,这时候地址后面会带有code参数。这就是所谓的二跳。

  • 二跳回来后根据code换取access_token,根据这个票据就能换取到用户信息。

egg中代码实现

  • 前端直接访问https://xxx.com/receive,该方法会获取是否有jwt,如果有jwt,则从定向到前端的主页面,否则的话,进行微信授权的跳转。
  • 路由router.js的配置。
'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  // 公众号校验
  router.get('/wechat/check', controller.wechat.user.check);
  // 用户授权
  router.get('/receive', controller.user.receive.index);
  // 重定向地址。一跳地址
  router.get('/wechat/user/redirect', controller.wechat.user.redirect);
  // 授权回调地址。二跳地址
  router.get('/wechat/user/oauth', controller.wechat.user.oauth);
  // 获取签名
  router.get('/wechat/user/signa', controller.wechat.user.signa);
};

  • 控制器controller的配置
'use strict';


const Controller = require('egg').Controller;

class UserController extends Controller {

  // 微信配置接口域名的时候,需要有验证地方
  async check() {
    const echostr = this.ctx.service.wechat.user.check();
    if (echostr) {
        // 加密后的数据需要与签名保持一致,验证成功
        this.ctx.body = echostr;
    } else {
        this.ctx.body = 'Fail';
    }
  }

  // 重定向页面
  async redirect() {
    const url = this.ctx.service.wechat.user.fetchUrl();
    // 重新定向到用户授权的微信页面
    this.ctx.redirect(url);
  }


  // 授权成功回调接口
  async oauth() {
    const { ctx } = this;
    const { code } = ctx.query;
    if (!code) { ctx.failure(422, 'code missing'); }
    const { access_token, openid } = await ctx.service.wechat.user.fechAccessToken(code);
    if (!access_token || !openid) { ctx.failure(422, 'access_token  openid missing'); }
    const userInfo = await ctx.service.wechat.user.getUserInfo(access_token, openid);
    const token = await ctx.service.wechat.user.saveInfo(userInfo.data);

    // 第一种方式:token拼到h5的地址后面
    // 授权成功后,将token拼接到地址给到前端
    // const url = `${this.ctx.app.config.wechat.Domain}?token=${token}`;

    // 第二种方式:存入session,从session去拿token。
    ctx.session.token = token;

    ctx.redirect(url);
  }

  // 获取微信签名,用于JSSDK的功能
  async signa() {
    const { ctx } = this;
    ctx.validate({ url: 'string' });
    // 先拿到token
    const { token } = await ctx.service.wechat.token.fetchToken();
    if (!token) { ctx.failure(422, 'assess_token missing') };
    // 根据token换ticket
    const { ticket } = await ctx.service.wechat.ticket.fetchTicket(token);
    if (!ticket) { ctx.failure(422, 'ticket missing') };
    const result = await ctx.service.wechat.user.signa(ticket);
    ctx.success(result);
  }
}

module.exports = UserController;

  • 代码核心的实现service的代码
'use strict';

const crypto = require('crypto');

const BaseService = require('../base');

class UserService extends BaseService {
  constructor(ctx) {
    super(ctx);
    this.model = ctx.model.Wechat.User;
  }


  // 签名方法,前端需要的签名字段有这四个 { appId, timestamp, nonceStr, signature }
  async signa(ticket) {
    const { ctx } = this;
    const { url } = ctx.request.body;

    // 随机字符串
    // const noncestr = 'bl32uux2kzhesx7';
    const noncestr = Math.random().toString(36).substr(2, 15);

    // 时间戳
    // const timestamp = '1521097369';
    const timestamp = parseInt(new Date().getTime() / 1000, 10);

    // 签名
    const string = `jsapi_ticket=${ticket}` + 
                   `&noncestr=${noncestr}` +
                   `&timestamp=${timestamp}` + 
                   `&url=${url}`;
    const signature = crypto.createHash('sha1').update(string).digest('hex'); 
    const appId = ctx.app.config.wechat.AppId;
    return { timestamp, noncestr, signature, appId };
  }




  //  ========== 授权需要的方法 ===============

  // 微信配置接口域名的时候,需要有验证地方
  check() {
    const { ctx } = this;
    const { signature, nonce, timestamp, echostr } = ctx.query;
    const token = ctx.app.config.wechat.Token;
    // 加密
    const str = [token, timestamp, nonce].sort().join('');
    const sha = crypto.createHash('sha1').update(str).digest('hex');
    return sha === signature ? echostr : null;
  }


  // 拼接授权页面的参数和地址
  fetchUrl() {
    // 授权成功后,二跳重定向,GET调用的服务器接口。就是controller中的oauth方法
    const target = this.ctx.app.config.wechat.Domain + 'wechat/user/oauth';
    // 两种类型 snsapi_base 这个只能获取到openid
    const scope = "snsapi_userinfo";
    // const scope = "snsapi_base";
    // 传递参数 /wechat-redirect?url=前端首页地址&b=前端可能需要的额外参数。 可以传递多个参数
    const { url, b } = this.ctx.request.body;
    const state = `${url}_${b}`;
    return this.getAuthorizeUrl(scope, target, state);
  }





  //  ===============  微信授权步骤 ============

  // 第一步:用户同意授权,获取code,授权一跳页面,进入让用户点击授权的页面。授权完毕,二跳到自己服务器上的页面
  // 默认 scope=snsapi_base 这个只能获取到openid
  getAuthorizeUrl(scope = 'snsapi_base', target, state) {
    const encodeTarget = encodeURIComponent(target);
    return `https://open.weixin.qq.com/connect/oauth2/authorize?` +
            `appid=${this.ctx.app.config.wechat.AppId}&` +
            `redirect_uri=${encodeTarget}&` +
            `response_type=code&`+
            `scope=${scope}&`+
            `state=${state}#wechat_redirect`
  }
  // 第二步:通过code换取网页授权access_token
  async fechAccessToken(code) {
    const url = `https://api.weixin.qq.com/sns/oauth2/access_token?` +
              `appid=${this.ctx.app.config.wechat.AppId}&` +
              `secret=${this.ctx.app.config.wechat.AppSecret}&` +
              `code=${code}&` +
              `grant_type=authorization_code`
    const result = await this.ctx.curl(url, { 
      dataType: 'json', 
      timeout: 5000
    });
    return result.data;
  }
  // 第三步:拉取用户信息(需scope为 snsapi_userinfo)
  async getUserInfo(token, openid, lang = 'zh_CN') {
    const url = `https://api.weixin.qq.com/sns/userinfo?`+
              `access_token=${token}&` +
              `openid=${openid}&` +
              `lang=${lang}`;
    const result = await this.ctx.curl(url, {
      dataType: 'json',
      timeout: 5000,
    });
    return result;
  }
}

module.exports = UserService;

调试注意事项

测试账号
  • Token这个值是自己设置的,用于上面URL的合法校验。这边填写的地址是/wechat/check。会校验上面路由的check方法。

  • 要测试授权需要,接口配置信息先把域名通过,然后在底下的文档有一个网页授权获取用户信息的,也需要配置域名。才会回调服务器的接口。


    需要和上面的域名一致
修改后的域名

代码解析

  • 代码核心都是和前面写的授权流程一样。
  • 首先通过ngrok将本地的服务映射到外网的域名上。
  • 其次先把接口的域名信息配置好,通过/wechat/check方法,将微信传来的值进行sha1的加密,验证signature是否一致,最终把echostr返回给微信后台,则可以验证通过。
  • 接下来就是浏览器上访问域名/wechat/user/redirect,这个方法会根据不同参数拼接出授权的一跳地址,后台直接重定向到授权页面。
  • 当用户做了相关操作,如果是同意,回调的接口/wechat/user/oauth可以拿到一个code
  • 根据回调的code,请求微信的接口可以换取到access_tokenopenid
  • 根据access_tokenopenid可以解析到用户的信息,头像昵称位置。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354