// GET 微信资源, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + suite_access_token; // 3. response 格式有要求, 要么是 *corp.Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // corp.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 // corp.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 corp.ErrCodeOK: return case corp.ErrCodeSuiteAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() corp.LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", ErrMsg) corp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } corp.LogInfoln("[WECHAT_RETRY] new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } corp.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://qyapi.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 corp.Error if err = json.NewDecoder(httpResp.Body).Decode(&result); err != nil { return } switch result.ErrCode { case corp.ErrCodeOK: return // 基本不会出现 case corp.ErrCodeAccessTokenExpired: // 失效(过期)重试一次 corp.LogInfoln("[WECHAT_RETRY] err_code:", result.ErrCode, ", err_msg:", result.ErrMsg) corp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } corp.LogInfoln("[WECHAT_RETRY] new token:", token) result = corp.Error{} goto RETRY } corp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: err = &result return } }
// 用 encoding/json 把 request marshal 为 JSON, 放入 http 请求的 body 中, // POST 到微信服务器, 然后将微信服务器返回的 JSON 用 encoding/json 解析到 response. // // NOTE: // 1. 一般不用调用这个方法, 请直接调用高层次的封装方法; // 2. 最终的 URL == incompleteURL + suite_access_token; // 3. response 格式有要求, 要么是 *corp.Error, 要么是下面结构体的指针(注意 Error 必须是第一个 Field): // struct { // corp.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) corp.LogInfoln("[WECHAT_DEBUG] request url:", finalURL) corp.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 } corp.LogInfoln("[WECHAT_DEBUG] response json:", string(respBody)) if err = json.Unmarshal(respBody, response); err != nil { return } var ErrorStructValue reflect.Value // corp.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 corp.ErrCodeOK: return case corp.ErrCodeSuiteAccessTokenExpired: ErrMsg := ErrorStructValue.Field(1).String() corp.LogInfoln("[WECHAT_RETRY] err_code:", ErrCode, ", err_msg:", ErrMsg) corp.LogInfoln("[WECHAT_RETRY] current token:", token) if !hasRetried { hasRetried = true if token, err = clt.TokenRefresh(); err != nil { return } corp.LogInfoln("[WECHAT_RETRY] new token:", token) responseStructValue.Set(reflect.New(responseStructValue.Type()).Elem()) goto RETRY } corp.LogInfoln("[WECHAT_RETRY] fallthrough, current token:", token) fallthrough default: return } }
// ServeHTTP 处理 http 消息请求 // NOTE: 调用者保证所有参数有效 func ServeHTTP(w http.ResponseWriter, r *http.Request, queryValues url.Values, srv Server, errHandler corp.ErrorHandler) { corp.LogInfoln("[WECHAT_DEBUG] request uri:", r.RequestURI) corp.LogInfoln("[WECHAT_DEBUG] request remote-addr:", r.RemoteAddr) corp.LogInfoln("[WECHAT_DEBUG] request user-agent:", r.UserAgent()) switch r.Method { case "POST": // 消息处理 msgSignature1 := queryValues.Get("msg_signature") if msgSignature1 == "" { errHandler.ServeError(w, r, errors.New("msg_signature is empty")) return } if len(msgSignature1) != 40 { // sha1 err := fmt.Errorf("the length of msg_signature mismatch, have: %d, want: 40", len(msgSignature1)) errHandler.ServeError(w, r, err) 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 } corp.LogInfoln("[WECHAT_DEBUG] request msg http body:\r\n", string(reqBody)) // 解析 RequestHttpBody var requestHttpBody RequestHttpBody if err := xml.Unmarshal(reqBody, &requestHttpBody); err != nil { errHandler.ServeError(w, r, err) return } suiteId := srv.SuiteId() haveSuiteId := requestHttpBody.SuiteId if len(haveSuiteId) != len(suiteId) { err = fmt.Errorf("the RequestHttpBody's ToUserName mismatch, have: %s, want: %s", haveSuiteId, suiteId) errHandler.ServeError(w, r, err) return } if subtle.ConstantTimeCompare([]byte(haveSuiteId), []byte(suiteId)) != 1 { err = fmt.Errorf("the RequestHttpBody's ToUserName mismatch, have: %s, want: %s", haveSuiteId, suiteId) errHandler.ServeError(w, r, err) return } suiteToken := srv.SuiteToken() // 验证签名 msgSignature2 := util.MsgSign(suiteToken, timestampStr, nonce, requestHttpBody.EncryptedMsg) if subtle.ConstantTimeCompare([]byte(msgSignature1), []byte(msgSignature2)) != 1 { 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, err := util.AESDecryptMsg(encryptedMsgBytes, suiteId, aesKey) if err != nil { // 尝试用上一次的 AESKey 来解密 lastAESKey, isLastAESKeyValid := srv.LastAESKey() if !isLastAESKeyValid { errHandler.ServeError(w, r, err) return } aesKey = lastAESKey // NOTE random, rawMsgXML, err = util.AESDecryptMsg(encryptedMsgBytes, suiteId, aesKey) if err != nil { errHandler.ServeError(w, r, err) return } } corp.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 } // 安全考虑再次验证 if haveSuiteId != mixedMsg.SuiteId { err = fmt.Errorf("the RequestHttpBody's ToUserName(==%s) mismatch the MixedMessage's SuiteId(==%s)", haveSuiteId, mixedMsg.SuiteId) errHandler.ServeError(w, r, err) return } // 成功, 交给 MessageHandler req := &Request{ HttpRequest: r, QueryValues: queryValues, MsgSignature: msgSignature1, Timestamp: timestamp, Nonce: nonce, RawMsgXML: rawMsgXML, MixedMsg: &mixedMsg, AESKey: aesKey, Random: random, SuiteId: haveSuiteId, SuiteToken: suiteToken, } srv.MessageHandler().ServeMessage(w, req) case "GET": // 首次验证 msgSignature1 := queryValues.Get("msg_signature") if msgSignature1 == "" { errHandler.ServeError(w, r, errors.New("msg_signature is empty")) return } if len(msgSignature1) != 40 { // sha1 err := fmt.Errorf("the length of msg_signature mismatch, have: %d, want: 40", len(msgSignature1)) errHandler.ServeError(w, r, err) 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 } encryptedMsg := queryValues.Get("echostr") if encryptedMsg == "" { errHandler.ServeError(w, r, errors.New("echostr is empty")) return } msgSignature2 := util.MsgSign(srv.SuiteToken(), timestamp, nonce, encryptedMsg) if subtle.ConstantTimeCompare([]byte(msgSignature1), []byte(msgSignature2)) != 1 { err := fmt.Errorf("check msg_signature failed, input: %s, local: %s", msgSignature1, msgSignature2) errHandler.ServeError(w, r, err) return } // 解密 encryptedMsgBytes, err := base64.StdEncoding.DecodeString(encryptedMsg) if err != nil { errHandler.ServeError(w, r, err) return } suiteId := srv.SuiteId() aesKey := srv.CurrentAESKey() _, echostr, err := util.AESDecryptMsg(encryptedMsgBytes, suiteId, aesKey) if err != nil { errHandler.ServeError(w, r, err) return } w.Write(echostr) } }
// 从微信服务器获取 suite_access_token. // 同一时刻只能一个 goroutine 进入, 防止没必要的重复获取. func (srv *DefaultAccessTokenServer) getToken() (token accessTokenInfo, cached bool, err error) { srv.tokenGet.Lock() defer srv.tokenGet.Unlock() timeNowUnix := time.Now().Unix() // 在收敛周期内直接返回最近一次获取的 suite_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 } suiteTicket, err := srv.ticketGetter.GetSuiteTicket(srv.suiteId) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } request := struct { SuiteId string `json:"suite_id"` SuiteSecret string `json:"suite_secret"` SuiteTicket string `json:"suite_ticket"` }{ SuiteId: srv.suiteId, SuiteSecret: srv.suiteSecret, SuiteTicket: suiteTicket, } 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://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token" corp.LogInfoln("[WECHAT_DEBUG] request url:", url) corp.LogInfoln("[WECHAT_DEBUG] request json:", string(requestBytes)) httpResp, err := srv.httpClient.Post(url, "application/json; charset=utf-8", requestBuf) 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 { corp.Error accessTokenInfo } respBody, err := ioutil.ReadAll(httpResp.Body) if err != nil { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() return } corp.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 != corp.ErrCodeOK { srv.tokenCache.Lock() srv.tokenCache.Token = "" srv.tokenCache.Unlock() err = &result.Error return } // 由于网络的延时, suite_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 }