Open wuyuedefeng opened 4 years ago
const crypto = require('crypto')
const axios = require('axios')
const Cache = require('./cache')
const URLs = {
tokenBaseUrl: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential',
ticketBaseUrl: 'https://api.weixin.qq.com/cgi-bin/ticket/getticket'
}
// params = { appId, appSecret }
async function getAccessToken (params) {
const res = await axios.get(`${URLs.tokenBaseUrl}&appid=${params.appId}&secret=${params.appSecret}`)
if (res.data['access_token']) {
return res.data['access_token']
}
throw new Error(res.data['errmsg'] || 'request accessToken error')
}
async function getTicket (accessToken) {
const res = await axios.get(`${URLs.ticketBaseUrl}?access_token=${accessToken}&type=jsapi`)
if (res.data['ticket']) {
return res.data['ticket']
}
throw new Error(res.data['errmsg'] || 'request ticket error')
}
function createSha1SignatureConfig (ticket, locationHref) {
const data = {
nonceStr: Math.random().toString(36).substr(2, 15),
timestamp: parseInt(new Date().getTime() / 1000)
}
const rawData = raw(Object.assign({ url: locationHref, jsapi_ticket: ticket }, data))
const sha1 = crypto.createHash('sha1')
sha1.update(rawData)
data['signature'] = sha1.digest('hex')
return data
function raw (args) {
const keys = Object.keys(args).sort()
const newArgs = []
keys.forEach((key) => {
newArgs.push([key.toLowerCase(), args[key]].join('='))
})
return newArgs.join('&')
}
}
// params = { appId, appSecret, locationHref }
async function getJSSDKSignatureConfig (params) {
const accessToken = await Cache.fetchAsync(`wechat_jssdk_accessToken_${params.appId}`, 7000, async () => {
return await getAccessToken(params) // 微信7200秒过期,个人缓存少于该值
})
const ticket = await Cache.fetchAsync(`wechat_jssdk_ticket_${params.appId}`, 7000, async () => {
return await getTicket(accessToken) // 微信7200秒过期,个人缓存少于该值
})
// 这里只缓存ticket也可,因为只有ticket失效才会重新拿accessToken换ticket,要注意其他场景是否也会用到accessToken
// const ticket = await Cache.fetchAsync(`wechat_jssdk_ticket_${params.appId}`, 7000, async () => {
// const accessToken = await getAccessToken(params)
// return await getTicket(accessToken) // 微信7200秒过期,个人缓存少于该值
// })
const config = createSha1SignatureConfig(ticket, decodeURIComponent(params['locationHref'].split('#')[0]))
// config = { nonceStr, timestamp, signature }
return config
}
module.exports = getJSSDKSignatureConfig
const CONFIG = require('./config')
const Koa = require('koa');
const router = require('koa-router')()
const logger = require('koa-logger')
const app = new Koa()
app.use(logger())
const getJSSDKSignatureConfig = require('./libs/getJSSDKSignatureConfig')
app.use(async (ctx, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
ctx.set('X-Response-Time', `${ms}ms`);
})
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.throw(422, err.message)
}
})
router.get('/wechatSignature', async (ctx)=>{
const appSecret = CONFIG.appSecrets[ctx.query.appId]
if (!ctx.query.appId || !appSecret) return ctx.throw(422, 'appId invalid')
if (!ctx.query.locationHref) return ctx.throw(422, 'locationHref required')
const signatureConfig = await getJSSDKSignatureConfig({...ctx.query, appSecret})
ctx.body = signatureConfig
})
app.use(router.routes())
app.use(router.allowedMethods())
const port = 5000
app.listen(port, () => {
console.log(`app start on ${ port }`)
})
config.js
{
"appSecrets": {
"wxcbaac81f3xxxx": "625978a0d1f3f9391136xxxxxxxxx"
}
}
5分钟
后过期,在多实例
集群的情况下,请使用redis缓存数据// https://github.com/luin/ioredis
const Redis = require('ioredis')
const redisClient = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
keyPrefix: "wechat:",
reconnectOnError(err) {
console.error(err)
return true
}
})
class RedisCache {
static async getWithInitValueWithLock(key, initValue, lockExpiresSeconds = 5) {
const exists = await redisClient.exists(key)
let value = null
if (exists) {
value = await redisClient.get(key)
} else {
if (typeof initValue === 'function') {
// 获取锁, set(key, value, 'NX', 'EX', seconds) <=> setnx(key, value) && expire(key, seconds)
const lockKey = `${key}:with_lock`
if (await redisClient.set(lockKey, 1, 'NX', 'EX', lockExpiresSeconds)) { // 已获得锁
value = await initValue().finally(async () => {
await redisClient.del(lockKey)
})
} else { // 未获取到锁
// 等待500ms 重新获取或者设置值
await new Promise(resolve => setTimeout(() => { resolve() }, 500))
return await this.getWithInitValueWithLock(...arguments)
}
} else {
value = initValue
}
}
return value
}
// expiresIn: second
static async fetchAsync (key, expiresIn, blockFnAsync) {
return await this.getWithInitValueWithLock(key, async () => {
// 异步获取值捕获异常
const value = blockFnAsync && (await blockFnAsync()) || null
redisClient.set(key, value, 'EX', expiresIn)
return value
})
}
static async clearAsync(key) {
if (key) {
await redisClient.del(key)
}
}
}
module.exports = RedisCache
公众号配置
appId
,appSecret
并配置IP地址白名单(白名单的ip才能调用成功拿到access_token)JS接口安全域名
(签名的api地址域名) 有特殊要求,比如已备案,下载文件上传到域名相应静态位置,并可访问签名算法(需要缓存,请自行添加)
cache 服务 (个人实现, 也可以借助
redis
)