OAuth如何构建一个即时消息应用

发布时间:2022-01-12 21:37:43 作者:iii
来源:亿速云 阅读:103

这篇“OAuth如何构建一个即时消息应用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“OAuth如何构建一个即时消息应用”文章吧。

社交登录的工作方式十分简单:用户点击链接,然后重定向到 GitHub  授权页面。当用户授予我们对他的个人信息的访问权限之后,就会重定向回登录页面。下一次尝试登录时,系统将不会再次请求授权,也就是说,我们的应用已经记住了这个用户。这使得整个登录流程看起来就和你用鼠标单击一样快。

如果进一步考虑其内部实现的话,过程就会变得复杂起来。首先,我们需要注册一个新的 GitHub OAuth 应用。

这一步中,比较重要的是回调 URL。我们将它设置为 http://localhost:3000/api/oauth/github/callback。这是因为,在开发过程中,我们总是在本地主机上工作。一旦你要将应用交付生产,请使用正确的回调 URL 注册一个新的应用。

注册以后,你将会收到“客户端 id”和“安全密钥”。安全起见,请不要与任何人分享他们 ?

顺便让我们开始写一些代码吧。现在,创建一个 main.go 文件:

package main import (    "database/sql"    "fmt"    "log"    "net/http"    "net/url"    "os"    "strconv"     "github.com/gorilla/securecookie"    "github.com/joho/godotenv"    "github.com/knq/jwt"    _ "github.com/lib/pq"    "github.com/matryer/way"    "golang.org/x/oauth3"    "golang.org/x/oauth3/github") var origin *url.URLvar db *sql.DBvar githubOAuthConfig *oauth3.Configvar cookieSigner *securecookie.SecureCookievar jwtSigner jwt.Signer func main() {    godotenv.Load()     port := intEnv("PORT", 3000)    originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))    databaseURL := env("DATABASE_URL", "postgresql://root@127.0.0.1:26257/messenger?sslmode=disable")    githubClientID := os.Getenv("GITHUB_CLIENT_ID")    githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")    hashKey := env("HASH_KEY", "secret")    jwtKey := env("JWT_KEY", "secret")     var err error    if origin, err = url.Parse(originString); err != nil || !origin.IsAbs() {        log.Fatal("invalid origin")        return    }     if i, err := strconv.Atoi(origin.Port()); err == nil {        port = i    }     if githubClientID == "" || githubClientSecret == "" {        log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")        return    }     if db, err = sql.Open("postgres", databaseURL); err != nil {        log.Fatalf("could not open database connection: %v\n", err)        return    }    defer db.Close()    if err = db.Ping(); err != nil {        log.Fatalf("could not ping to db: %v\n", err)        return    }     githubRedirectURL := *origin    githubRedirectURL.Path = "/api/oauth/github/callback"    githubOAuthConfig = &oauth3.Config{        ClientID:     githubClientID,        ClientSecret: githubClientSecret,        Endpoint:     github.Endpoint,        RedirectURL:  githubRedirectURL.String(),        Scopes:       []string{"read:user"},    }     cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)     jwtSigner, err = jwt.HS256.New([]byte(jwtKey))    if err != nil {        log.Fatalf("could not create JWT signer: %v\n", err)        return    }     router := way.NewRouter()    router.HandleFunc("GET", "/api/oauth/github", githubOAuthStart)    router.HandleFunc("GET", "/api/oauth/github/callback", githubOAuthCallback)    router.HandleFunc("GET", "/api/auth_user", guard(getAuthUser))     log.Printf("accepting connections on port %d\n", port)    log.Printf("starting server at %s\n", origin.String())    addr := fmt.Sprintf(":%d", port)    if err = http.ListenAndServe(addr, router); err != nil {        log.Fatalf("could not start server: %v\n", err)    }} func env(key, fallbackValue string) string {    v, ok := os.LookupEnv(key)    if !ok {        return fallbackValue    }    return v} func intEnv(key string, fallbackValue int) int {    v, ok := os.LookupEnv(key)    if !ok {        return fallbackValue    }    i, err := strconv.Atoi(v)    if err != nil {        return fallbackValue    }    return i}

安装依赖项:

go get -u github.com/gorilla/securecookiego get -u github.com/joho/godotenvgo get -u github.com/knq/jwtgo get -u github.com/lib/pqge get -u github.com/matoous/go-nanoidgo get -u github.com/matryer/waygo get -u golang.org/x/oauth3

