You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Golang与Gothic对接Yahoo Fantasy Sports API遇OAuth2客户端ID错误

解决Yahoo Fantasy Sports API OAuth2授权时"invalid client id"错误

问题说明

我要开发一个调用Yahoo Fantasy Sports API的Web应用,用Golang配合Gothic包实现OAuth2授权。已经拿到客户端ID和密钥,但登录时出现&error=unauthorized_client&error_description=invalid+client+id错误,原代码如下:

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
    "os"
    "sort"

    "github.com/gorilla/pat"
    "github.com/markbates/goth"
    "github.com/markbates/goth/gothic"
    "github.com/markbates/goth/providers/openidConnect"
    "github.com/markbates/goth/providers/yahoo"
)

func main() {
    sessionSecret := os.Setenv("SESSION_SECRET","default_secret_value")
    if sessionSecret != nil {
        fmt.Println(sessionSecret)
    }
    

    goth.UseProviders(
        // Pointed localhost.com to http://localhost:3000/auth/yahoo/callback through proxy as yahoo
        // does not allow to put custom ports in redirection uri
        yahoo.New(os.Getenv("YAHOO_CLIENT_ID"), os.Getenv("YAHOO_SECRET_KEY"), "https://myurlname.com/auth/openid-connect/callback"),
    )
    // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html)
    // because the OpenID Connect provider initialize itself in the New(), it can return an error which should be handled or ignored
    // ignore the error for now
    openidConnect, _ := openidConnect.New(os.Getenv("OPENID_CONNECT_KEY"), os.Getenv("I don't actually have anything hooked up to this open id connect secret, I have tried using my client secret and it didn't work OPENID_CONNECT_SECRET"), "https://MYURLNAME/auth/openid-connect/callback", os.Getenv("I dont have this as an eviroment variable but i left it in anyways, normally this will probably be nil OPENID_CONNECT_DISCOVERY_URL"))
    if openidConnect != nil {
        goth.UseProviders(openidConnect)
    }

    m := map[string]string{
        "yahoo": "Yahoo",
    }
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    providerIndex := &ProviderIndex{Providers: keys, ProvidersMap: m}

    p := pat.New()
    p.Get("/auth/{provider}/callback", func(res http.ResponseWriter, req *http.Request) {

        user, err := gothic.CompleteUserAuth(res, req)
        if err != nil {
            fmt.Fprintln(res, err)
            return
        }
        t, _ := template.New("foo").Parse(userTemplate)
        t.Execute(res, user)
    })

    p.Get("/logout/{provider}", func(res http.ResponseWriter, req *http.Request) {
        gothic.Logout(res, req)
        res.Header().Set("Location", "/")
        res.WriteHeader(http.StatusTemporaryRedirect)
    })

    p.Get("/auth/{provider}", func(res http.ResponseWriter, req *http.Request) {
        // try to get the user without re-authenticating
        if gothUser, err := gothic.CompleteUserAuth(res, req); err == nil {
            t, _ := template.New("foo").Parse(userTemplate)
            t.Execute(res, gothUser)
        } else {
            gothic.BeginAuthHandler(res, req)
        }
    })

    p.Get("/", func(res http.ResponseWriter, req *http.Request) {
        t, _ := template.New("foo").Parse(indexTemplate)
        t.Execute(res, providerIndex)
    })

    log.Println("listening on localhost:3000")
    log.Fatal(http.ListenAndServe(":3000", p))
}

type ProviderIndex struct {
    Providers    []string
    ProvidersMap map[string]string
}

var indexTemplate = `{{range $key,$value:=.Providers}}
        <p><a href="/auth/{{$value}}">Log in with {{index $.ProvidersMap $value}}</a></p>
    {{end}}`

var userTemplate = `
    <p><a href="/logout/{{.Provider}}">logout</a></p>
    <p>Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]</p>
    <p>Email: {{.Email}}</p>
    <p>NickName: {{.NickName}}</p>
    <p>Location: {{.Location}}</p>
    <p>AvatarURL: {{.AvatarURL}} <img src="{{.AvatarURL}}"></p>
    <p>Description: {{.Description}}</p>
    <p>UserID: {{.UserID}}</p>
    <p>AccessToken: {{.AccessToken}}</p>
    <p>ExpiresAt: {{.ExpiresAt}}</p>
    <p>RefreshToken: {{.RefreshToken}}</p>
    `

错误原因分析

  1. 客户端ID/密钥未正确加载:代码用os.Getenv获取环境变量,但如果变量未设置或名称拼写错误,会传入空值给Yahoo provider,触发"invalid client id"错误。
  2. 回调URL不匹配:Yahoo开发者控制台配置的回调URL必须和代码里的完全一致,包括协议、域名、路径,大小写和斜杠都不能错。
  3. 无效的OpenID Connect配置:代码里加载了未配置的OpenID Connect provider,参数都是无效字符串,可能干扰Yahoo provider的正常工作。
  4. Session密钥设置错误:os.Setenv返回的是错误值,代码逻辑搞反了,且Gothic依赖Session存储,未正确配置会导致授权流程异常。
  5. 模板路由链接错误:模板生成的授权链接是/auth/Yahoo,但路由定义的是/auth/{provider}(对应小写yahoo),路由不匹配可能间接引发授权错误。

修复步骤

1. 正确设置环境变量

