Bootstrap
博客内容搜索
Kaysama's Blog

好久以前写过使用微信js-sdk实现自定义分享的功能。这次来填坑,把获取用户信息的功能给实现了。由于上次关于微信的文章比较久远了,这次全都换成了更简洁的koa来实现,基本逻辑不变。

首先后端要定义这样几个接口:

① /authorize,用于获取微信授权,即微信文档中的第一步,获取授权code

② /get_user_openid,接收code参数,作为第一步中的微信重定向回调地址,用来调用微信接口获取用户的openid和授权凭证access_token,以及刷新凭证refresh_token。把openid和两个凭证做好一对一映射

③ /check_user_access_token,检测用户的授权凭证是否失效,接收openid参数,根据第二步的映射获取授权凭证access_token,调用微信接口检测凭证有效性

④ /get_user_info,获取微信用户信息,接收openid参数,根据第二步的映射获取有效的授权凭证access_token,调用微信拉取信息接口

几个接口分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247

/**
* 拉取微信授权页
*/
router.get('/authorize', async (ctx) => {
const callbackUrl = encodeURIComponent('https://omgfaq.com/wechat/get_user_openid')
ctx.redirect(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${callbackUrl}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`)
})

/**
* 获取用户openid(微信回调地址)
*/
router.get('/get_user_openid', async (ctx) => {
const code = ctx.query.code
log2file(`code:${code}`)
const openId = await getUserOpenId(code)
log2file(`openId:${openId}`)
// 用户openid获取成功写入cookie,重定向到首页
ctx.cookies.set('openid', openId, {
path: '/',
httpOnly: false,
expires: new Date('2222-12-12')
})
ctx.redirect('/wechat/index.html')
})

/**
* 校验用户accessToken是否失效
*/
router.get('/check_user_access_token', async (ctx) => {
const openId = ctx.cookies.get('openid', { path: '/' })
const res = await checkPageAccessToken(openId)
ctx.body = res
})

/**
* 根据cookie里的openid获取用户信息
*/
router.get('/get_user_info', async (ctx) => {
const openId = ctx.cookies.get('openid', { path: '/' })
const accessToken = openId2accessToken[openId]
let userInfo = null
// 检测授权凭证是否失效
const res = await checkPageAccessToken(openId)
if (res.errcode !== 0) {
log2file(`page access token invalidate:${openId}, ${accessToken}`)
await refreshPageAccessToken()
}
userInfo = await getUserInfo(openId, accessToken)
ctx.body = userInfo
})

app.use(router.routes()).use(router.allowedMethods());
app.use(async (ctx) => {
await send(ctx, ctx.path.replace(reg, '/$1'), { root })
})

// openid和授权凭证/刷新token的映射,代替数据库
const openId2accessToken = {}
const openId2refreshToken = {}

/**
* 根据授权回调code获取用户授权凭证access_token和用户openID
* @returns {Promise}
*/
function getUserOpenId(code) {
return new Promise((resolve, reject) => {
https.get(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${APP_ID}&secret=${APP_SECRET}&code=${code}&grant_type=authorization_code`, function (req, res) {
let jsonStr = ''
req.on('data', function (data) {
jsonStr += data
})
req.on('end', function () {
log2file(`getUserOpenId:${jsonStr}`)
const res = JSON.parse(jsonStr)
const openId = res.openid
const pageAccessToken = res.access_token
if (openId && pageAccessToken) {
// TODO 超时后刷新token
// setTimeout(() => {
// refreshPageToken()
// }, res.expires_in)
log2file(`getUserOpenId:${JSON.stringify(res)}`)
openId2accessToken[openId] = pageAccessToken
openId2refreshToken[openId] = res.refresh_token
resolve(openId)
}
else {
reject(`openId or accessToken empty:${openId},${pageAccessToken}`)
}
})
})
})
}

/**
* 获取网页授权凭证access_token和用户openID
* @returns {Promise}
*/
function getUserInfo(openId, accessToken) {
return new Promise((resolve, reject) => {
https.get(`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`, function (req, res) {
let jsonStr = ''
req.on('data', function (data) {
jsonStr += data
})
req.on('end', function () {
log2file(`getUserInfo:${jsonStr}`)
resolve(JSON.parse(jsonStr))
})
})
})
}

