diff --git a/src/WechatSDK.ts b/src/WechatSDK.ts index b652de1..4d7c64b 100644 --- a/src/WechatSDK.ts +++ b/src/WechatSDK.ts @@ -1,19 +1,23 @@ import { WechatMpInstance } from './service/wechat/WechatMp'; +import { WechatPublicInstance } from './service/wechat/WechatPublic'; class WechatSDK { mpMap: Record; publicMap: Record; + webMap: Record; constructor() { this.mpMap = {}; this.publicMap = {}; + this.webMap = {}; } getInstance( appId: string, appSecret: string, - type: 'wechatMp' | 'wechatPublic' + type: 'wechatMp' | 'wechatPublic' | 'web' ) { + // type 支持web网站扫码登录 if (type === 'wechatMp') { if (this.mpMap[appId]) { return this.mpMap[appId]; @@ -23,6 +27,15 @@ class WechatSDK { [appId]: instance, }); return instance; + } else if (type === 'wechatPublic') { + if (this.publicMap[appId]) { + return this.publicMap[appId]; + } + const instance = new WechatPublicInstance(appId, appSecret); + Object.assign(this.publicMap, { + [appId]: instance, + }); + return instance; } else { throw new Error('public not implemented'); } diff --git a/src/service/wechat/WechatMp.ts b/src/service/wechat/WechatMp.ts index 9112759..8606001 100644 --- a/src/service/wechat/WechatMp.ts +++ b/src/service/wechat/WechatMp.ts @@ -6,7 +6,7 @@ export class WechatMpInstance { appSecret: string; accessToken?: string; - refreshAcessTokenHandler?: any; + refreshAccessTokenHandler?: any; constructor(appId: string, appSecret: string) { this.appId = appId; @@ -61,7 +61,7 @@ export class WechatMpInstance { const { access_token, expires_in } = result; this.accessToken = access_token; // 生成下次刷新的定时器 - this.refreshAcessTokenHandler = setTimeout(() => { + this.refreshAccessTokenHandler = setTimeout(() => { this.refreshAccessToken(); }, (expires_in - 10) * 1000); } diff --git a/src/service/wechat/WechatPublic.ts b/src/service/wechat/WechatPublic.ts new file mode 100644 index 0000000..3a2ec5e --- /dev/null +++ b/src/service/wechat/WechatPublic.ts @@ -0,0 +1,90 @@ +import crypto from 'crypto'; +import { Buffer } from 'buffer'; + +export class WechatPublicInstance { + appId: string; + appSecret: string; + + accessToken?: string; + refreshAccessTokenHandler?: any; + + constructor(appId: string, appSecret: string) { + this.appId = appId; + this.appSecret = appSecret; + + this.refreshAccessToken(); + } + + private async access(url: string, init?: RequestInit) { + const response = await fetch(url, init); + + const { headers, status } = response; + if (![200, 201].includes(status)) { + throw new Error(`微信服务器返回不正确应答:${status}`); + } + const contentType = (headers as any)['Content-Type'] || headers.get('Content-Type')!; + if (contentType.includes('application/json')) { + const json = await response.json(); + if (typeof json.errcode === 'number' && json.errcode !== 0) { + throw new Error(`调用微信接口返回出错,code是${json.errcode},信息是${json.errmsg}`); + } + return json; + } + if ( + contentType.includes('text') + || contentType.includes('xml') + || contentType.includes('html') + ) { + const data = await response.text(); + return data; + } + if (contentType.includes('application/octet-stream')) { + return await response.arrayBuffer(); + } + + return response; + } + + async code2Session(code: string) { + const result = await this.access(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`); + const { session_key, openid, unionid } = JSON.parse(result); // 这里微信返回的数据竟然是text/plain + + return { + sessionKey: session_key as string, + openId: openid as string, + unionId: unionid as string, + }; + } + + private async refreshAccessToken() { + const result = await this.access(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`); + const { access_token, expires_in } = result; + this.accessToken = access_token; + // 生成下次刷新的定时器 + this.refreshAccessTokenHandler = setTimeout(() => { + this.refreshAccessToken(); + }, (expires_in - 10) * 1000); + } + + + decryptData(sessionKey: string, encryptedData: string, iv: string, signature: string) { + const skBuf = Buffer.from(sessionKey, 'base64'); + // const edBuf = Buffer.from(encryptedData, 'base64'); + const ivBuf = Buffer.from(iv, 'base64'); + + + const decipher = crypto.createDecipheriv('aes-128-cbc', skBuf, ivBuf); + // 设置自动 padding 为 true,删除填充补位 + decipher.setAutoPadding(true); + let decoded = decipher.update(encryptedData, 'base64', 'utf8'); + decoded += decipher.final('utf8'); + + const data = JSON.parse(decoded); + + if (data.watermark.appid !== this.appId) { + throw new Error('Illegal Buffer'); + } + + return data; + } +}; \ No newline at end of file