Google OAuth2 是目前最主流的第三方授权方案之一,无论你的应用是网站、移动端还是桌面程序,都可以借助它让用户使用 Google 账号一键登录。在 Go 语言中,官方维护的 golang.org/x/oauth2 包和 google.golang.org/api 提供了完整的支持,整个流程不需要手写太多底层细节。不过要真正跑通一个安全、稳定的登录链路,授权申请、回调处理、state 校验、用户信息解析每一步都有需要注意的地方。本文将从零开始,完整演示 Go 接入 Google OAuth2 并获取用户信息的全过程。

一、前置准备:创建 Google Cloud 凭据
在写代码之前,必须先到 Google Cloud Console 创建一个项目并配置 OAuth 客户端。进入 API 和服务菜单,选择凭据页面,点击创建凭据,选择 OAuth 客户端 ID。应用类型选择 Web 应用,然后配置两个关键信息:授权重定向 URI 和授权 JavaScript 来源。
授权重定向 URI 是整个流程中最容易出错的一项。它必须与你代码中注册的回调地址完全一致,包括协议、域名、端口和路径。比如你本地调试用的是 http://localhost:8080/auth/google/callback,那么在控制台里也要填写这个地址,一个字符都不能差。填错之后 Google 会在授权页面直接报 redirect_uri_mismatch 错误,而不是跳转到你的回调地址。
创建完成后,你会拿到客户端 ID 和客户端密钥。客户端密钥属于敏感信息,切勿写死在代码里提交到仓库,推荐通过环境变量读取。如果是生产环境,最好接入密钥管理服务。准备工作做完,接下来安装依赖:
go get golang.org/x/oauth2 go get google.golang.org/api/option go get google.golang.org/api/oauth2/v2
二、授权码流程的核心实现
Google OAuth2 使用的是标准的授权码模式,整体分为两步:第一步引导用户跳转到 Google 的授权页面,用户同意授权后,Google 会带着一次性授权码重定向回你的回调地址;第二步是服务端用授权码换取 access token,再用 token 去请求用户信息。理解这个两段式流程,是写代码之前必须搞清楚的基础。
首先定义 OAuth2 配置。Go 的 oauth2 包提供了 Endpoint 的预置定义,直接引用即可,不需要手动拼接授权地址和令牌地址:
package main
import (
"context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var googleOAuthConfig = &oauth2.Config{
RedirectURL: "http://localhost:8080/auth/google/callback",
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}Scopes 决定了你能拿到用户的哪些信息。openid、email、profile 是最常用的三个范围,分别对应用户身份标识、邮箱和基本资料。申请的 Scope 越多,Google 在授权页面展示的提示越详细,用户拒绝的可能性也越高,所以按需申请是最佳实践。
接着实现授权跳转接口。这里有一个非常重要的安全细节:state 参数必须使用随机字符串,并在服务端保存,回调时严格比对,用来防止 CSRF 攻击:
func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
// 生成随机 state,存入 cookie 或 session
state, err := generateRandomState()
if err != nil {
http.Error(w, "内部错误", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "oauth_state",
Value: state,
Path: "/",
Expires: time.Now().Add(10 * time.Minute),
HttpOnly: true,
})
url := googleOAuthConfig.AuthCodeURL(state)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func generateRandomState() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}state 的有效期建议设置得短一些,十分钟足够覆盖正常的授权流程。如果把 state 存在 Cookie 中,一定要开启 HttpOnly 属性,避免被脚本读取。生产环境中还建议加上 Secure 属性,强制走 HTTPS 传输。
三、回调处理与用户信息获取
回调接口是整个流程的核心。用户在 Google 页面完成授权后,浏览器会带着 code 和 state 两个参数访问你的回调地址。这里必须先校验 state,再换取 token,顺序不能颠倒:
func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
// 第一步:校验 state
oauthState, err := r.Cookie("oauth_state")
if err != nil {
http.Error(w, "缺少 state", http.StatusBadRequest)
return
}
if r.URL.Query().Get("state") != oauthState.Value {
http.Error(w, "state 校验失败", http.StatusBadRequest)
return
}
// 第二步:用授权码换取 token
code := r.URL.Query().Get("code")
token, err := googleOAuthConfig.Exchange(context.Background(), code)
if err != nil {
http.Error(w, "换取令牌失败", http.StatusInternalServerError)
return
}
// 第三步:用 token 获取用户信息
userInfo, err := getUserInfo(token)
if err != nil {
http.Error(w, "获取用户信息失败", http.StatusInternalServerError)
return
}
// 第四步:创建自己的会话,写入响应
fmt.Fprintf(w, "欢迎,%s(邮箱:%s)", userInfo.Name, userInfo.Email)
}获取用户信息推荐直接使用官方的 oauth2/v2 客户端,它会自动处理鉴权头和响应解析,比手工请求 userinfo 端点更可靠:
func getUserInfo(token *oauth2.Token) (*oauth2_v2.Userinfo, error) {
ctx := context.Background()
tokenSource := googleOAuthConfig.TokenSource(ctx, token)
service, err := oauth2_v2.NewService(ctx, option.WithTokenSource(tokenSource))
if err != nil {
return nil, err
}
return service.Userinfo.Get().Do()
}Userinfo 结构体中包含 Id、Email、Name、Picture、VerifiedEmail 等字段。注意这里的 Id 是 Google 分配的唯一数字标识,与邮箱不同,即使用户修改了邮箱,这个 Id 也不会变,因此数据库中应以 Id 作为关联键,而不是邮箱。
四、Token 管理与会话保持的进阶建议
拿到 token 之后如何维护用户会话,是很多人容易忽略的一环。access token 的有效期通常只有一个小时左右,如果你需要持续调用 Google 的 API(比如读取用户日历),就要利用 refresh token 自动刷新。Go 的 oauth2 包内置了 TokenSource 机制,只要传入实现了 TokenSource 接口的对象,它会在 token 过期时自动刷新:
// 将 token 持久化,并在请求时自动刷新
ts := googleOAuthConfig.TokenSource(ctx, storedToken)
client := oauth2.NewClient(ctx, ts)
// 之后所有通过 client 发出的请求都会自动带 token 并按需刷新
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")对于普通的登录场景,其实不需要长期保存 Google 的 token。更常见的做法是:拿到用户信息后,在数据库中查找或创建用户记录,然后签发自己的会话凭证,比如基于 JWT 的 token 或传统的 session,之后 Google 的 access token 就可以丢弃了。这样架构更简单,也不存在 Google token 泄露的风险。
如果选择 JWT 方案,签发时建议把 Google 的用户 Id 放进 payload 作为 subject,并设置合理的过期时间。同时要注意为 JWT 设计刷新机制,避免用户频繁被登出。另外,无论采用哪种方案,回调地址都必须部署 HTTPS,Google 对生产环境的 OAuth 回调有强制要求,本地调试除外。
总结一下,Go 接入 Google OAuth2 的完整链路包括:控制台创建凭据、生成带 state 的授权链接、回调时校验 state、用授权码换 token、拉取用户信息、建立本地会话。每一步都不复杂,但 state 校验、凭据保管、Id 作为唯一关联键这几个安全细节决定了整套方案是否可靠。把这些要点落实好,一套生产可用的第三方登录体系就搭建完成了。