liuyu 5 年之前
父節點
當前提交
7e58c5568d
共有 3 個文件被更改,包括 374 次插入0 次删除
  1. 143 0
      app/controller/weixin.js
  2. 5 0
      app/router.js
  3. 226 0
      app/service/weixin.js

+ 143 - 0
app/controller/weixin.js

@@ -0,0 +1,143 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const uuid = require('uuid');
+const urljoin = require('url-join');
+const stringRandom = require('string-random');
+const Controller = require('egg').Controller;
+
+/**
+ * 微信认证,获得openid和用户信息,生成微信Jwt
+ */
+class WeixinController extends Controller {
+  /**
+   * 认证流程
+   * 1. 缓存原始请求地址,生成state和认证回调地址
+   * 2. 通过wxapi认证,获得认证code
+   * 3. 通过code获得openid,通过openid,查询绑定用户,创建jwt
+   * 4. jwt写入redis,返回认证code
+   * 5. 通过code获取jwt
+   */
+  // GET 请求认证
+  // response_type:
+  //       code - url带上code参数重定向到原始地址
+  //       store - 默认,认证结果写入sessionStore,然后重定向回请求页面(要求请求页面和认证服务在同一域名下)
+  //       token - url带上token参数重定向到原始地址
+  async auth() {
+    const { redirect_uri, code, test, type, response_type = 'store', uid, qrcode } = this.ctx.query;
+    if (test) {
+      return await this.authTest();
+    }
+    if (code) {
+      return await this.authBack(this.ctx.query);
+    }
+
+    this.ctx.logger.debug(`[auth] reditect_uri - ${redirect_uri}`);
+    // assert(redirect_uri, '回调地址不能为空');
+
+    // TODO: 保存原始请求地址
+    // const { config } = this.app;
+    // console.log('ctx.host: ', this.ctx.host);
+    // console.log('config.hostHeaders: ', config.hostHeaders);
+    // TODO: 保存原始请求地址
+    const state = uuid();
+    const key = `visit:auth:state:${state}`;
+    const val = JSON.stringify({ redirect_uri, type, uid, qrcode });
+    await this.app.redis.set(key, val, 'EX', 600);
+
+    // TODO: 生成回调地址
+    const { wxapi, authUrl = this.ctx.path } = this.app.config;
+    const backUrl = encodeURI(`${this.app.config.baseUrl}${this.config.authUrl}?state=${state}`);
+    const to_uri = `${wxapi.baseUrl}/api/auth?appid=${wxapi.appid}&response_type=code&redirect_uri=${backUrl}&connect_redirect=1#wechat`;
+    console.log('url-->' + to_uri);
+    this.ctx.redirect(to_uri);
+  }
+
+  // GET 认证回调
+  async authBack({ code, state }) {
+    // const { code, state, type, redirecturi } = this.ctx.query;
+    console.log(this.ctx.query);
+    this.ctx.logger.debug(`[auth-back] code - ${code}, state - ${state}`);
+    assert(code, 'code不能为空');
+    assert(state, 'state不能为空');
+    console.log('code-->' + code);
+
+    const { weixin } = this.ctx.service;
+    let openid;
+    try {
+      ({ openid } = await weixin.fetch(code));
+    } catch (err) {
+      await this.ctx.render('error.njk', { title: err.message, message: err.details });
+      return;
+    }
+    console.log('openid--->' + openid);
+    if (openid) {
+      const key = `visit:auth:state:${state}`;
+      const val = await this.app.redis.get(key);
+      const { redirect_uri, type, uid, qrcode } = JSON.parse(val);
+      console.log('redirect_uri-->' + redirect_uri);
+      const user = await this.ctx.service.user.findByOpenid(openid);
+      if (type === '0') {
+        // 通过openid取得用户信息
+        if (user) {
+          const token = await this.ctx.service.login.createJwt(user);
+          const to_uri = urljoin(redirect_uri, `?token=${token}`);
+          // TODO: 重定性页面
+          console.log('to_uri000-->' + to_uri);
+          this.ctx.redirect(to_uri);
+
+        } else {
+          console.log('rrr0000--->' + redirect_uri);
+          const touri = `${this.app.config.baseUrl}/mobile/error`;
+          const to_uri = urljoin(touri, `?openid=${openid}`);
+          // TODO: 重定性页面
+          this.ctx.redirect(to_uri);
+        }
+      } else if (type === '1') {
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&uid=${uid}&type=${type}&qrcode=${qrcode}`);
+        // TODO: 重定性页面
+        console.log('1111---?' + to_uri);
+        this.ctx.redirect(to_uri);
+      }
+    }
+
+  }
+
+  // GET 用户授权内部测试接口
+  async authTest() {
+    const { redirect_uri, type, uid, qrcode, openid } = this.ctx.query;
+    this.ctx.logger.debug(`[auth-test] reditect_uri - ${redirect_uri}, openid - ${openid}`);
+    assert(redirect_uri, '回调地址不能为空');
+    assert(openid, 'openid不能为空');
+    if (openid) {
+      console.log('redirect_uri-->' + redirect_uri);
+      const user = await this.ctx.service.user.findByOpenid(openid);
+      if (type === '0') {
+        // 通过openid取得用户信息
+        if (user) {
+          const token = await this.ctx.service.login.createJwt(user);
+          const to_uri = urljoin(redirect_uri, `?token=${token}`);
+          // TODO: 重定性页面
+          console.log('to_uri000-->' + to_uri);
+          this.ctx.redirect(to_uri);
+
+        } else {
+          console.log('rrr0000--->' + redirect_uri);
+          const touri = `${this.app.config.baseUrl}/mobile/error`;
+          const to_uri = urljoin(touri, `?openid=${openid}`);
+          // TODO: 重定性页面
+          this.ctx.redirect(to_uri);
+        }
+      } else if (type === '1') {
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&uid=${uid}&type=${type}&qrcode=${qrcode}`);
+        // TODO: 重定性页面
+        console.log('1111---?' + to_uri);
+        this.ctx.redirect(to_uri);
+      }
+    }
+
+  }
+}
+
+module.exports = WeixinController;

