/
public_account.go
191 lines (163 loc) · 6.94 KB
/
public_account.go
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
package wechat
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/adamesong/go-util/logging"
"github.com/adamesong/go-util/redis"
"github.com/adamesong/go-util/signature"
"github.com/pkg/errors"
)
const (
AccessTokenCachePrefix = "wechat_public_access_token:" // 用于缓存 access_token(微信公众号接口调用凭证,缓存2小时) 的prefix,与微信公众号AppID组合成为cache key
JSApiTicketCachePrefix = "wechat_public_jsapi_ticket:" // 用户缓存 jsapi_ticket(公众号用于调用微信 JS 接口的临时票据。缓存2小时) 的prefix,与微信公众号AppID组合成为 cache key
)
type WeichatPublicDev struct {
AppID string // 微信公众号后台-基本配置-公众号开发信息-开发者ID(AppID)
AppSecret string // 微信公众号后台-基本配置-公众号开发信息-开发者密码(AppSecret)
RedisClient *redis.RedisClient // 获取的Access Token将被缓存到哪里
// AccessTokenCacheKey string // access token在缓存中的key
// JSApiTicketCacheKey string // jsapi_ticket 在缓存中的key
}
// 将prefix 与appID组合成 access_token的cache key
func (wx *WeichatPublicDev) AccessTokenCacheKey() string {
return fmt.Sprintf("%s%s", AccessTokenCachePrefix, wx.AppID)
}
// 将prefix 与appID组合成 jsapi_ticket的cache key
func (wx *WeichatPublicDev) JSApiTicketCacheKey() string {
return fmt.Sprintf("%s%s", JSApiTicketCachePrefix, wx.AppID)
}
// GetAccessToken,access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。
// https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
func (wx *WeichatPublicDev) GetAccessToken() (token string, err error) {
// 先从缓存中检查是否有有效的accessToken,
expDuration, err := wx.RedisClient.TTL(wx.AccessTokenCacheKey())
if err != nil {
return "", err
}
// 如果有效期大大于20秒,则从缓存中取出这个值,并返回
if expDuration.Seconds() >= 20 {
if tokenByte, err := wx.RedisClient.Get(wx.AccessTokenCacheKey()); err != nil {
return "", err
} else {
return string(tokenByte), nil
}
}
// 有效期如果低于20秒,则重新请求微信接口,避免频繁请求导致微信拒绝调用
return wx.FetchAccessToken()
}
// 从微信获取新的access token(不从缓存中获取)
func (wx *WeichatPublicDev) FetchAccessToken() (token string, err error) {
if resp, err := http.Get(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", wx.AppID, wx.AppSecret)); err != nil {
return "", err
} else {
if body, err := ioutil.ReadAll(resp.Body); err != nil {
return "", err
} else {
respMap := make(map[string]interface{})
if err := json.Unmarshal(body, &respMap); err != nil {
return "", err
}
if token, exist := respMap["access_token"].(string); exist {
// 获取成功后,存入缓存,并返回
if err := wx.RedisClient.Set(wx.AccessTokenCacheKey(), token, time.Second*time.Duration(respMap["expires_in"].(float64))); err != nil {
return "", err
} else {
return token, nil
}
}
logging.Error(fmt.Sprintf("Getting wechat public account access_token error: %v %v", respMap["errcode"], respMap["errmsg"]))
return "", fmt.Errorf("Error: %v %v", respMap["errcode"], respMap["errmsg"])
}
}
}
// 获得jsapi_ticket, jsapi_ticket是公众号用于调用微信 JS 接口的临时票据
func (wx *WeichatPublicDev) GetJSApiTicket() (ticket string, err error) {
// 先从缓存中检查是否有有效的jsapi_ticket
expDuration, err := wx.RedisClient.TTL(wx.JSApiTicketCacheKey())
if err != nil {
return "", err
}
// 如果有效期大大于20秒,则从缓存中取出这个值,并返回
if expDuration.Seconds() >= 20 {
if ticketByte, err := wx.RedisClient.Get(wx.JSApiTicketCacheKey()); err != nil {
return "", err
} else {
return string(ticketByte), nil
}
}
// 有效期如果低于20秒,则重新请求微信接口,避免频繁请求导致微信拒绝调用
return wx.FetchJSApiTicket()
}
// 从微信获取新的jsapi_ticket(不从缓存中获取)
func (wx *WeichatPublicDev) FetchJSApiTicket() (ticket string, err error) {
// 先获得access_token
accessToken, err := wx.GetAccessToken()
if err != nil {
return "", err
}
// 用access_token获得jsapi_ticket
if resp, err := http.Get(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi", accessToken)); err != nil {
return "", err
} else {
if body, err := ioutil.ReadAll(resp.Body); err != nil {
return "", err
} else {
// {
// "errcode":0,
// "errmsg":"ok",
// "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
// "expires_in":7200
// }
respMap := make(map[string]interface{})
if err := json.Unmarshal(body, &respMap); err != nil {
return "", err
}
if respMap["errmsg"] != "ok" {
return "", errors.Errorf("Error: %v %v", respMap["errcode"], respMap["errmsg"])
}
if ticket, exist := respMap["ticket"].(string); exist {
// 获取成功后,存入缓存,并返回
if err := wx.RedisClient.Set(wx.JSApiTicketCacheKey(), ticket, time.Second*time.Duration(respMap["expires_in"].(float64))); err != nil {
return "", err
} else {
return ticket, nil
}
}
logging.Error(fmt.Sprintf("Getting wechat public account jsapi_ticket error: %v %v", respMap["errcode"], respMap["errmsg"]))
return "", fmt.Errorf("Error: %v %v", respMap["errcode"], respMap["errmsg"])
}
}
}
// 用于签名的结构体
type SignParams struct {
Nonce string `sign:"noncestr"`
JSApiTicket string `sign:"jsapi_ticket"`
Timestamp string `sign:"timestamp"`
URL string `sign:"url"`
}
// 签名算法:
// 签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。
// 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用 URL 键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
// 这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。即signature=sha1(string1)。
// noncestr=Wm3WZYTPz0wzccnW
// jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
// timestamp=1414587457
// url=http://mp.weixin.qq.com?params=value
func (signParams *SignParams) GetSign() (sign string) {
strToSign := signature.GetValidStr(*signParams)
// 方法1:
signByte := sha1.Sum([]byte(strToSign))
sign = hex.EncodeToString(signByte[:])
// 方法2:
// hash := sha1.New()
// hash.Write([]byte(strToSign))
// md := hash.Sum(nil)
// mdStr := hex.EncodeToString(md)
// sign = strings.ToLower(mdStr)
return
}