/**
* 获取接口调用凭证accessToken(全局使用,accessToken用于获取jsapi_ticket,jsapi_ticket用于生成权限签名)
* @returns {Promise}
*/
function getApiAccessToken() {
let apiAccessToken = '' // 凭证
let needRefresh = true // 是否需要刷新凭证
if (fs.existsSync('./api-access-token.txt')) { // 从文件系统中获取(代替数据库)
const jsonData = fs.readFileSync('./api-access-token.txt', { flag: 'r', encoding: 'utf8' })
console.log('getApiAccessToken cached: ', jsonData)
apiAccessToken = jsonData.accessToken
const expiresAt = jsonData.expiresAt
// 如果没过期不需要重新获取
if (expiresAt > Date.now()) {
needRefresh = false
}
}

if (needRefresh) {
// 从微信服务器中获取
return new Promise((resolve, reject) => {
https.get('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + APP_ID + '&secret=' + APP_SECRET, function (req, res) {
let jsonStr = ''
req.on('data', function (data) {
jsonStr += data
})
req.on('end', function () {
log2file(`refresh apiAccessToken:${jsonStr}`)
const ret = JSON.parse(jsonStr)
apiAccessToken = ret.access_token
const expiresAt = Date.now() + ret.expires_in * 1000 // 设置失效时间
if (apiAccessToken) {
// 将凭证和失效时间覆盖写入文件系统
fs.writeFileSync('./api-access-token.txt', JSON.stringify({ accessToken: apiAccessToken, expiresAt }), { flag: 'w', encoding: 'utf8' })
resolve(apiAccessToken)
}
else {
reject('apiAccessToken empty:' + apiAccessToken)
}
})
})
})
}
else {
return Promise.resolve(apiAccessToken)
}
}

/**
* 检测用户的页面授权凭证是否失效
*/
function checkPageAccessToken(openId) {
const pageAccessToken = openId2accessToken[openId]
return new Promise((resolve, reject) => {
https.get(`https://api.weixin.qq.com/sns/auth?access_token=${pageAccessToken}&openid=${openId}`, function (req, res) {
let jsonStr = ''
req.on('data', function (data) {
jsonStr += data
})
req.on('end', function () {
log2file(`checkPageAccessToken:${jsonStr}`)
const res = JSON.parse(jsonStr)
if (res['errcode'] === 0) {
resolve(res)
}
else {
resolve(res)
}
})
})
})
}

/**
* 刷新授权凭证
*/
function refreshPageAccessToken(openId) {
const refreshToken = openId2refreshToken[openId]
return new Promise((resolve, reject) => {
https.get(`https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${APP_ID}&grant_type=refresh_token&refresh_token=${refreshToken}`, function (req, res) {
let jsonStr = ''
req.on('data', function (data) {
jsonStr += data
})
req.on('end', function () {
log2file(`refreshPageAccessToken:${jsonStr}`)
const res = JSON.parse(jsonStr)
openId2accessToken[openId] = res.access_token
openId2refreshToken[openId] = res.refresh_token
resolve()
})
})
})
}

/**
* 获取缓存中的ticket
* @param {boolean} accessToken
* @returns {Promise}
*/
function getTicket(accessToken) {
if (fs.existsSync('./ticket.txt')) { // 从文件系统中获取
const ticket = fs.readFileSync('./ticket.txt', { flag: 'r', encoding: 'utf8' })
console.log('get cached ticket: ', ticket)
return Promise.resolve(ticket)
}
// 从微信服务器中获取
else {
return new Promise((resolve, reject) => {
https.get('https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' + accessToken + '&type=jsapi', function (req, res) {
let jsonStr = ''
req.on('data', function (data) {
jsonStr += data
})
req.on('end', function () {
log2file(`refresh ticket:${jsonStr}`)
const ticket = JSON.parse(jsonStr)['ticket']
if (ticket) {
fs.writeFileSync('./ticket.txt', ticket, { flag: 'w', encoding: 'utf8' })
resolve(ticket)
}
else {
reject('ticket empty:' + ticket)
}
})
})
})
}
}

function log2file(log) {
fs.writeFileSync('./std-console.txt', `${new Date().toLocaleTimeString()} : ${log}\n`, { flag: 'a', encoding: 'utf8' })
}

代码已部署,可在微信打开查看(因为是测试号,还没申请自己的公众号,不知道其他人的微信能否使用,待测试):https://omgfaq.com/wechat/index.html

完整代码戳这里

目录指引: