Golang中如何使用OAuth1授权eTrade API

Golang中如何使用OAuth1授权eTrade API https://forum.golangbridge.org/t/etrade-api-authentication-could-not-be-completed-at-this-time/28660

我仍在尝试解决这个问题。eTrade的技术支持只是让我去阅读说明文档。据我所知,我已经按照说明在做了。

如果您能提供任何帮助或指导,我将不胜感激。


更多关于Golang中如何使用OAuth1授权eTrade API的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中如何使用OAuth1授权eTrade API的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中实现OAuth1授权访问eTrade API,关键在于正确构建签名基础字符串和签名。以下是完整的实现示例:

package main

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/base64"
    "fmt"
    "math/rand"
    "net/http"
    "net/url"
    "sort"
    "strconv"
    "strings"
    "time"
)

type OAuth1Config struct {
    ConsumerKey    string
    ConsumerSecret string
    RequestTokenURL string
    AuthorizeURL   string
    AccessTokenURL string
    CallbackURL    string
}

func generateNonce() string {
    const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    result := make([]byte, 32)
    for i := range result {
        result[i] = chars[rand.Intn(len(chars))]
    }
    return string(result)
}

func generateTimestamp() string {
    return strconv.FormatInt(time.Now().Unix(), 10)
}

func percentEncode(s string) string {
    return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
}