我们将会使用 .env 文件来保存密钥和其他配置。请创建这个文件,并保证里面至少包含以下内容:

GITHUB_CLIENT_ID=your_github_client_idGITHUB_CLIENT_SECRET=your_github_client_secret

我们还要用到的其他环境变量有:

因为代码中已经设定了默认值,所以你也不用把它们写到 .env 文件中。

在读取配置并连接到数据库之后,我们会创建一个 OAuth 配置。我们会使用 ORIGIN 信息来构建回调  URL(就和我们在 GitHub 页面上注册的一样)。我们的数据范围设置为  “read:user”。这会允许我们读取公开的用户信息,这里我们只需要他的用户名和头像就够了。然后我们会初始化 cookie 和 JWT  签名器。定义一些端点并启动服务器。

在实现 HTTP 处理程序之前,让我们编写一些函数来发送 HTTP 响应。

func respond(w http.ResponseWriter, v interface{}, statusCode int) {    b, err := json.Marshal(v)    if err != nil {        respondError(w, fmt.Errorf("could not marshal response: %v", err))        return    }    w.Header().Set("Content-Type", "application/json; charset=utf-8")    w.WriteHeader(statusCode)    w.Write(b)} func respondError(w http.ResponseWriter, err error) {    log.Println(err)    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)}

第一个函数用来发送 JSON,而第二个将错误记录到控制台并返回一个 500 Internal Server Error 错误信息。

OAuth 开始

所以,用户点击写着 “Access with GitHub” 的链接。该链接指向 /api/oauth/github,这将会把用户重定向到 github。

func githubOAuthStart(w http.ResponseWriter, r *http.Request) {    state, err := gonanoid.Nanoid()    if err != nil {        respondError(w, fmt.Errorf("could not generte state: %v", err))        return    }     stateCookieValue, err := cookieSigner.Encode("state", state)    if err != nil {        respondError(w, fmt.Errorf("could not encode state cookie: %v", err))        return    }     http.SetCookie(w, &http.Cookie{        Name:     "state",        Value:    stateCookieValue,        Path:     "/api/oauth/github",        HttpOnly: true,    })    http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)}

OAuth3 使用一种机制来防止 CSRF ,因此它需要一个“状态”(state)。我们使用 Nanoid() 来创建一个随机字符串,并用这个字符串作为状态。我们也把它保存为一个 cookie。

OAuth 回调

一旦用户授权我们访问他的个人信息,他将会被重定向到这个端点。这个 URL 的查询字符串上将会包含状态(state)和授权码(code): /api/oauth/github/callback?state=&code=

const jwtLifetime = time.Hour * 24 * 14 type GithubUser struct {    ID        int     `json:"id"`    Login     string  `json:"login"`    AvatarURL *string `json:"avatar_url,omitempty"`} type User struct {    ID        string  `json:"id"`    Username  string  `json:"username"`    AvatarURL *string `json:"avatarUrl"`} func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {    stateCookie, err := r.Cookie("state")    if err != nil {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    }     http.SetCookie(w, &http.Cookie{        Name:     "state",        Value:    "",        MaxAge:   -1,        HttpOnly: true,    })     var state string    if err = cookieSigner.Decode("state", stateCookie.Value, &state); err != nil {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    }     q := r.URL.Query()     if state != q.Get("state") {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    }     ctx := r.Context()     t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))    if err != nil {        respondError(w, fmt.Errorf("could not fetch github token: %v", err))        return    }     client := githubOAuthConfig.Client(ctx, t)    resp, err := client.Get("https://api.github.com/user")    if err != nil {        respondError(w, fmt.Errorf("could not fetch github user: %v", err))        return    }     var githubUser GithubUser    if err = json.NewDecoder(resp.Body).Decode(&githubUser); err != nil {        respondError(w, fmt.Errorf("could not decode github user: %v", err))        return    }    defer resp.Body.Close()     tx, err := db.BeginTx(ctx, nil)    if err != nil {        respondError(w, fmt.Errorf("could not begin tx: %v", err))        return    }     var user User    if err = tx.QueryRowContext(ctx, `        SELECT id, username, avatar_url FROM users WHERE github_id = $1    `, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {        if err = tx.QueryRowContext(ctx, `            INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3)            RETURNING id        `, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err != nil {            respondError(w, fmt.Errorf("could not insert user: %v", err))            return        }        user.Username = githubUser.Login        user.AvatarURL = githubUser.AvatarURL    } else if err != nil {        respondError(w, fmt.Errorf("could not query user by github ID: %v", err))        return    }     if err = tx.Commit(); err != nil {        respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))        return    }     exp := time.Now().Add(jwtLifetime)    token, err := jwtSigner.Encode(jwt.Claims{        Subject:    user.ID,        Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),    })    if err != nil {        respondError(w, fmt.Errorf("could not create token: %v", err))        return    }     expiresAt, _ := exp.MarshalText()     data := make(url.Values)    data.Set("token", string(token))    data.Set("expires_at", string(expiresAt))     http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)}