运行程序前,在终端设置真实的Yahoo客户端信息和Session密钥:

export YAHOO_CLIENT_ID="你的Yahoo客户端ID"
export YAHOO_SECRET_KEY="你的Yahoo客户端密钥"
export SESSION_SECRET="随机生成的安全密钥(生产环境务必用复杂值)"

2. 修正回调URL

确保Yahoo开发者控制台里的回调URL和代码中yahoo.New的第三个参数完全一致,比如代码写的是https://myurlname.com/auth/yahoo/callback,控制台必须一模一样。

3. 移除无效的OpenID Connect配置

删掉代码中关于openidConnect的所有代码,未配置的服务留着只会引发冲突。

4. 修正Session密钥初始化逻辑

把原来的Session密钥设置代码改成正确逻辑,并配置Gothic的Session存储:

// 正确处理Session密钥
sessionSecret := os.Getenv("SESSION_SECRET")
if sessionSecret == "" {
    sessionSecret = "default_secret_value" // 生产环境务必更换为安全的随机密钥
    os.Setenv("SESSION_SECRET", sessionSecret)
}
// 配置Gothic使用的Session存储
gothic.Store = sessions.NewCookieStore([]byte(sessionSecret))

5. 修正模板路由链接

修改indexTemplate,用$key生成正确的路由路径:

var indexTemplate = `{{range $key,$value:=.Providers}}
        <p><a href="/auth/{{$key}}">Log in with {{index $.ProvidersMap $key}}</a></p>
    {{end}}`

修正后的完整代码

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
    "os"
    "sort"

    "github.com/gorilla/pat"
    "github.com/gorilla/sessions"
    "github.com/markbates/goth"
    "github.com/markbates/goth/gothic"
    "github.com/markbates/goth/providers/yahoo"
)

func main() {
    // 正确处理Session密钥
    sessionSecret := os.Getenv("SESSION_SECRET")
    if sessionSecret == "" {
        sessionSecret = "default_secret_value" // 生产环境务必更换为安全的随机密钥
        os.Setenv("SESSION_SECRET", sessionSecret)
    }
    // 配置Gothic使用的Session存储
    gothic.Store = sessions.NewCookieStore([]byte(sessionSecret))

    // 加载Yahoo Provider,确保环境变量已正确设置
    yahooClientID := os.Getenv("YAHOO_CLIENT_ID")
    yahooSecret := os.Getenv("YAHOO_SECRET_KEY")
    if yahooClientID == "" || yahooSecret == "" {
        log.Fatal("请设置YAHOO_CLIENT_ID和YAHOO_SECRET_KEY环境变量")
    }

    goth.UseProviders(
        yahoo.New(yahooClientID, yahooSecret, "https://myurlname.com/auth/yahoo/callback"),
    )

    m := map[string]string{
        "yahoo": "Yahoo",
    }
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    providerIndex := &ProviderIndex{Providers: keys, ProvidersMap: m}

    p := pat.New()
    p.Get("/auth/{provider}/callback", func(res http.ResponseWriter, req *http.Request) {
        user, err := gothic.CompleteUserAuth(res, req)
        if err != nil {
            fmt.Fprintln(res, err)
            return
        }
        t, _ := template.New("foo").Parse(userTemplate)
        t.Execute(res, user)
    })

    p.Get("/logout/{provider}", func(res http.ResponseWriter, req *http.Request) {
        gothic.Logout(res, req)
        res.Header().Set("Location", "/")
        res.WriteHeader(http.StatusTemporaryRedirect)
    })

    p.Get("/auth/{provider}", func(res http.ResponseWriter, req *http.Request) {
        if gothUser, err := gothic.CompleteUserAuth(res, req); err == nil {
            t, _ := template.New("foo").Parse(userTemplate)
            t.Execute(res, gothUser)
        } else {
            gothic.BeginAuthHandler(res, req)
        }
    })

    p.Get("/", func(res http.ResponseWriter, req *http.Request) {
        t, _ := template.New("foo").Parse(indexTemplate)
        t.Execute(res, providerIndex)
    })

    log.Println("listening on localhost:3000")
    log.Fatal(http.ListenAndServe(":3000", p))
}

type ProviderIndex struct {
    Providers    []string
    ProvidersMap map[string]string
}

// 修正模板中的链接,使用$key来生成正确的路由路径
var indexTemplate = `{{range $key,$value:=.Providers}}
        <p><a href="/auth/{{$key}}">Log in with {{index $.ProvidersMap $key}}</a></p>
    {{end}}`

var userTemplate = `
    <p><a href="/logout/{{.Provider}}">logout</a></p>
    <p>Name: {{.Name}} [{{.LastName}}, {{.FirstName}}]</p>
    <p>Email: {{.Email}}</p>
    <p>NickName: {{.NickName}}</p>
    <p>Location: {{.Location}}</p>
    <p>AvatarURL: {{.AvatarURL}} <img src="{{.AvatarURL}}"></p>
    <p>Description: {{.Description}}</p>
    <p>UserID: {{.UserID}}</p>
    <p>AccessToken: {{.AccessToken}}</p>
    <p>ExpiresAt: {{.ExpiresAt}}</p>
    <p>RefreshToken: {{.RefreshToken}}</p>
    `

内容的提问来源于stack exchange,提问作者Mason Hill

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 22:38:05