Golang调用Twitter API返回Unauthorized错误求助
核心问题分析
你代码里的OAuth参数是硬编码的,而Twitter API的OAuth 1.0a要求每次请求必须生成全新的动态参数,尤其是以下几个关键项:
oauth_timestamp:必须是当前请求的Unix时间戳(秒级),硬编码的旧时间戳会直接失效oauth_nonce:每次请求的随机唯一字符串,重复的nonce+timestamp组合会被拒绝oauth_signature:基于请求方法、URL、所有OAuth参数、请求体(如果是POST)计算的HMAC-SHA1签名,硬编码的签名完全不匹配当前请求的上下文
另外,你代码里的请求体是{},如果Postman里实际发送的是有内容的payload(比如包含text字段的推文内容),签名计算时的请求体哈希也会不匹配,导致授权失败。
具体修正步骤
1. 动态生成基础OAuth参数
每次请求前生成新的timestamp、nonce:
import ( "crypto/rand" "encoding/base64" "strconv" "time" ) // 生成随机nonce func generateNonce() string { b := make([]byte, 16) rand.Read(b) return base64.URLEncoding.EncodeToString(b) } // 获取当前时间戳(秒) func getTimestamp() string { return strconv.FormatInt(time.Now().Unix(), 10) }
2. 正确计算OAuth签名
OAuth 1.0a签名需要以下步骤:
- 收集所有OAuth参数(包括consumer key、token、timestamp、nonce等)和请求体的哈希(
oauth_body_hash,POST请求必须包含) - 对参数按键名排序,拼接成规范化的参数字符串
- 拼接签名基串:
请求方法&URL编码后的请求URL&URL编码后的规范化参数字符串 - 用
consumer_secret&oauth_token_secret作为密钥,对基串做HMAC-SHA1加密,结果base64编码后再URL编码得到签名
示例代码片段(替换为你的真实密钥):
import ( "crypto/hmac" "crypto/sha1" "encoding/base64" "fmt" "net/url" "sort" "strings" ) // 替换为你的真实密钥 consumerKey := "your_consumer_key" consumerSecret := "your_consumer_secret" oauthToken := "your_oauth_token" oauthTokenSecret := "your_oauth_token_secret" // 真实请求体(和Postman保持完全一致) payloadStr := `{"text":"Hello from Golang!"}` payload := strings.NewReader(payloadStr) // 计算请求体的SHA1哈希作为oauth_body_hash bodyHash := sha1.Sum([]byte(payloadStr)) oauthBodyHash := base64.StdEncoding.EncodeToString(bodyHash[:]) // 生成动态参数 timestamp := getTimestamp() nonce := generateNonce() // 收集所有OAuth参数 oauthParams := url.Values{ "oauth_consumer_key": {consumerKey}, "oauth_token": {oauthToken}, "oauth_signature_method": {"HMAC-SHA1"}, "oauth_timestamp": {timestamp}, "oauth_nonce": {nonce}, "oauth_version": {"1.0"}, "oauth_body_hash": {oauthBodyHash}, } // 规范化参数:按键排序,拼接成key=value&key=value格式 sortedKeys := make([]string, 0, len(oauthParams)) for k := range oauthParams { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) var normalizedParams string for i, k := range sortedKeys { if i > 0 { normalizedParams += "&" } normalizedParams += url.QueryEscape(k) + "=" + url.QueryEscape(oauthParams[k][0]) } // 构建签名基串 baseURL := "https://api.twitter.com/2/tweets" baseString := fmt.Sprintf("POST&%s&%s", url.QueryEscape(baseURL), url.QueryEscape(normalizedParams), ) // 生成签名密钥 signingKey := fmt.Sprintf("%s&%s", url.QueryEscape(consumerSecret), url.QueryEscape(oauthTokenSecret), ) // HMAC-SHA1加密生成签名 mac := hmac.New(sha1.New, []byte(signingKey)) mac.Write([]byte(baseString)) signatureBytes := mac.Sum(nil) oauthSignature := url.QueryEscape(base64.StdEncoding.EncodeToString(signatureBytes)) // 构建最终的Authorization头 authHeader := fmt.Sprintf(`OAuth oauth_consumer_key="%s",oauth_token="%s",oauth_signature_method="HMAC-SHA1",oauth_timestamp="%s",oauth_nonce="%s",oauth_version="1.0",oauth_body_hash="%s",oauth_signature="%s"`, consumerKey, oauthToken, timestamp, nonce, oauthBodyHash, oauthSignature, )
3. 替换硬编码的Authorization头
把代码里的req.Header.Add("Authorization", "...")替换成上面生成的authHeader,同时确保请求体和Postman完全一致。
额外注意事项
- 确保所有密钥(consumer secret、token secret)正确无误,无多余空格或复制错误
- 确认请求的
Content-Type已正确设置为application/json - Postman的"生成代码"功能不会自动处理动态签名逻辑,需要手动修改为上述动态生成逻辑
内容的提问来源于stack exchange,提问作者Angelo van Cleef
相关产品推荐
相关产品推荐

