weixin.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. 'use strict';
  2. const assert = require('assert');
  3. const uuid = require('uuid');
  4. const _ = require('lodash');
  5. const { BusinessError, ErrorCode } = require('naf-core').Error;
  6. const jwt = require('jsonwebtoken');
  7. const { AxiosService } = require('naf-framework-mongoose/lib/service');
  8. class WeixinAuthService extends AxiosService {
  9. constructor(ctx) {
  10. super(ctx, {}, _.get(ctx.app.config, 'wxapi'));
  11. this.prefix = 'visit-auth:';
  12. this.wxInfo = ctx.app.config.wxapi;
  13. }
  14. /**
  15. * 网页授权
  16. * @param {Object} query 参数
  17. */
  18. async auth(query) {
  19. let { redirect_uri, ...others } = query;
  20. if (!redirect_uri) {
  21. redirect_uri = `${this.ctx.app.config.baseUrl}/api/visit/authBack`;
  22. }
  23. const { appid } = this.wxInfo;
  24. if (!appid) {
  25. throw new BusinessError(ErrorCode.SERVICE_FAULT, '缺少公众号设置');
  26. }
  27. // 用于redis
  28. const state = uuid.v4();
  29. const key = `${this.prefix}${state}`;
  30. const val = JSON.stringify({ ...others, redirect_uri });
  31. await this.app.redis.set(key, val, 'EX', 600);
  32. const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_base&state=${state}#wechat_redirect`;
  33. this.ctx.redirect(url);
  34. }
  35. /**
  36. * 网页授权回调,获取openid
  37. * @param {Object} query 参数
  38. */
  39. async authBack(query) {
  40. const { code, state } = query;
  41. if (!code) throw new BusinessError(ErrorCode.SERVICE_FAULT, '授权未成功');
  42. const { appid, appSecret } = this.wxInfo;
  43. const url = ` https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appSecret}&code=${code}&grant_type=authorization_code`;
  44. const req = this.ctx.curl(url, {
  45. method: 'GET',
  46. });
  47. if (req.status !== 200) throw new BusinessError(ErrorCode.SERVICE_FAULT, 'access_token获取失败');
  48. const openid = _.get(req, 'data.openid');
  49. if (!openid) {
  50. this.ctx.logger.error(JSON.stringify(req.data));
  51. throw new BusinessError(ErrorCode.SERVICE_FAULT, '未获取到openid');
  52. }
  53. // 验证获取openid结束,接下来应该返回前端
  54. const key = `${this.prefix}${state}`;
  55. let fqueries = await this.app.redis.get(key);
  56. if (fqueries)fqueries = JSON.parse(fqueries);
  57. const { redirect_uri } = fqueries;
  58. this.ctx.redirect(redirect_uri);
  59. }
  60. async createJwt({ openid, nickname, subscribe }) {
  61. const { secret, expiresIn = '1d', issuer = 'weixin' } = this.config.jwt;
  62. const subject = openid;
  63. const userinfo = { nickname, subscribe };
  64. const token = await jwt.sign(userinfo, secret, { expiresIn, issuer, subject });
  65. return token;
  66. }
  67. /**
  68. * 创建二维码
  69. * 随机生成二维码,并保存在Redis中,状态初始为pending
  70. * 状态描述:
  71. * pending - 等待扫码
  72. * consumed - 使用二维码登录完成
  73. * scand:token - Jwt登录凭证
  74. */
  75. async createQrcode() {
  76. const qrcode = uuid();
  77. const key = `visit:qrcode:group:${qrcode}`;
  78. await this.app.redis.set(key, 'pending', 'EX', 600);
  79. return qrcode;
  80. }
  81. /**
  82. * 创建二维码
  83. * 生成群二维码
  84. * 状态描述:
  85. * pending - 等待扫码
  86. * consumed - 使用二维码登录完成
  87. * scand:token - Jwt登录凭证
  88. */
  89. async createQrcodeGroup({ groupid }) {
  90. const { authUrl = this.ctx.path } = this.app.config;
  91. let backUrl;
  92. if (authUrl.startsWith('http')) {
  93. backUrl = encodeURI(`${authUrl}?state=${groupid}`);
  94. } else {
  95. backUrl = encodeURI(`${this.ctx.protocol}://${this.ctx.host}${authUrl}?state=${groupid}`);
  96. }
  97. console.log(backUrl);
  98. return backUrl;
  99. }
  100. /**
  101. * 扫码登录确认
  102. */
  103. async scanQrcode({ qrcode, token }) {
  104. assert(qrcode, 'qrcode不能为空');
  105. assert(token, 'token不能为空');
  106. const key = `smart:qrcode:login:${qrcode}`;
  107. const status = await this.app.redis.get(key);
  108. if (!status) {
  109. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
  110. }
  111. if (status !== 'pending') {
  112. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
  113. }
  114. // 验证Token
  115. const { secret } = this.config.jwt;
  116. const decoded = jwt.verify(token, secret, { issuer: 'weixin' });
  117. this.ctx.logger.debug(`[weixin] qrcode login - ${decoded}`);
  118. // TODO: 修改二维码状态,登录凭证保存到redis
  119. await this.app.redis.set(key, `scaned:${token}`, 'EX', 600);
  120. // TODO: 发布扫码成功消息
  121. const { mq } = this.ctx;
  122. const ex = 'qrcode.login';
  123. if (mq) {
  124. await mq.topic(ex, qrcode, 'scaned', { durable: true });
  125. } else {
  126. this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
  127. }
  128. }
  129. // 使用二维码换取登录凭证
  130. async qrcodeLogin(qrcode) {
  131. assert(qrcode, 'qrcode不能为空');
  132. const key = `smart:qrcode:login:${qrcode}`;
  133. const val = await this.app.redis.get(key);
  134. if (!val) {
  135. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
  136. }
  137. const [ status, token ] = val.split(':', 2);
  138. if (status !== 'scaned' || !token) {
  139. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码状态无效');
  140. }
  141. // TODO: 修改二维码状态
  142. await this.app.redis.set(key, 'consumed', 'EX', 600);
  143. return { token };
  144. }
  145. // 检查二维码状态
  146. async checkQrcode(qrcode) {
  147. assert(qrcode, 'qrcode不能为空');
  148. const key = `smart:qrcode:login:${qrcode}`;
  149. const val = await this.app.redis.get(key);
  150. if (!val) {
  151. throw new BusinessError(ErrorCode.SERVICE_FAULT, '二维码已过期');
  152. }
  153. const [ status ] = val.split(':', 2);
  154. return { status };
  155. }
  156. }
  157. module.exports = WeixinAuthService;