首先,我们会尝试使用之前保存的状态对 cookie 进行解码。并将其与查询字符串中的状态进行比较。如果它们不匹配,我们会返回一个 418 I'm teapot(未知来源)错误。

接着,我们使用授权码生成一个令牌。这个令牌被用于创建 HTTP 客户端来向 GitHub API 发出请求。所以最终我们会向 https://api.github.com/user 发送一个 GET 请求。这个端点将会以 JSON 格式向我们提供当前经过身份验证的用户信息。我们将会解码这些内容,一并获取用户的 ID、登录名(用户名)和头像 URL。

然后我们将会尝试在数据库上找到具有该 GitHub ID 的用户。如果没有找到,就使用该数据创建一个新的。

之后,对于新创建的用户,我们会发出一个将用户 ID 作为主题(Subject)的 JSON 网络令牌,并使用该令牌重定向到前端,查询字符串中一并包含该令牌的到期日(Expiration)。

这一 Web 应用也会被用在其他帖子,但是重定向的链接会是 /callback?token=&expires_at=。在那里,我们将会利用 JavaScript 从 URL 中获取令牌和到期日,并通过 Authorization 标头中的令牌以 Bearer token_here 的形式对 /api/auth_user 进行 GET 请求,来获取已认证的身份用户并将其保存到 localStorage。

Guard 中间件

为了获取当前已经过身份验证的用户,我们设计了 Guard 中间件。这是因为在接下来的文章中,我们会有很多需要进行身份认证的端点,而中间件将会允许我们共享这一功能。

type ContextKey struct {    Name string} var keyAuthUserID = ContextKey{"auth_user_id"} func guard(handler http.HandlerFunc) http.HandlerFunc {    return func(w http.ResponseWriter, r *http.Request) {        var token string        if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {            token = a[7:]        } else if t := r.URL.Query().Get("token"); t != "" {            token = t        } else {            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)            return        }         var claims jwt.Claims        if err := jwtSigner.Decode([]byte(token), &claims); err != nil {            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)            return        }         ctx := r.Context()        ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)         handler(w, r.WithContext(ctx))    }}

首先,我们尝试从 Authorization 标头或者是 URL 查询字符串中的 token 字段中读取令牌。如果没有找到,我们需要返回 401 Unauthorized(未授权)错误。然后我们将会对令牌中的申明进行解码,并使用该主题作为当前已经过身份验证的用户 ID。

现在,我们可以用这一中间件来封装任何需要授权的 http.handlerFunc,并且在处理函数的上下文中保有已经过身份验证的用户 ID。

var guarded = guard(func(w http.ResponseWriter, r *http.Request) {    authUserID := r.Context().Value(keyAuthUserID).(string)})

获取认证用户

func getAuthUser(w http.ResponseWriter, r *http.Request) {    ctx := r.Context()    authUserID := ctx.Value(keyAuthUserID).(string)     var user User    if err := db.QueryRowContext(ctx, `        SELECT username, avatar_url FROM users WHERE id = $1    `, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    } else if err != nil {        respondError(w, fmt.Errorf("could not query auth user: %v", err))        return    }     user.ID = authUserID     respond(w, user, http.StatusOK)}

我们使用 Guard 中间件来获取当前经过身份认证的用户 ID 并查询数据库。

以上就是关于“OAuth如何构建一个即时消息应用”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注亿速云行业资讯频道。

推荐阅读:
  1. 使用vue怎么构建一个多页面应用
  2. Android中怎么构建一个Material Design应用

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

oauth

上一篇:java基础环境识怎样的

下一篇:软件测试实例分析

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》