From a9836ad9371d5fdfa807707b6d776dfe7dd8c55e Mon Sep 17 00:00:00 2001 From: LiuSongJiang Date: Mon, 8 Apr 2019 22:59:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E9=99=86=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.dev.json | 2 +- package-lock.json | 11 +---- package.json | 1 + src/api/hapi/controller.ts | 0 src/api/hapi/routes.ts | 19 --------- src/api/hapi/validator.ts | 0 src/api/index.ts | 35 ++++++++-------- src/api/wxLogin/controller.ts | 79 +++++++++++++++++++++++++++++++++++ src/api/wxLogin/interfaces.ts | 15 +++++++ src/api/wxLogin/routes.ts | 33 +++++++++++++++ src/api/wxLogin/validator.ts | 15 +++++++ src/db/models/users.model.ts | 9 ++++ src/interfaces/config.ts | 4 ++ src/server.ts | 4 +- src/utils/decrypted-data.ts | 52 +++++++++++++++++++++++ src/utils/router-helper.ts | 15 +++++++ 16 files changed, 246 insertions(+), 48 deletions(-) delete mode 100644 src/api/hapi/controller.ts delete mode 100644 src/api/hapi/routes.ts delete mode 100644 src/api/hapi/validator.ts create mode 100644 src/api/wxLogin/controller.ts create mode 100644 src/api/wxLogin/interfaces.ts create mode 100644 src/api/wxLogin/routes.ts create mode 100644 src/api/wxLogin/validator.ts create mode 100644 src/utils/decrypted-data.ts create mode 100644 src/utils/router-helper.ts diff --git a/config.dev.json b/config.dev.json index b3fcfc2..b8305b8 100644 --- a/config.dev.json +++ b/config.dev.json @@ -7,7 +7,7 @@ "wxSecret": "a413d6545e63a35fc2a0336d66534b89" }, "server": { - "port": 5000, + "port": 3000, "host": "localhost", "jwtSecret": "random-secret-password", "jwtExpiration": "1h", diff --git a/package-lock.json b/package-lock.json index 29a1706..b20db86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -468,17 +468,10 @@ }, "boom": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", - "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", + "resolved": "http://npm.dev.56qq.com/boom/-/boom-7.3.0.tgz", + "integrity": "sha1-czptlW0zsLGZnaP+bBKZaVDQF7k=", "requires": { "hoek": "6.x.x" - }, - "dependencies": { - "hoek": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", - "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==" - } } }, "bounce": { diff --git a/package.json b/package.json index 942c892..12e3e44 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "ISC", "dependencies": { "axios": "^0.18.0", + "boom": "^7.3.0", "hapi": "^18.0.0", "hapi-auth-jwt2": "^8.3.0", "hapi-pagination": "^2.0.0", diff --git a/src/api/hapi/controller.ts b/src/api/hapi/controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/hapi/routes.ts b/src/api/hapi/routes.ts deleted file mode 100644 index c24c2da..0000000 --- a/src/api/hapi/routes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Users from "../../db/models/users.model"; - -export default [ - { - method: "GET", - path: "/hello", - handler: async (request, h) => { - const results = await Users.findOrCreate({ - where: { open_id: 'ojMHa1XuP_ADU7rqaB9eew2ZFmF4' }, - }); - return results; - }, - config: { - auth: false, // 不需要用户验证 - tags: ["api", "GROUP_NAME"], - description: "创建订单" - } - } -]; diff --git a/src/api/hapi/validator.ts b/src/api/hapi/validator.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/index.ts b/src/api/index.ts index 9a7e94a..65b3778 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,25 +1,26 @@ -import * as requireAll from 'require-all'; +import * as requireAll from "require-all"; +import * as Hapi from "hapi"; // 自动引入routes.js const allApi: object = requireAll({ dirname: __dirname, - filter: /routes.js$/, -}) + filter: /routes.js$/ +}); -const routes = []; +export default (server: Hapi.Server): Hapi.Server => { + const routes = []; -function reduceRoute(routeNote: object) { - Object.values(routeNote).forEach((route = {}) => { - const module = route['routes.js'] || []; - if (module) { - const [moduleDefault] = module.default - routes.push(moduleDefault); - } - }); -} + function reduceRoute(routeNote: object) { + Object.values(routeNote).forEach((route = {}) => { + const module = route["routes.js"]; + if (module) { + const [moduleDefault] = module.default(server); + routes.push(moduleDefault); + } + }); + } -reduceRoute(allApi) - - -export default routes; + reduceRoute(allApi); + return server.route(routes); +}; diff --git a/src/api/wxLogin/controller.ts b/src/api/wxLogin/controller.ts new file mode 100644 index 0000000..5d14f03 --- /dev/null +++ b/src/api/wxLogin/controller.ts @@ -0,0 +1,79 @@ +import * as Hapi from "hapi"; +import axios from "axios"; +import * as JWT from "jsonwebtoken"; +import { IMixinsServerWechat } from "../../interfaces/config"; +import { IWxLoginRquest, IWxLoginParams } from "./interfaces"; +import decryptData from "../../utils/decrypted-data"; +import Users, { IUsers } from "../../db/models/users.model"; + +export default class WxLoginController { + private config: IMixinsServerWechat; + private model = Users; + private wxSessionUrl = "https://api.weixin.qq.com/sns/jscode2session"; + private grant_type = "authorization_code"; + + constructor(conifg: IMixinsServerWechat) { + this.config = conifg; + } + + public async wxLogin(request: IWxLoginRquest, h: Hapi.ResponseToolkit) { + const appid = this.config.wxAppid; // 小程序 appid + const secret = this.config.wxSecret; // 小程序 appsecret + + const { code, encryptedData, iv } = request.payload; + + const { openid, sessionKey } = await this.getOpid({ + appid, + secret, + js_code: code, + grant_type: this.grant_type + }); + + const user = await this.model.findOrCreate({ + where: { open_id: openid } + }); + + const userInfo = decryptData(encryptedData, iv, sessionKey, appid); + + await this.updateUser( + { + nick_name: userInfo.nickName, + gender: userInfo.gender, + avatar_url: userInfo.avatarUrl, + open_id: openid, + session_key: sessionKey + }, + openid + ); + + const token = { token: this.generateToken(user[0].id) }; + + return token; + } + + public async updateUser(updateData: IUsers, openid: string) { + await this.model.update(updateData, { + where: { open_id: openid } + }); + } + + private generateToken(userId: number) { + const payload = { + id: userId + }; + const jwtSecret = this.config.jwtSecret; + const jwtExpiration = this.config.jwtExpiration; + return JWT.sign(payload, jwtSecret, { expiresIn: jwtExpiration }); + } + + // 通过微信服务器获取opid + private async getOpid(params: IWxLoginParams) { + const response = await axios({ + url: this.wxSessionUrl, + method: "GET", + params + }); + const { openid, session_key: sessionKey } = response.data; + return { openid, sessionKey }; + } +} diff --git a/src/api/wxLogin/interfaces.ts b/src/api/wxLogin/interfaces.ts new file mode 100644 index 0000000..3ca4ae8 --- /dev/null +++ b/src/api/wxLogin/interfaces.ts @@ -0,0 +1,15 @@ +export interface IWxLoginRquest { + payload: { + code: string; + encryptedData: string; + iv: string; + }; +} + +export interface IWxLoginParams { + appid: string; + secret: string; + js_code: string; + grant_type: string; +} + diff --git a/src/api/wxLogin/routes.ts b/src/api/wxLogin/routes.ts new file mode 100644 index 0000000..78b9ce0 --- /dev/null +++ b/src/api/wxLogin/routes.ts @@ -0,0 +1,33 @@ +import * as Hapi from "hapi"; +import { getWeChatConfigs, getServerConfigs } from "../../configurations"; +import { + IMixinsServerWechat, + IWeChatConifg, + IServerConfigurations +} from "../../interfaces/config"; +import WxLoginController from "./controller"; +import { wxLogin } from "./validator"; + +export default (server: Hapi.Server) => { + const weChatConfig: IWeChatConifg = getWeChatConfigs(); + const serverConfig: IServerConfigurations = getServerConfigs(); + const configs: IMixinsServerWechat = { ...weChatConfig, ...serverConfig }; + const loginController = new WxLoginController(configs); + server.bind(loginController); + + return [ + { + method: "POST", + path: "/wxLogin", + handler: loginController.wxLogin, + config: { + auth: false, // 不需要用户验证 + tags: ["api", "GROUP_NAME"], + description: "创建订单", + validate: { + payload: wxLogin + } + } + } + ]; +}; diff --git a/src/api/wxLogin/validator.ts b/src/api/wxLogin/validator.ts new file mode 100644 index 0000000..0c5e7b6 --- /dev/null +++ b/src/api/wxLogin/validator.ts @@ -0,0 +1,15 @@ +import * as Joi from "joi"; + +export const wxLogin = Joi.object().keys({ + code: Joi.string() + .required() + .description("微信用户登录的临时code"), + + encryptedData: Joi.string() + .required() + .description("微信用户信息encryptedData"), + + iv: Joi.string() + .required() + .description("微信用户信息iv") +}); diff --git a/src/db/models/users.model.ts b/src/db/models/users.model.ts index 71dc6a9..a2b337f 100644 --- a/src/db/models/users.model.ts +++ b/src/db/models/users.model.ts @@ -1,5 +1,14 @@ import { Table, Column, Model } from "sequelize-typescript"; +export interface IUsers { + id?: number; + nick_name: string; + avatar_url: string; + gender: number; + open_id: string; + session_key: string; +} + @Table({ tableName: "users", timestamps: false diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index 882d103..dd19f5c 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -12,6 +12,10 @@ export interface IServerConfigurations { routePrefix: string; } +export interface IMixinsServerWechat + extends IWeChatConifg, + IServerConfigurations {} + export interface IDataConfiguration { url: string; } diff --git a/src/server.ts b/src/server.ts index 79253eb..0e5ef08 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,6 @@ import * as Hapi from "hapi"; import { IServerConfigurations } from "./interfaces/config"; -import apis from "./api"; +import registerRoute from "./api"; import registerModels from "./db/models"; export async function init( @@ -43,7 +43,7 @@ export async function init( // 注册model registerModels(); // 注册路由 - server.route(apis); + registerRoute(server); return server; } diff --git a/src/utils/decrypted-data.ts b/src/utils/decrypted-data.ts new file mode 100644 index 0000000..8109224 --- /dev/null +++ b/src/utils/decrypted-data.ts @@ -0,0 +1,52 @@ +// 封装的 decryptData,用于解码小程序的 encryptData +import * as crypto from "crypto"; + +export interface IDecodeUserData { + avatarUrl: string; + gender: number; + nickName: string; + watermark: { + appid: string; + }; +} + +function decryptData( + encryptedData: string, + iv: string, + sessionKey: string, + appid: string +) { + // base64 decode + const encryptedDataNew = Buffer.from(encryptedData, "base64"); + const sessionKeyNew = Buffer.from(sessionKey, "base64"); + const ivNew = Buffer.from(iv, "base64"); + + let decoded = ""; + let decodeData: IDecodeUserData; + try { + // 解密,使用的算法是 aes-128-cbc + const decipher = crypto.createDecipheriv( + "aes-128-cbc", + sessionKeyNew, + ivNew + ); + // 设置自动 padding 为 true,删除填充补位 + decipher.setAutoPadding(true); + decoded = decipher.update(encryptedDataNew, undefined, "utf8"); + decoded += decipher.final("utf8"); + decodeData = JSON.parse(decoded); + // decoded 是解密后的用户信息 + } catch (err) { + throw new Error("Illegal Buffer" + err); + } + + // 解密后的用户数据中会有一个 watermark 属性,这个属性中包含这个小程序的 appid 和时间戳,下面是校验 appid + if (decodeData.watermark.appid !== appid) { + throw new Error("Illegal Buffer"); + } + + // 返回解密后的用户数据 + return decodeData; +} + +export default decryptData; diff --git a/src/utils/router-helper.ts b/src/utils/router-helper.ts new file mode 100644 index 0000000..ff84128 --- /dev/null +++ b/src/utils/router-helper.ts @@ -0,0 +1,15 @@ +import * as Joi from 'joi'; + +export const paginationDefine = { + limit: Joi.number().integer().min(1).default(10) + .description('每页的条目数'), + page: Joi.number().integer().min(1).default(1) + .description('页码数'), + pagination: Joi.boolean().description('是否开启分页,默认为true'), +} + +export const jwtHeaderDefine = { + headers: Joi.object({ + authorization: Joi.string().required(), + }).unknown(), +}