使用 Go 语言实现 Google 的 Oauth2
身份验证是任何应用程序中最常见的部分。您可以实现自己的身份验证系统,也可以使用现有的众多替代方案之一,但在本例中,我们将使用 OAuth2。
OAuth 是一种规范,允许用户委托对其数据的访问权限,而无需与该服务共享其用户名和密码,如果您想了解有关 Oauth2 的更多信息,请点击此处。
配置 Google 项目
首先,我们需要创建我们的 Google 项目并创建 OAuth2 凭据。
- 前往 Google Cloud Platform
- 创建一个新项目或选择一个(如果已有)。
- 转到“凭据”,然后选择“OAuth 客户端 ID”创建一个新的凭据
- 对于此示例,添加“授权重定向 URL”
localhost:8000/auth/google/callback
- 复制client_id和客户端密钥
OAuth2 如何与 Google 配合使用
当您的应用程序将浏览器重定向到 Google 网址时,授权序列就开始了;该网址包含指示所请求访问类型的查询参数。Google 负责处理用户身份验证、会话选择和用户同意。授权结果是授权码,应用程序可以用它来交换访问令牌和刷新令牌。
应用程序应存储刷新令牌以备将来使用,并使用访问令牌访问 Google API。访问令牌过期后,应用程序将使用刷新令牌获取新的访问令牌。
让我们来看看代码
我们将使用“golang.org/x/oauth2”包,该包为进行 OAuth2 授权和身份验证的 HTTP 请求提供支持。
在您的工作目录中创建一个新项目(文件夹),在我的情况下,我将其称为“oauth2-example”,并且我们需要包含oauth2的包。
go get golang.org/x/oauth2
因此我们在项目中创建一个main.go。
package main
import (
"fmt"
"net/http"
"log"
"github.com/douglasmakey/oauth2-example/handlers"
)
func main() {
server := &http.Server{
Addr: fmt.Sprintf(":8000"),
Handler: handlers.New(),
}
log.Printf("Starting HTTP Server. Listening at %q", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("%v", err)
} else {
log.Println("Server closed!")
}
}
我们使用 http.Server 创建一个简单的服务器并运行。
接下来,我们创建包含应用程序处理程序的文件夹“handlers”,在此文件夹中创建“base.go”。
package handlers
import (
"net/http"
)
func New() http.Handler {
mux := http.NewServeMux()
// Root
mux.Handle("/", http.FileServer(http.Dir("templates/")))
// OauthGoogle
mux.HandleFunc("/auth/google/login", oauthGoogleLogin)
mux.HandleFunc("/auth/google/callback", oauthGoogleCallback)
return mux
}
我们使用http.ServeMux来处理我们的端点,接下来我们创建根端点“/”来提供具有最少 HTML 和 CSS 的简单模板,在这个例子中我们使用“http.http.FileServer”,该模板是“index.html”并且位于文件夹“templates”中。
另外,我们为 Google 的 Oauth 创建了两个端点,分别为“/auth/google/login”和“/auth/google/callback”。还记得我们在 Google 控制台中配置应用程序时的操作吗?回调 URL 必须相同。
接下来,我们在处理程序中创建另一个文件,我们将其命名为“oauth_google.go”,该文件包含在我们的应用程序中处理与 Google 的 OAuth 的所有逻辑。
我们通过 auth.Config 声明 var googleOauthConfig 来与 Google 进行通信。
范围:OAuth 2.0 范围提供了一种限制授予访问令牌的访问量的方法。
var googleOauthConfig = &oauth2.Config{
RedirectURL: "http://localhost:8000/auth/google/callback",
ClientID: os.Getenv("GOOGLE_OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: google.Endpoint,
}
处理程序 oauthGoogleLogin
该处理程序创建一个登录链接并将用户重定向到该链接:
AuthCodeURL 接收 state 令牌,用于保护用户免受 CSRF 攻击。您必须始终提供一个非空字符串,并验证其是否与重定向回调中的 state 查询参数匹配。建议每次请求都随机生成此令牌,因此我们使用了一个简单的 cookie。
func oauthGoogleLogin(w http.ResponseWriter, r *http.Request) {
// Create oauthState cookie
oauthState := generateStateOauthCookie(w)
u := googleOauthConfig.AuthCodeURL(oauthState)
http.Redirect(w, r, u, http.StatusTemporaryRedirect)
}
func generateStateOauthCookie(w http.ResponseWriter) string {
var expiration = time.Now().Add(365 * 24 * time.Hour)
b := make([]byte, 16)
rand.Read(b)
state := base64.URLEncoding.EncodeToString(b)
cookie := http.Cookie{Name: "oauthstate", Value: state, Expires: expiration}
http.SetCookie(w, &cookie)
return state
}
处理程序 oauthGoogleCallback
该处理程序检查状态是否等于 oauthStateCookie,并将代码传递给函数getUserDataFromGoogle。
func oauthGoogleCallback(w http.ResponseWriter, r *http.Request) {
// Read oauthState from Cookie
oauthState, _ := r.Cookie("oauthstate")
if r.FormValue("state") != oauthState.Value {
log.Println("invalid oauth google state")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
data, err := getUserDataFromGoogle(r.FormValue("code"))
if err != nil {
log.Println(err.Error())
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
// GetOrCreate User in your db.
// Redirect or response with a token.
// More code .....
fmt.Fprintf(w, "UserInfo: %s\n", data)
}
func getUserDataFromGoogle(code string) ([]byte, error) {
// Use code to get token and get user info from Google.
token, err := googleOauthConfig.Exchange(context.Background(), code)
if err != nil {
return nil, fmt.Errorf("code exchange wrong: %s", err.Error())
}
response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
if err != nil {
return nil, fmt.Errorf("failed getting user info: %s", err.Error())
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed read response: %s", err.Error())
}
return contents, nil
}
完整代码 oauth_google.go
package handlers
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"net/http"
"fmt"
"io/ioutil"
"context"
"log"
"encoding/base64"
"crypto/rand"
"os"
"time"
)
// Scopes: OAuth 2.0 scopes provide a way to limit the amount of access that is granted to an access token.
var googleOauthConfig = &oauth2.Config{
RedirectURL: "http://localhost:8000/auth/google/callback",
ClientID: os.Getenv("GOOGLE_OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: google.Endpoint,
}
const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="
func oauthGoogleLogin(w http.ResponseWriter, r *http.Request) {
// Create oauthState cookie
oauthState := generateStateOauthCookie(w)
/*
AuthCodeURL receive state that is a token to protect the user from CSRF attacks. You must always provide a non-empty string and
validate that it matches the the state query parameter on your redirect callback.
*/
u := googleOauthConfig.AuthCodeURL(oauthState)
http.Redirect(w, r, u, http.StatusTemporaryRedirect)
}
func oauthGoogleCallback(w http.ResponseWriter, r *http.Request) {
// Read oauthState from Cookie
oauthState, _ := r.Cookie("oauthstate")
if r.FormValue("state") != oauthState.Value {
log.Println("invalid oauth google state")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
data, err := getUserDataFromGoogle(r.FormValue("code"))
if err != nil {
log.Println(err.Error())
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
// GetOrCreate User in your db.
// Redirect or response with a token.
// More code .....
fmt.Fprintf(w, "UserInfo: %s\n", data)
}
func generateStateOauthCookie(w http.ResponseWriter) string {
var expiration = time.Now().Add(365 * 24 * time.Hour)
b := make([]byte, 16)
rand.Read(b)
state := base64.URLEncoding.EncodeToString(b)
cookie := http.Cookie{Name: "oauthstate", Value: state, Expires: expiration}
http.SetCookie(w, &cookie)
return state
}
func getUserDataFromGoogle(code string) ([]byte, error) {
// Use code to get token and get user info from Google.
token, err := googleOauthConfig.Exchange(context.Background(), code)
if err != nil {
return nil, fmt.Errorf("code exchange wrong: %s", err.Error())
}
response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
if err != nil {
return nil, fmt.Errorf("failed getting user info: %s", err.Error())
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed read response: %s", err.Error())
}
return contents, nil
}
让我们运行并测试一下
go run main.go
带有代码库的存储库
文章来源:https://dev.to/douglasmakey/oauth2-example-with-go-3n8a