lrf402788946 hace 4 años
padre
commit
8017cdd87f
Se han modificado 6 ficheros con 261 adiciones y 3 borrados
  1. 2 0
      app/controller/.service.js
  2. 25 0
      app/controller/weixin.js
  3. 4 1
      app/router.js
  4. 222 0
      app/service/weixin.js
  5. 5 1
      config/config.default.js
  6. 3 1
      package.json

+ 2 - 0
app/controller/.service.js

@@ -41,6 +41,8 @@ module.exports = {
     parameters: {
     parameters: {
       query: {
       query: {
         user_id: "user_id",
         user_id: "user_id",
+        type: "type",
+        title: "title",
         "create_time@start": "create_time@start",
         "create_time@start": "create_time@start",
         "create_time@end": "create_time@end",
         "create_time@end": "create_time@end",
       },
       },

+ 25 - 0
app/controller/weixin.js

@@ -0,0 +1,25 @@
+'use strict';
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+class WeixinController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.weixin;
+  }
+  async auth() {
+    await this.service.auth(this.ctx.query);
+  }
+  async authBack() {
+    await this.service.authBack(this.ctx.query);
+  }
+  async wxUser() {
+    const data = await this.service.wxUser(this.ctx.query);
+    this.ctx.ok({ data });
+  }
+  async jsapiAuth() {
+    const res = await this.service.jsapiAuth(this.ctx.request.body);
+    this.ctx.ok({ data: res });
+  }
+}
+module.exports = CrudController(WeixinController, {});

+ 4 - 1
app/router.js

@@ -8,7 +8,10 @@ module.exports = app => {
   const profix = '/api/article/';
   const profix = '/api/article/';
   router.get('/', controller.home.index);
   router.get('/', controller.home.index);
   router.post('user', `${profix}login`, controller.user.login);
   router.post('user', `${profix}login`, controller.user.login);
-  router.post('user', `${profix}init`, controller.user.init);
+  // 微信认证,生成包含微信用户信息的token写入cookie
+  router.get(`${profix}auth`, controller.weixin.auth);
+  router.get(`${profix}authBack`, controller.weixin.authBack);
+  router.get(`${profix}wxUser`, controller.weixin.wxUser);
   require('./router/refute')(app); // 文章/辟谣
   require('./router/refute')(app); // 文章/辟谣
   require('./router/topic')(app); // 社区话题
   require('./router/topic')(app); // 社区话题
   require('./router/service')(app); // 咨询服务
   require('./router/service')(app); // 咨询服务

+ 222 - 0
app/service/weixin.js

@@ -0,0 +1,222 @@
+'use strict';
+
+const assert = require('assert');
+const uuid = require('uuid');
+const random = require('string-random');
+const crypto = require('crypto');
+const urljoin = require('url-join');
+const _ = require('lodash');
+const moment = require('moment');
+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'));
+    this.prefix = 'auth:';
+    this.jsapiKey = 'jsapi_ticket';
+    this.access_tokenKey = 'access_token';
+    this.wxInfo = ctx.app.config.wxapi;
+    this.authBackUrl = `${ctx.app.config.baseUrl}/api/article/authBack`;
+  }
+  /**
+   * 网页授权
+   * @param {Object} query 参数
+   */
+  async auth(query) {
+    const { redirect_uri, ...others } = query;
+    const { appid, baseUrl } = this.wxInfo;
+    if (!appid) {
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '缺少公众号设置');
+    }
+    // 用于redis
+    const state = uuid.v4();
+    const key = `${this.prefix}${state}`;
+    const val = JSON.stringify({ ...others, redirect_uri });
+    await this.app.redis.set(key, val, 'EX', 600);
+    // const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${this.authBackUrl}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
+    let backUrl;
+    if (this.authBackUrl.startsWith('http')) {
+      backUrl = encodeURI(`${this.authBackUrl}?state=${state}`);
+    } else {
+      backUrl = encodeURI(`${this.ctx.protocol}://${this.ctx.host}${this.authBackUrl}?state=${state}`);
+    }
+    const to_uri = `${baseUrl}/api/auth?appid=${appid}&response_type=code&redirect_uri=${backUrl}#wechat`;
+    this.ctx.redirect(to_uri);
+  }
+
+  /**
+   * 网页授权回调,获取openid
+   * @param {Object} query 参数
+   */
+  async authBack(query) {
+    const { code, state } = query;
+    if (!code) throw new BusinessError(ErrorCode.SERVICE_FAULT, '授权未成功');
+    const req = await this.httpGet('/api/fetch', { code });
+    if (req.errcode && req.errcode !== 0) throw new BusinessError(ErrorCode.SERVICE_FAULT, 'openid获取失败');
+    const openid = _.get(req, 'openid');
+    if (!openid) {
+      this.ctx.logger.error(JSON.stringify(req.data));
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '未获取到openid');
+    }
+    // 验证获取openid结束,接下来应该返回前端
+    const key = `${this.prefix}${state}`;
+    let fqueries = await this.app.redis.get(key);
+    if (fqueries)fqueries = JSON.parse(fqueries);
+    let { redirect_uri } = fqueries;
+    const queryStr = `?openid=${openid}`;
+    redirect_uri = urljoin(redirect_uri, queryStr);
+    this.ctx.redirect(redirect_uri);
+  }
+  /**
+   * 换微信用户信息
+   * @param {Object} {openid}
+   */
+  async wxUser({ openid }) {
+    const { appid } = this.wxInfo;
+    const res = await this.httpGet('/api.weixin.qq.com/cgi-bin/user/info?lang=zh_CN', { appid, openid });
+    const object = _.pick(res, [ 'nickname', 'headimgurl', 'openid' ]); // 昵称,头像,openid
+    return object;
+  }
+  /**
+   * JsApi验证
+   * @param {Object} query 参数
+   */
+  async jsapiAuth(query) {
+    let { url } = query;
+    url = decodeURIComponent(url);
+    let jsapi_ticket = await this.app.redis.get(this.jsapiKey);
+    const { appid, appSecret } = this.wxInfo;
+    if (!jsapi_ticket) {
+      // 1,获取access_token
+      const atUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appSecret}`;
+      const req = await this.ctx.curl(atUrl, { method: 'GET', dataType: 'json' });
+      if (req.status !== 200) throw new BusinessError(ErrorCode.SERVICE_FAULT, 'access_token获取失败');
+      const access_token = _.get(req, 'data.access_token');
+      // 2,获取jsapi_token
+      const jtUrl = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${access_token}&type=jsapi`;
+      const jtReq = await this.ctx.curl(jtUrl, { method: 'GET', dataType: 'json' });
+      if (jtReq.status !== 200) throw new BusinessError(ErrorCode.SERVICE_FAULT, 'jsapi_ticket获取失败');
+      jsapi_ticket = _.get(jtReq, 'data.ticket');
+      // 实际过期时间是7200s(2h),系统默认设置6000s
+      const expiresIn = _.get(jtReq, 'data.expires_in', 6000);
+      // 缓存jsapi_ticket,重复使用
+      await this.app.redis.set(this.jsapiKey, jsapi_ticket, 'EX', expiresIn);
+    }
+    const noncestr = random(16).toLowerCase();
+    const timestamp = moment().unix();
+    const signStr = `jsapi_ticket=${jsapi_ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`;
+    const sign = crypto.createHash('sha1').update(signStr).digest('hex');
+    return { jsapi_ticket, noncestr, timestamp, sign, appid, url };
+  }
+
+
+  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 };
+  }
+}
+
+module.exports = WeixinAuthService;

+ 5 - 1
config/config.default.js

@@ -57,7 +57,11 @@ module.exports = appInfo => {
       port: 10010,
       port: 10010,
     },
     },
   };
   };
-
+  config.wxapi = {
+    appid: 'wxdf3ed83c095be97a', // 微信公众号APPID
+    appSecret: 'd85dbe075c090cb12ce416bbda8e698c',
+    baseUrl: 'http://wx.cc-lotus.info', // 微信网关地址
+  };
   return {
   return {
     ...config,
     ...config,
     ...userConfig,
     ...userConfig,

+ 3 - 1
package.json

@@ -12,7 +12,9 @@
     "jsonwebtoken": "^8.5.1",
     "jsonwebtoken": "^8.5.1",
     "lodash": "^4.17.15",
     "lodash": "^4.17.15",
     "moment": "^2.29.1",
     "moment": "^2.29.1",
-    "naf-framework-mongoose": "^0.6.11"
+    "naf-framework-mongoose": "^0.6.11",
+    "string-random": "^0.1.3",
+    "url-join": "^4.0.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "autod": "^3.0.1",
     "autod": "^3.0.1",