Selaa lähdekoodia

增加微信登录功能

liuyu 5 vuotta sitten
vanhempi
commit
cef3589e02
5 muutettua tiedostoa jossa 300 lisäystä ja 1 poistoa
  1. 130 0
      app/controller/weixin.js
  2. 7 1
      app/service/school.js
  3. 6 0
      app/service/user.js
  4. 144 0
      app/service/weixin.js
  5. 13 0
      config/config.default.js

+ 130 - 0
app/controller/weixin.js

@@ -0,0 +1,130 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const uuid = require('uuid');
+const urljoin = require('url-join');
+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', groupid, doctorid } = 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, groupid, doctorid });
+    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 } = JSON.parse(val);
+      console.log('redirect_uri-->' + redirect_uri);
+      // 通过openid取得用户信息
+      const user = await this.ctx.service.user.findByOpenid(openid);
+      if (user) {
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}&uid=${user.id}&type=${user.type}`);
+        // TODO: 重定性页面
+        console.log('to_uri222-->' + to_uri);
+        this.ctx.redirect(to_uri);
+      } else {
+        console.log('rrr--->' + redirect_uri);
+        const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+
+        // TODO: 重定性页面
+        this.ctx.redirect(to_uri);
+      }
+    }
+
+  }
+
+  // GET 用户授权内部测试接口
+  async authTest() {
+    const { redirect_uri, type, groupid, doctorid } = this.ctx.query;
+    const openid = '1234567';
+    this.ctx.logger.debug(`[auth-test] reditect_uri - ${redirect_uri}, openid - ${openid}`);
+    assert(redirect_uri, '回调地址不能为空');
+    assert(openid, 'openid不能为空');
+
+    if (type === 'group') {
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}&groupid=${groupid}`);
+      // TODO: 重定性页面
+      console.log('to_uri222-->' + to_uri);
+      this.ctx.redirect(to_uri);
+    } else if (type === 'login') {
+      console.log('to_uri333-->' + redirect_uri);
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+      console.log('to_uri333-->' + to_uri);
+      // TODO: 重定性页面
+      this.ctx.redirect(to_uri);
+    } else if (type === 'doctor') {
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}&doctorid=${doctorid}`);
+      // TODO: 重定性页面
+      this.ctx.redirect(to_uri);
+    } else {
+      console.log('rrr--->' + redirect_uri);
+      const to_uri = urljoin(redirect_uri, `?openid=${openid}`);
+
+      // TODO: 重定性页面
+      this.ctx.redirect(to_uri);
+    }
+  }
+}
+
+module.exports = WeixinController;

+ 7 - 1
app/service/school.js

@@ -12,6 +12,7 @@ class SchoolService extends CrudService {
   constructor(ctx) {
     super(ctx, 'schoolctrl');
     this.model = this.ctx.model.Student;
+    this.umodel = this.ctx.model.User;
   }
 
   async stuimport(data) {
@@ -31,7 +32,12 @@ class SchoolService extends CrudService {
     }
     // 将数据存入数据库中
     for (const stu of studatas) {
-      await this.model.create(stu);
+      const res = await this.model.create(stu);
+      if (res) {
+        const newdata = { name: stu.name, mobile: stu.phone, type: '4', uid: res.id };
+        newdata.passwd = { secret: '12345678' };
+        await this.umodel.create(newdata);
+      }
     }
     return datacheck;
   }

+ 6 - 0
app/service/user.js

@@ -35,6 +35,12 @@ class UserService extends CrudService {
     return res;
   }
 
+  // 通过openid查询用户信息
+  async findByOpenid(openid) {
+    // 通过openid查询用户信息
+    const user = await this.model.findOne({ openid });
+    return user;
+  }
 
 }
 

+ 144 - 0
app/service/weixin.js

@@ -0,0 +1,144 @@
+'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;
+  }
+
+  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;

+ 13 - 0
config/config.default.js

@@ -37,8 +37,21 @@ module.exports = appInfo => {
     },
   };
 
+  config.proxy = true;
+  config.hostHeaders = 'x-forwarded-host';
   // 服务器发布路径
   config.baseUrl = 'http://free.liaoningdoupo.com';
+  // 认证回调地址
+  config.authUrl = '/api/auth';
+  // 回调地址
+  config.redirect_uri = `${config.baseUrl}/weixin`;
+  config.redirect_uri_doctor = `${config.baseUrl}/patient/manage`;
+  config.redirect_uri_patient = `${config.baseUrl}/doctor/manage`;
+  // base路径
+  config.sendDirMq = 'http://wx.cc-lotus.info/api.weixin.qq.com/cgi-bin/message/template/send?appid=';
+  // appID
+  config.appid = 'wxdf3ed83c095be97a';
+  config.REVIEW_TEMPLATE_ID = 'BI4h0AQpdctm74I7-7PyHAspSMX2oJOTJVQsgrppOag';
 
   // config.amqp = {
   //   client: {