// GET 微信资源, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + component_access_token; // 3. response 格式有要求, 要么是 *mp.Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // mp.Error // ... // } func (clt *Client) GetJSON(incompleteURL string, response interface{}) (err error) { token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) httpResp, err := clt.HttpClient.Get(finalURL) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } if err = json.NewDecoder(httpResp.Body).Decode(response); err != nil { return } var ErrorStructValue reflect.Value // mp.Error // 下面的代码对 response 有特定要求, 见此函数 NOTE responseStructValue := reflect.ValueOf(response).Elem() if v := responseStructValue.Field(0); v.Kind() == reflect.Struct { ErrorStructValue = v } else { ErrorStructValue = responseStructValue } switch ErrCode := ErrorStructValue.Field(0).Int(); ErrCode { case mp.ErrCodeOK: return case mp.ErrCodeInvalidCredential, mp.ErrCodeAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() mp.LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", ErrMsg) mp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } mp.LogInfoln("[WECHAT_RETRY] new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } mp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: return } }
// 下载多媒体到 io.Writer. func (clt Client) downloadMediaToWriter(mediaId string, writer io.Writer) (err error) { token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := "https://api.weixin.qq.com/cgi-bin/media/get?media_id=" + url.QueryEscape(mediaId) + "&access_token=" + url.QueryEscape(token) httpResp, err := clt.HttpClient.Get(finalURL) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } ContentType, _, _ := mime.ParseMediaType(httpResp.Header.Get("Content-Type")) if ContentType != "text/plain" && ContentType != "application/json" { // 返回的是媒体流 _, err = io.Copy(writer, httpResp.Body) return } // 返回的是错误信息 var result mp.Error if err = json.NewDecoder(httpResp.Body).Decode(&result); err != nil { return } switch result.ErrCode { case mp.ErrCodeOK: return // 基本不会出现 case mp.ErrCodeInvalidCredential, mp.ErrCodeAccessTokenExpired: // 失效(过期)重试一次 mp.LogInfoln("[WECHAT_RETRY] err_code:", result.ErrCode, ", err_msg:", result.ErrMsg) mp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } mp.LogInfoln("[WECHAT_RETRY] new token:", token) result = mp.Error{} goto RETRY } mp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: err = &result return } }
// 检验授权凭证(access_token)是否有效. // NOTE: // 1. Client 需要指定 OAuth2Token // 2. 先判断 err 然后再判断 valid func (clt *Client) CheckAccessTokenValid() (valid bool, err error) { if clt.OAuth2Token == nil { err = errors.New("没有提供 OAuth2Token") return } if clt.AccessToken == "" { err = errors.New("没有有效的 AccessToken") return } if clt.OpenId == "" { err = errors.New("没有有效的 OpenId") return } _url := "https://api.weixin.qq.com/sns/auth?access_token=" + url.QueryEscape(clt.AccessToken) + "&openid=" + url.QueryEscape(clt.OpenId) httpResp, err := clt.httpClient().Get(_url) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { err = fmt.Errorf("http.Status: %s", httpResp.Status) return } var result mp.Error respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } mp.LogInfoln("[WECHAT_DEBUG] request url:", _url) mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, &result); err != nil { return } switch result.ErrCode { case mp.ErrCodeOK: valid = true return case 40001: return default: err = &result return } }
func (clt *Client) getJSON(url string, response interface{}) (err error) { mp.LogInfoln("[WECHAT_DEBUG] request url:", url) httpResp, err := clt.httpClient().Get(url) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) return json.Unmarshal(respBody, response) }
// 下载多媒体到 io.Writer. func (clt Client) downloadMaterialToWriter(mediaId string, writer io.Writer) (err error) { var request = struct { MediaId string `json:"media_id"` }{ MediaId: mediaId, } requestBody, err := json.Marshal(&request) if err != nil { return } token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=" + url.QueryEscape(token) httpResp, err := clt.HttpClient.Post(finalURL, "application/json; charset=utf-8", bytes.NewReader(requestBody)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } // f**k, 騰訊這次又蛋疼了, Content-Type 不能區分返回的是媒體類型還是錯誤 var respBegin [11]byte // {"errcode": or {"errmsg":" n, err := io.ReadFull(httpResp.Body, respBegin[:]) switch { case err == nil: break case err == io.ErrUnexpectedEOF: _, err = writer.Write(respBegin[:n]) return case err == io.EOF: err = nil return default: return } httpRespBody := io.MultiReader(bytes.NewReader(respBegin[:]), httpResp.Body) if !bytes.Equal(respBegin[:], errRespBeginCode) && !bytes.Equal(respBegin[:], errRespBeginMsg) { // 返回的是媒體內容 _, err = io.Copy(writer, httpRespBody) return } // 返回的是错误信息 var result mp.Error if err = json.NewDecoder(httpRespBody).Decode(&result); err != nil { return } switch result.ErrCode { case mp.ErrCodeOK: return // 基本不会出现 case mp.ErrCodeInvalidCredential, mp.ErrCodeAccessTokenExpired: // 失效(过期)重试一次 mp.LogInfoln("[WECHAT_RETRY] err_code:", result.ErrCode, ", err_msg:", result.ErrMsg) mp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } mp.LogInfoln("[WECHAT_RETRY] new token:", token) result = mp.Error{} goto RETRY } mp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: err = &result return } }
// 从微信服务器获取 component_access_token. // 同一时刻只能一个 goroutine 进入, 防止没必要的重复获取. func (srv *DefaultAccessTokenServer) getToken() (token accessTokenInfo, cached bool, err error) { srv.tokenGet.Lock() defer srv.tokenGet.Unlock() timeNowUnix := time.Now().Unix() // 在收敛周期内直接返回最近一次获取的 component_access_token, 这里的收敛时间设定为4秒. if n := srv.tokenGet.LastTimestamp; n <= timeNowUnix && timeNowUnix < n+4 { // 因为只有成功获取后才会更新 srv.tokenGet.LastTimestamp, 所以这些都是有效数据 token = accessTokenInfo{ Token: srv.tokenGet.LastTokenInfo.Token, ExpiresIn: srv.tokenGet.LastTokenInfo.ExpiresIn - timeNowUnix + n, } cached = true return } verifyTicket, err := srv.verifyTicketGetter.GetComponentVerifyTicket(srv.appId) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } request := struct { AppId string `json:"component_appid"` AppSecret string `json:"component_appsecret"` VerifyTicket string `json:"component_verify_ticket"` }{ AppId: srv.appId, AppSecret: srv.appSecret, VerifyTicket: verifyTicket, } requestBuf := textBufferPool.Get().(*bytes.Buffer) requestBuf.Reset() defer textBufferPool.Put(requestBuf) if err = json.NewEncoder(requestBuf).Encode(&request); err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } requestBytes := requestBuf.Bytes() url := "https://api.weixin.qq.com/cgi-bin/component/api_component_token" mp.LogInfoln("[WECHAT_DEBUG] request url:", url) mp.LogInfoln("[WECHAT_DEBUG] request json:", string(requestBytes)) httpResp, err := srv.httpClient.Post(url, "application/json; charset=utf-8", bytes.NewReader(requestBytes)) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = fmt.Errorf("http.Status: %s", httpResp.Status) return } var result struct { mp.Error accessTokenInfo } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, &result); err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } if result.ErrCode != mp.ErrCodeOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = &result.Error return } // 由于网络的延时, component_access_token 过期时间留了一个缓冲区 switch { case result.ExpiresIn > 31556952: // 60*60*24*365.2425 srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = errors.New("expires_in too large: " + strconv.FormatInt(result.ExpiresIn, 10)) return case result.ExpiresIn > 60*60: result.ExpiresIn -= 60 * 10 case result.ExpiresIn > 60*30: result.ExpiresIn -= 60 * 5 case result.ExpiresIn > 60*5: result.ExpiresIn -= 60 case result.ExpiresIn > 60: result.ExpiresIn -= 10 default: srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = errors.New("expires_in too small: " + strconv.FormatInt(result.ExpiresIn, 10)) return } // 更新 tokenGet 信息 srv.tokenGet.LastTokenInfo = result.accessTokenInfo srv.tokenGet.LastTimestamp = timeNowUnix // 更新缓存 srv.tokenCache.Lock() srv.tokenCache.Token = result.accessTokenInfo.Token srv.tokenCache.Unlock() token = result.accessTokenInfo return }
// 用 encoding/json 把 request marshal 为 JSON, 放入 http 请求的 body 中, // POST 到微信服务器, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + component_access_token; // 3. response 格式有要求, 要么是 *mp.Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // mp.Error // ... // } func (clt *Client) PostJSON(incompleteURL string, request interface{}, response interface{}) (err error) { buf := textBufferPool.Get().(*bytes.Buffer) buf.Reset() defer textBufferPool.Put(buf) if err = json.NewEncoder(buf).Encode(request); err != nil { return } requestBytes := buf.Bytes() token, err := clt.Token() if err != nil { return } hasRetried := false RETRY: finalURL := incompleteURL + url.QueryEscape(token) mp.LogInfoln("[WECHAT_DEBUG] request url:", finalURL) mp.LogInfoln("[WECHAT_DEBUG] request json:", string(requestBytes)) httpResp, err := clt.HttpClient.Post(finalURL, "application/json; charset=utf-8", bytes.NewReader(requestBytes)) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, response); err != nil { return } var ErrorStructValue reflect.Value // mp.Error // 下面的代码对 response 有特定要求, 见此函数 NOTE responseStructValue := reflect.ValueOf(response).Elem() if v := responseStructValue.Field(0); v.Kind() == reflect.Struct { ErrorStructValue = v } else { ErrorStructValue = responseStructValue } switch ErrCode := ErrorStructValue.Field(0).Int(); ErrCode { case mp.ErrCodeOK: return case mp.ErrCodeInvalidCredential, mp.ErrCodeAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() mp.LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", ErrMsg) mp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } mp.LogInfoln("[WECHAT_RETRY] new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } mp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: return } }
// 获取用户信息(需scope为 snsapi_userinfo). // NOTE: // 1. Client 需要指定 OAuth2Config, OAuth2Token // 2. lang 可能的取值是 zh_CN, zh_TW, en, 如果留空 "" 则默认为 zh_CN. func (clt *Client) UserInfo(lang string) (info *UserInfo, err error) { switch lang { case "": lang = Language_zh_CN case Language_zh_CN, Language_zh_TW, Language_en: default: err = errors.New("错误的 lang 参数") return } if clt.OAuth2Config == nil { // clt.TokenRefresh() 需要 err = errors.New("没有提供 OAuth2Config") return } if clt.OAuth2Token == nil { err = errors.New("没有提供 OAuth2Token") return } if clt.accessTokenExpired() { if _, err = clt.TokenRefresh(); err != nil { return } } if clt.AccessToken == "" { err = errors.New("没有有效的 AccessToken") return } if clt.OpenId == "" { err = errors.New("没有有效的 OpenId") return } _url := "https://api.weixin.qq.com/sns/userinfo" + "?access_token=" + url.QueryEscape(clt.AccessToken) + "&openid=" + url.QueryEscape(clt.OpenId) + "&lang=" + url.QueryEscape(lang) httpResp, err := clt.httpClient().Get(_url) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { err = fmt.Errorf("http.Status: %s", httpResp.Status) return } var result struct { mp.Error UserInfo } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } mp.LogInfoln("[WECHAT_DEBUG] request url:", _url) mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, &result); err != nil { return } if result.ErrCode != mp.ErrCodeOK { err = &result.Error return } info = &result.UserInfo return }
// ServeHTTP 处理 http 消息请求 // NOTE: 调用者保证所有参数有效 func ServeHTTP(w http.ResponseWriter, r *http.Request, queryValues url.Values, srv Server, errHandler mp.ErrorHandler) { mp.LogInfoln("[WECHAT_DEBUG] request uri:", r.RequestURI) mp.LogInfoln("[WECHAT_DEBUG] request remote-addr:", r.RemoteAddr) mp.LogInfoln("[WECHAT_DEBUG] request user-agent:", r.UserAgent()) switch r.Method { case "POST": // 消息处理 switch encryptType := queryValues.Get("encrypt_type"); encryptType { case "aes": msgSignature1 := queryValues.Get("msg_signature") if msgSignature1 == "" { errHandler.ServeError(w, r, errors.New("msg_signature is empty")) return } timestampStr := queryValues.Get("timestamp") if timestampStr == "" { errHandler.ServeError(w, r, errors.New("timestamp is empty")) return } timestamp, err := strconv.ParseInt(timestampStr, 10, 64) if err != nil { err = errors.New("can not parse timestamp to int64: " + timestampStr) errHandler.ServeError(w, r, err) return } nonce := queryValues.Get("nonce") if nonce == "" { errHandler.ServeError(w, r, errors.New("nonce is empty")) return } reqBody, err := ioutil.ReadAll(r.Body) if err != nil { errHandler.ServeError(w, r, err) return } mp.LogInfoln("[WECHAT_DEBUG] request msg http body:\r\n", string(reqBody)) var requestHttpBody RequestHttpBody if err := xml.Unmarshal(reqBody, &requestHttpBody); err != nil { errHandler.ServeError(w, r, err) return } haveAppId := requestHttpBody.AppId wantAppId := srv.AppId() if wantAppId != "" && !security.SecureCompareString(haveAppId, wantAppId) { err = fmt.Errorf("the RequestHttpBody's AppId mismatch, have: %s, want: %s", haveAppId, wantAppId) errHandler.ServeError(w, r, err) return } token := srv.Token() // 验证签名 msgSignature2 := util.MsgSign(token, timestampStr, nonce, requestHttpBody.EncryptedMsg) if !security.SecureCompareString(msgSignature1, msgSignature2) { err = fmt.Errorf("check msg_signature failed, input: %s, local: %s", msgSignature1, msgSignature2) errHandler.ServeError(w, r, err) return } // 解密 encryptedMsgBytes, err := base64.StdEncoding.DecodeString(requestHttpBody.EncryptedMsg) if err != nil { errHandler.ServeError(w, r, err) return } aesKey := srv.CurrentAESKey() random, rawMsgXML, aesAppId, err := util.AESDecryptMsg(encryptedMsgBytes, aesKey) if err != nil { // 尝试用上一次的 AESKey 来解密 lastAESKey, isLastAESKeyValid := srv.LastAESKey() if !isLastAESKeyValid { errHandler.ServeError(w, r, err) return } aesKey = lastAESKey // NOTE random, rawMsgXML, aesAppId, err = util.AESDecryptMsg(encryptedMsgBytes, aesKey) if err != nil { errHandler.ServeError(w, r, err) return } } if haveAppId != string(aesAppId) { err = fmt.Errorf("the RequestHttpBody's ToUserName(==%s) mismatch the AppId with aes encrypt(==%s)", haveAppId, aesAppId) errHandler.ServeError(w, r, err) return } mp.LogInfoln("[WECHAT_DEBUG] request msg raw xml:\r\n", string(rawMsgXML)) // 解密成功, 解析 MixedMessage var mixedMsg MixedMessage if err := xml.Unmarshal(rawMsgXML, &mixedMsg); err != nil { errHandler.ServeError(w, r, err) return } // 安全考虑再次验证 AppId if haveAppId != mixedMsg.AppId { err = fmt.Errorf("the RequestHttpBody's AppId(==%s) mismatch the MixedMessage's AppId(==%s)", haveAppId, mixedMsg.AppId) errHandler.ServeError(w, r, err) return } // 成功, 交给 MessageHandler req := &Request{ Token: token, HttpRequest: r, QueryValues: queryValues, MsgSignature: msgSignature1, EncryptType: encryptType, Timestamp: timestamp, Nonce: nonce, RawMsgXML: rawMsgXML, MixedMsg: &mixedMsg, AESKey: aesKey, Random: random, AppId: haveAppId, } srv.MessageHandler().ServeMessage(w, req) default: // 未知的加密类型 err := errors.New("unknown encrypt_type: " + encryptType) errHandler.ServeError(w, r, err) return } case "GET": // 首次验证 signature1 := queryValues.Get("signature") if signature1 == "" { errHandler.ServeError(w, r, errors.New("signature is empty")) return } timestamp := queryValues.Get("timestamp") if timestamp == "" { errHandler.ServeError(w, r, errors.New("timestamp is empty")) return } nonce := queryValues.Get("nonce") if nonce == "" { errHandler.ServeError(w, r, errors.New("nonce is empty")) return } echostr := queryValues.Get("echostr") if echostr == "" { errHandler.ServeError(w, r, errors.New("echostr is empty")) return } signature2 := util.Sign(srv.Token(), timestamp, nonce) if !security.SecureCompareString(signature1, signature2) { err := fmt.Errorf("check signature failed, input: %s, local: %s", signature1, signature2) errHandler.ServeError(w, r, err) return } io.WriteString(w, echostr) } }
// 从服务器获取新的 token 更新 tk func (clt *Client) updateToken(tk *OAuth2Token, url string) (err error) { if tk == nil { return errors.New("nil OAuth2Token") } httpResp, err := clt.httpClient().Get(url) if err != nil { return } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { return fmt.Errorf("http.Status: %s", httpResp.Status) } var result struct { mp.Error AccessToken string `json:"access_token"` // 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 RefreshToken string `json:"refresh_token"` // 用户刷新access_token ExpiresIn int64 `json:"expires_in"` // access_token接口调用凭证超时时间,单位(秒) OpenId string `json:"openid"` // 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID UnionId string `json:"unionid"` // UnionID机制 Scope string `json:"scope"` // 用户授权的作用域,使用逗号(,)分隔 } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { return } mp.LogInfoln("[WECHAT_DEBUG] request url:", url) mp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, &result); err != nil { return } if result.ErrCode != mp.ErrCodeOK { return &result.Error } // 由于网络的延时, 分布式服务器之间的时间可能不是绝对同步, access_token 过期时间留了一个缓冲区; switch { case result.ExpiresIn > 31556952: // 60*60*24*365.2425 err = errors.New("expires_in too large: " + strconv.FormatInt(result.ExpiresIn, 10)) return case result.ExpiresIn > 60*60: result.ExpiresIn -= 60 * 20 case result.ExpiresIn > 60*30: result.ExpiresIn -= 60 * 10 case result.ExpiresIn > 60*15: result.ExpiresIn -= 60 * 5 case result.ExpiresIn > 60*5: result.ExpiresIn -= 60 case result.ExpiresIn > 60: result.ExpiresIn -= 20 default: err = errors.New("expires_in too small: " + strconv.FormatInt(result.ExpiresIn, 10)) return } tk.AccessToken = result.AccessToken if result.RefreshToken != "" { tk.RefreshToken = result.RefreshToken } tk.ExpiresAt = time.Now().Unix() + result.ExpiresIn tk.OpenId = result.OpenId tk.UnionId = result.UnionId strs := strings.Split(result.Scope, ",") tk.Scopes = make([]string, 0, len(strs)) for _, str := range strs { str = strings.TrimSpace(str) if str == "" { continue } tk.Scopes = append(tk.Scopes, str) } return }