+ 5 - 0
app/router.js

@@ -30,4 +30,9 @@ module.exports = app => {
   router.post('/api/auth/token', controller.login.token);
   // 用户退出登录
   router.post('/api/auth/logout', controller.login.destroy);
+
+  // 微信端访问地址
+  router.get('/api/auth/wxchat', controller.weixin.auth); // 微信登录
+  // 微信端访问地址
+  router.get('/api/auth/wxchattest', controller.weixin.authTest); // 微信登录测试
 };

+ 226 - 0
app/service/weixin.js

@@ -0,0 +1,226 @@
+'use strict';
+
+const assert = require('assert');
+const uuid = require('uuid');
+const _ = require('lodash');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const jwt = require('jsonwebtoken');
+const { AxiosService } = require('naf-framework-mongoose/lib/service');
+
+class WeixinAuthService extends AxiosService {
+  constructor(ctx) {
+    super(ctx, {}, _.get(ctx.app.config, 'wxapi'));
+  }
+
+  // 通过认证码获得用户信息
+  async fetch(code) {
+    // TODO:参数检查和默认参数处理
+    assert(code);
+    const { wxapi } = this.app.config;
+    let res = await this.httpGet('/api/fetch', { code });
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch open by code fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信认证信息失败');
+    }
+    const { openid } = res;
+
+    // TODO: 获得用户信息
+    res = await this.httpGet('/api.weixin.qq.com/cgi-bin/user/info?lang=zh_CN', { appid: wxapi.appid, openid });
+    // console.debug('res: ', res);
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
+    }
+    return res;
+  }
+
+  // 通过openid获得用户信息
+  async fetchUnionID(openid) {
+    // TODO:参数检查和默认参数处理
+    assert(openid);
+    const appid = 'wxdf3ed83c095be97a';
+    const grant_type = 'client_credential';
+    const secret = '748df7c2a75077a79ae0c971b1638244';
+    // TODO: 获得用户信息
+    const url = 'http://wx.cc-lotus.info/api.weixin.qq.com/cgi-bin/token?appid=' + appid + '&grant_type=' + grant_type + '&secret=' + secret;
+    const res = await this.ctx.curl(url, {
+      method: 'get',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+    });
+    // console.debug('res: ', res);
+    if (res.errcode && res.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
+    }
+    const token = res.access_token;
+    console.log(token);
+    const urlun = 'https://api.weixin.qq.com/cgi-bin/user/info?access_token=' + token + '&openid=' + openid + '&lang=zh_CN';
+    const result = await this.ctx.curl(urlun, {
+      method: 'get',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+    });
+    // console.debug('res: ', res);
+    if (result.errcode && result.errcode !== 0) {
+      this.ctx.logger.error(`[WeixinAuthService] fetch userinfo by openid fail, errcode: ${res.errcode}, errmsg: ${res.errmsg}`);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '获得微信用户信息失败');
+    }
+    return result;
+  }
+
+  async createJwt({ openid, nickname, subscribe }) {
+    const { secret, expiresIn = '1d', issuer = 'weixin' } = this.config.jwt;
+    const subject = openid;
+    const userinfo = { nickname, subscribe };
+    const token = await jwt.sign(userinfo, secret, { expiresIn, issuer, subject });
+    return token;
+  }
+  /**
+   * 创建二维码
+   * 随机生成二维码,并保存在Redis中,状态初始为pending
+   * 状态描述:
+   * pending - 等待扫码
+   * consumed - 使用二维码登录完成
+   * scand:token - Jwt登录凭证
+   */
+  async createQrcode() {
+    const qrcode = uuid();
+    const key = `visit:qrcode:group:${qrcode}`;
+    await this.app.redis.set(key, 'pending', 'EX', 600);
+    return qrcode;
+  }
+
+  /**
+   * 创建二维码
+   * 生成群二维码
+   * 状态描述:
+   * pending - 等待扫码
+   * consumed - 使用二维码登录完成
+   * scand:token - Jwt登录凭证
+   */
+  async createQrcodeGroup({ groupid }) {
+    const { authUrl = this.ctx.path } = this.app.config;
+    let backUrl;
+    if (authUrl.startsWith('http')) {
+      backUrl = encodeURI(`${authUrl}?state=${groupid}`);
+    } else {
+      backUrl = encodeURI(`${this.ctx.protocol}://${this.ctx.host}${authUrl}?state=${groupid}`);
+    }
+    console.log(backUrl);
+    return backUrl;
+  }
+
+  /**
+   * 扫码登录确认
+   */
+  async scanQrcode({ qrcode, token }) {
+    assert(qrcode, 'qrcode不能为空');
+    assert(token, 'token不能为空');
+    const key = `smart:qrcode:login:${qrcode}`;
+    const status = await this.app.redis.get(key);
+    if (!status) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    if (status !== 'pending') {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
+    }
+
+    // 验证Token
+    const { secret } = this.config.jwt;
+    const decoded = jwt.verify(token, secret, { issuer: 'weixin' });
+    this.ctx.logger.debug(`[weixin] qrcode login - ${decoded}`);
+
+    // TODO: 修改二维码状态,登录凭证保存到redis
+    await this.app.redis.set(key, `scaned:${token}`, 'EX', 600);
+
+    // TODO: 发布扫码成功消息
+    const { mq } = this.ctx;
+    const ex = 'qrcode.login';
+    if (mq) {
+      await mq.topic(ex, qrcode, 'scaned', { durable: true });
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+
+  // 使用二维码换取登录凭证
+  async qrcodeLogin(qrcode) {
+    assert(qrcode, 'qrcode不能为空');
+    const key = `smart:qrcode:login:${qrcode}`;
+    const val = await this.app.redis.get(key);
+    if (!val) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    const [ status, token ] = val.split(':', 2);
+    if (status !== 'scaned' || !token) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
+    }
+
+    // TODO: 修改二维码状态
+    await this.app.redis.set(key, 'consumed', 'EX', 600);
+
+    return { token };
+  }
+
+  // 检查二维码状态
+  async checkQrcode(qrcode) {
+    assert(qrcode, 'qrcode不能为空');
+    const key = `smart:qrcode:login:${qrcode}`;
+    const val = await this.app.redis.get(key);
+    if (!val) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
+    }
+    const [ status ] = val.split(':', 2);
+    return { status };
+  }
+
+  // 发送微信模板消息
+  async sendTemplateMsg(templateid, openid, first, keyword1, keyword2, remark, tourl) {
+    const url = this.ctx.app.config.sendDirMq + this.ctx.app.config.appid;
+    let _url = '';
+    if (tourl) {
+      _url = this.ctx.app.config.baseUrl + '/api/auth?state=1&redirect_uri=' + this.ctx.app.config.baseUrl + '/classinfo/&type=template&objid=' + tourl;
+    }
+    const requestData = { // 发送模板消息的数据
+      touser: openid,
+      template_id: templateid,
+      url: _url,
+      data: {
+        first: {
+          value: first,
+          color: '#173177',
+        },
+        keyword1: {
+          value: keyword1,
+          color: '#1d1d1d',
+        },
+        keyword2: {
+          value: keyword2,
+          color: '#1d1d1d',
+        },
+        remark: {
+          value: remark,
+          color: '#173177',
+        },
+      },
+    };
+    console.log('templateid---' + templateid);
+    console.log('openid---' + openid);
+    console.log('requestData---' + JSON.stringify(requestData));
+    await this.ctx.curl(url, {
+      method: 'post',
+      headers: {
+        'content-type': 'application/json',
+      },
+      dataType: 'json',
+      data: JSON.stringify(requestData),
+    });
+  }
+}
+
+module.exports = WeixinAuthService;