func buildSignatureBaseString(method, baseURL string, params map[string]string) string {
    // 收集所有参数
    allParams := make(map[string]string)
    for k, v := range params {
        allParams[k] = v
    }
    
    // 排序参数
    keys := make([]string, 0, len(allParams))
    for k := range allParams {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    
    // 构建参数字符串
    paramPairs := make([]string, 0, len(keys))
    for _, k := range keys {
        paramPairs = append(paramPairs, 
            percentEncode(k)+"="+percentEncode(allParams[k]))
    }
    parameterString := strings.Join(paramPairs, "&")
    
    // 构建签名基础字符串
    baseComponents := []string{
        method,
        percentEncode(baseURL),
        percentEncode(parameterString),
    }
    
    return strings.Join(baseComponents, "&")
}

func generateSignature(baseString, consumerSecret, tokenSecret string) string {
    signingKey := percentEncode(consumerSecret) + "&" + percentEncode(tokenSecret)
    
    h := hmac.New(sha1.New, []byte(signingKey))
    h.Write([]byte(baseString))
    signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
    
    return percentEncode(signature)
}

func buildOAuthHeader(params map[string]string) string {
    headerParts := []string{"OAuth"}
    for k, v := range params {
        if strings.HasPrefix(k, "oauth_") {
            headerParts = append(headerParts, 
                percentEncode(k)+`="`+percentEncode(v)+`"`)
        }
    }
    return strings.Join(headerParts, " ")
}

func GetRequestToken(config OAuth1Config) (string, error) {
    nonce := generateNonce()
    timestamp := generateTimestamp()
    
    oauthParams := map[string]string{
        "oauth_callback":         config.CallbackURL,
        "oauth_consumer_key":     config.ConsumerKey,
        "oauth_nonce":            nonce,
        "oauth_signature_method": "HMAC-SHA1",
        "oauth_timestamp":        timestamp,
        "oauth_version":          "1.0",
    }
    
    // 构建签名
    baseString := buildSignatureBaseString(
        "POST",
        config.RequestTokenURL,
        oauthParams,
    )
    
    signature := generateSignature(baseString, config.ConsumerSecret, "")
    oauthParams["oauth_signature"] = signature
    
    // 创建请求
    req, err := http.NewRequest("POST", config.RequestTokenURL, nil)
    if err != nil {
        return "", err
    }
    
    req.Header.Set("Authorization", buildOAuthHeader(oauthParams))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    
    // 发送请求
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    // 解析响应
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    
    values, err := url.ParseQuery(string(body))
    if err != nil {
        return "", err
    }
    
    return values.Get("oauth_token"), nil
}

func GetAccessToken(config OAuth1Config, requestToken, verifier string) (string, error) {
    nonce := generateNonce()
    timestamp := generateTimestamp()
    
    oauthParams := map[string]string{
        "oauth_consumer_key":     config.ConsumerKey,
        "oauth_token":            requestToken,
        "oauth_verifier":         verifier,
        "oauth_nonce":            nonce,
        "oauth_signature_method": "HMAC-SHA1",
        "oauth_timestamp":        timestamp,
        "oauth_version":          "1.0",
    }
    
    // 构建签名
    baseString := buildSignatureBaseString(
        "POST",
        config.AccessTokenURL,
        oauthParams,
    )
    
    signature := generateSignature(baseString, config.ConsumerSecret, "")
    oauthParams["oauth_signature"] = signature
    
    // 创建请求
    req, err := http.NewRequest("POST", config.AccessTokenURL, nil)
    if err != nil {
        return "", err
    }
    
    req.Header.Set("Authorization", buildOAuthHeader(oauthParams))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    
    // 发送请求
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    // 解析响应
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    
    values, err := url.ParseQuery(string(body))
    if err != nil {
        return "", err
    }
    
    return values.Get("oauth_token"), nil
}

func MakeAuthenticatedRequest(method, urlStr, accessToken string, config OAuth1Config) (*http.Response, error) {
    nonce := generateNonce()
    timestamp := generateTimestamp()
    
    oauthParams := map[string]string{
        "oauth_consumer_key":     config.ConsumerKey,
        "oauth_token":            accessToken,
        "oauth_nonce":            nonce,
        "oauth_signature_method": "HMAC-SHA1",
        "oauth_timestamp":        timestamp,
        "oauth_version":          "1.0",
    }
    
    // 构建签名
    baseString := buildSignatureBaseString(method, urlStr, oauthParams)
    signature := generateSignature(baseString, config.ConsumerSecret, "")
    oauthParams["oauth_signature"] = signature
    
    // 创建请求
    req, err := http.NewRequest(method, urlStr, nil)
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("Authorization", buildOAuthHeader(oauthParams))
    req.Header.Set("Content-Type", "application/json")
    
    client := &http.Client{}
    return client.Do(req)
}

func main() {
    config := OAuth1Config{
        ConsumerKey:     "your_consumer_key",
        ConsumerSecret:  "your_consumer_secret",
        RequestTokenURL: "https://api.etrade.com/oauth/request_token",
        AuthorizeURL:    "https://us.etrade.com/e/t/etws/authorize",
        AccessTokenURL:  "https://api.etrade.com/oauth/access_token",
        CallbackURL:     "oob",
    }
    
    // 获取请求令牌
    requestToken, err := GetRequestToken(config)
    if err != nil {
        fmt.Printf("Error getting request token: %v\n", err)
        return
    }
    
    fmt.Printf("Request Token: %s\n", requestToken)
    fmt.Printf("Authorize URL: %s?key=%s&token=%s\n", 
        config.AuthorizeURL, config.ConsumerKey, requestToken)
    
    // 用户在此处授权后获取验证码
    // verifier := "user_provided_verifier"
    
    // 获取访问令牌
    // accessToken, err := GetAccessToken(config, requestToken, verifier)
    // if err != nil {
    //     fmt.Printf("Error getting access token: %v\n", err)
    //     return
    // }
    
    // 使用访问令牌进行API调用
    // resp, err := MakeAuthenticatedRequest("GET", 
    //     "https://api.etrade.com/v1/accounts/list", 
    //     accessToken, config)
}

关键注意事项:

  1. eTrade API要求使用HMAC-SHA1签名方法
  2. 所有参数必须按字母顺序排序后进行百分比编码
  3. 签名基础字符串的格式为:METHOD&URL&PARAMETERS
  4. 授权头部的格式必须严格符合OAuth1规范
  5. 时间戳和非随机数必须为每个请求重新生成

常见的错误原因包括:

  • 签名基础字符串构建不正确
  • 参数编码不符合规范
  • 时间戳与非随机数重复使用
  • 授权头部格式错误

确保你的Consumer Key和Secret已正确从eTrade开发者门户获取,并且API终端点URL与你的账户区域匹配(美国账户使用api.etrade.com,沙盒环境使用apisb.etrade.com)。

回到顶部