使用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> `
错误原因分析
- 客户端ID/密钥未正确加载:代码用
os.Getenv获取环境变量,但如果变量未设置或名称拼写错误,会传入空值给Yahoo provider,触发"invalid client id"错误。 - 回调URL不匹配:Yahoo开发者控制台配置的回调URL必须和代码里的完全一致,包括协议、域名、路径,大小写和斜杠都不能错。
- 无效的OpenID Connect配置:代码里加载了未配置的OpenID Connect provider,参数都是无效字符串,可能干扰Yahoo provider的正常工作。
- Session密钥设置错误:
os.Setenv返回的是错误值,代码逻辑搞反了,且Gothic依赖Session存储,未正确配置会导致授权流程异常。 - 模板路由链接错误:模板生成的授权链接是
/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
相关产品推荐
相关产品推荐

