mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-02-03 23:40:14 +08:00
PRF
@PsiACE
This commit is contained in:
parent
35a5046571
commit
b3231d7d59
@ -10,7 +10,7 @@
|
|||||||
构建一个即时消息应用(二):OAuth
|
构建一个即时消息应用(二):OAuth
|
||||||
======
|
======
|
||||||
|
|
||||||
[上一篇:模式](https://linux.cn/article-11396-1.html),[原文][1]。
|
[上一篇:模式](https://linux.cn/article-11396-1.html)。
|
||||||
|
|
||||||
在这篇帖子中,我们将会通过为应用添加社交登录功能进入后端开发。
|
在这篇帖子中,我们将会通过为应用添加社交登录功能进入后端开发。
|
||||||
|
|
||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
这一步中,比较重要的是回调 URL。我们将它设置为 `http://localhost:3000/api/oauth/github/callback`。这是因为,在开发过程中,我们总是在本地主机上工作。一旦你要将应用交付生产,请使用正确的回调 URL 注册一个新的应用。
|
这一步中,比较重要的是回调 URL。我们将它设置为 `http://localhost:3000/api/oauth/github/callback`。这是因为,在开发过程中,我们总是在本地主机上工作。一旦你要将应用交付生产,请使用正确的回调 URL 注册一个新的应用。
|
||||||
|
|
||||||
注册以后,你将会收到「客户端 id」和「安全密钥」。安全起见,请不要与任何人分享他们 👀
|
注册以后,你将会收到“客户端 id”和“安全密钥”。安全起见,请不要与任何人分享他们 👀
|
||||||
|
|
||||||
顺便让我们开始写一些代码吧。现在,创建一个 `main.go` 文件:
|
顺便让我们开始写一些代码吧。现在,创建一个 `main.go` 文件:
|
||||||
|
|
||||||
@ -28,21 +28,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/knq/jwt"
|
"github.com/knq/jwt"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"github.com/matryer/way"
|
"github.com/matryer/way"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/github"
|
"golang.org/x/oauth2/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
var origin *url.URL
|
var origin *url.URL
|
||||||
@ -52,90 +52,90 @@ var cookieSigner *securecookie.SecureCookie
|
|||||||
var jwtSigner jwt.Signer
|
var jwtSigner jwt.Signer
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
|
|
||||||
port := intEnv("PORT", 3000)
|
port := intEnv("PORT", 3000)
|
||||||
originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))
|
originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))
|
||||||
databaseURL := env("DATABASE_URL", "postgresql://root@127.0.0.1:26257/messenger?sslmode=disable")
|
databaseURL := env("DATABASE_URL", "postgresql://root@127.0.0.1:26257/messenger?sslmode=disable")
|
||||||
githubClientID := os.Getenv("GITHUB_CLIENT_ID")
|
githubClientID := os.Getenv("GITHUB_CLIENT_ID")
|
||||||
githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
|
githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
|
||||||
hashKey := env("HASH_KEY", "secret")
|
hashKey := env("HASH_KEY", "secret")
|
||||||
jwtKey := env("JWT_KEY", "secret")
|
jwtKey := env("JWT_KEY", "secret")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if origin, err = url.Parse(originString); err != nil || !origin.IsAbs() {
|
if origin, err = url.Parse(originString); err != nil || !origin.IsAbs() {
|
||||||
log.Fatal("invalid origin")
|
log.Fatal("invalid origin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if i, err := strconv.Atoi(origin.Port()); err == nil {
|
if i, err := strconv.Atoi(origin.Port()); err == nil {
|
||||||
port = i
|
port = i
|
||||||
}
|
}
|
||||||
|
|
||||||
if githubClientID == "" || githubClientSecret == "" {
|
if githubClientID == "" || githubClientSecret == "" {
|
||||||
log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")
|
log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if db, err = sql.Open("postgres", databaseURL); err != nil {
|
if db, err = sql.Open("postgres", databaseURL); err != nil {
|
||||||
log.Fatalf("could not open database connection: %v\n", err)
|
log.Fatalf("could not open database connection: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
if err = db.Ping(); err != nil {
|
if err = db.Ping(); err != nil {
|
||||||
log.Fatalf("could not ping to db: %v\n", err)
|
log.Fatalf("could not ping to db: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
githubRedirectURL := *origin
|
githubRedirectURL := *origin
|
||||||
githubRedirectURL.Path = "/api/oauth/github/callback"
|
githubRedirectURL.Path = "/api/oauth/github/callback"
|
||||||
githubOAuthConfig = &oauth2.Config{
|
githubOAuthConfig = &oauth2.Config{
|
||||||
ClientID: githubClientID,
|
ClientID: githubClientID,
|
||||||
ClientSecret: githubClientSecret,
|
ClientSecret: githubClientSecret,
|
||||||
Endpoint: github.Endpoint,
|
Endpoint: github.Endpoint,
|
||||||
RedirectURL: githubRedirectURL.String(),
|
RedirectURL: githubRedirectURL.String(),
|
||||||
Scopes: []string{"read:user"},
|
Scopes: []string{"read:user"},
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)
|
cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)
|
||||||
|
|
||||||
jwtSigner, err = jwt.HS256.New([]byte(jwtKey))
|
jwtSigner, err = jwt.HS256.New([]byte(jwtKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("could not create JWT signer: %v\n", err)
|
log.Fatalf("could not create JWT signer: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
router := way.NewRouter()
|
router := way.NewRouter()
|
||||||
router.HandleFunc("GET", "/api/oauth/github", githubOAuthStart)
|
router.HandleFunc("GET", "/api/oauth/github", githubOAuthStart)
|
||||||
router.HandleFunc("GET", "/api/oauth/github/callback", githubOAuthCallback)
|
router.HandleFunc("GET", "/api/oauth/github/callback", githubOAuthCallback)
|
||||||
router.HandleFunc("GET", "/api/auth_user", guard(getAuthUser))
|
router.HandleFunc("GET", "/api/auth_user", guard(getAuthUser))
|
||||||
|
|
||||||
log.Printf("accepting connections on port %d\n", port)
|
log.Printf("accepting connections on port %d\n", port)
|
||||||
log.Printf("starting server at %s\n", origin.String())
|
log.Printf("starting server at %s\n", origin.String())
|
||||||
addr := fmt.Sprintf(":%d", port)
|
addr := fmt.Sprintf(":%d", port)
|
||||||
if err = http.ListenAndServe(addr, router); err != nil {
|
if err = http.ListenAndServe(addr, router); err != nil {
|
||||||
log.Fatalf("could not start server: %v\n", err)
|
log.Fatalf("could not start server: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func env(key, fallbackValue string) string {
|
func env(key, fallbackValue string) string {
|
||||||
v, ok := os.LookupEnv(key)
|
v, ok := os.LookupEnv(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fallbackValue
|
return fallbackValue
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func intEnv(key string, fallbackValue int) int {
|
func intEnv(key string, fallbackValue int) int {
|
||||||
v, ok := os.LookupEnv(key)
|
v, ok := os.LookupEnv(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fallbackValue
|
return fallbackValue
|
||||||
}
|
}
|
||||||
i, err := strconv.Atoi(v)
|
i, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fallbackValue
|
return fallbackValue
|
||||||
}
|
}
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -163,30 +163,30 @@ GITHUB_CLIENT_SECRET=your_github_client_secret
|
|||||||
* `PORT`:服务器运行的端口,默认值是 `3000`。
|
* `PORT`:服务器运行的端口,默认值是 `3000`。
|
||||||
* `ORIGIN`:你的域名,默认值是 `http://localhost:3000/`。我们也可以在这里指定端口。
|
* `ORIGIN`:你的域名,默认值是 `http://localhost:3000/`。我们也可以在这里指定端口。
|
||||||
* `DATABASE_URL`:Cockroach 数据库的地址。默认值是 `postgresql://root@127.0.0.1:26257/messenger?sslmode=disable`。
|
* `DATABASE_URL`:Cockroach 数据库的地址。默认值是 `postgresql://root@127.0.0.1:26257/messenger?sslmode=disable`。
|
||||||
* `HASH_KEY`:用于为 cookies 签名的密钥。没错,我们会使用已签名的 cookies 来确保安全。
|
* `HASH_KEY`:用于为 cookie 签名的密钥。没错,我们会使用已签名的 cookie 来确保安全。
|
||||||
* `JWT_KEY`:用于签署 JSON 网络令牌(Json Web Token)的密钥。
|
* `JWT_KEY`:用于签署 JSON <ruby>网络令牌<rt>Web Token</rt></ruby>的密钥。
|
||||||
|
|
||||||
因为代码中已经设定了默认值,所以你也不用把它们写到 `.env` 文件中。
|
因为代码中已经设定了默认值,所以你也不用把它们写到 `.env` 文件中。
|
||||||
|
|
||||||
在读取配置并连接到数据库之后,我们会创建一个 OAuth 配置。我们会使用 `ORIGIN` 来构建回调 URL(就和我们在 GitHub 页面上注册的一样)。我们的数据范围设置为 “read:user”。这会允许我们读取公开的用户信息,这里我们只需要他的用户名和头像就够了。然后我们会初始化 cookie 和 JWT 签名器。定义一些端点并启动服务器。
|
在读取配置并连接到数据库之后,我们会创建一个 OAuth 配置。我们会使用 `ORIGIN` 信息来构建回调 URL(就和我们在 GitHub 页面上注册的一样)。我们的数据范围设置为 “read:user”。这会允许我们读取公开的用户信息,这里我们只需要他的用户名和头像就够了。然后我们会初始化 cookie 和 JWT 签名器。定义一些端点并启动服务器。
|
||||||
|
|
||||||
在实现 HTTP 处理程序之前,让我们编写一些函数来发送 HTTP 响应。
|
在实现 HTTP 处理程序之前,让我们编写一些函数来发送 HTTP 响应。
|
||||||
|
|
||||||
```
|
```
|
||||||
func respond(w http.ResponseWriter, v interface{}, statusCode int) {
|
func respond(w http.ResponseWriter, v interface{}, statusCode int) {
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not marshal response: %v", err))
|
respondError(w, fmt.Errorf("could not marshal response: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
w.Write(b)
|
w.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func respondError(w http.ResponseWriter, err error) {
|
func respondError(w http.ResponseWriter, err error) {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -198,156 +198,156 @@ func respondError(w http.ResponseWriter, err error) {
|
|||||||
|
|
||||||
```
|
```
|
||||||
func githubOAuthStart(w http.ResponseWriter, r *http.Request) {
|
func githubOAuthStart(w http.ResponseWriter, r *http.Request) {
|
||||||
state, err := gonanoid.Nanoid()
|
state, err := gonanoid.Nanoid()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not generte state: %v", err))
|
respondError(w, fmt.Errorf("could not generte state: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCookieValue, err := cookieSigner.Encode("state", state)
|
stateCookieValue, err := cookieSigner.Encode("state", state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not encode state cookie: %v", err))
|
respondError(w, fmt.Errorf("could not encode state cookie: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "state",
|
Name: "state",
|
||||||
Value: stateCookieValue,
|
Value: stateCookieValue,
|
||||||
Path: "/api/oauth/github",
|
Path: "/api/oauth/github",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
})
|
})
|
||||||
http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
OAuth2 使用一种机制来防止 CSRF 攻击,因此它需要一个「状态」 "state"。我们使用 `Nanoid()` 来创建一个随机字符串,并用这个字符串作为状态。我们也把它保存为一个 cookie。
|
OAuth2 使用一种机制来防止 CSRF 攻击,因此它需要一个“状态”(`state`)。我们使用 `Nanoid()` 来创建一个随机字符串,并用这个字符串作为状态。我们也把它保存为一个 cookie。
|
||||||
|
|
||||||
### OAuth 回调
|
### OAuth 回调
|
||||||
|
|
||||||
一旦用户授权我们访问他的个人信息,他将会被重定向到这个端点。这个 URL 的查询字符串上将会包含状态(state)和授权码(code) `/api/oauth/github/callback?state=&code=`
|
一旦用户授权我们访问他的个人信息,他将会被重定向到这个端点。这个 URL 的查询字符串上将会包含状态(`state`)和授权码(`code`): `/api/oauth/github/callback?state=&code=`。
|
||||||
|
|
||||||
```
|
```
|
||||||
const jwtLifetime = time.Hour * 24 * 14
|
const jwtLifetime = time.Hour * 24 * 14
|
||||||
|
|
||||||
type GithubUser struct {
|
type GithubUser struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
AvatarURL *string `json:"avatarUrl"`
|
AvatarURL *string `json:"avatarUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
stateCookie, err := r.Cookie("state")
|
stateCookie, err := r.Cookie("state")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "state",
|
Name: "state",
|
||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
var state string
|
var state string
|
||||||
if err = cookieSigner.Decode("state", stateCookie.Value, &state); err != nil {
|
if err = cookieSigner.Decode("state", stateCookie.Value, &state); err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
|
|
||||||
if state != q.Get("state") {
|
if state != q.Get("state") {
|
||||||
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))
|
t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not fetch github token: %v", err))
|
respondError(w, fmt.Errorf("could not fetch github token: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := githubOAuthConfig.Client(ctx, t)
|
client := githubOAuthConfig.Client(ctx, t)
|
||||||
resp, err := client.Get("https://api.github.com/user")
|
resp, err := client.Get("https://api.github.com/user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not fetch github user: %v", err))
|
respondError(w, fmt.Errorf("could not fetch github user: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var githubUser GithubUser
|
var githubUser GithubUser
|
||||||
if err = json.NewDecoder(resp.Body).Decode(&githubUser); err != nil {
|
if err = json.NewDecoder(resp.Body).Decode(&githubUser); err != nil {
|
||||||
respondError(w, fmt.Errorf("could not decode github user: %v", err))
|
respondError(w, fmt.Errorf("could not decode github user: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
tx, err := db.BeginTx(ctx, nil)
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
respondError(w, fmt.Errorf("could not begin tx: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user User
|
var user User
|
||||||
if err = tx.QueryRowContext(ctx, `
|
if err = tx.QueryRowContext(ctx, `
|
||||||
SELECT id, username, avatar_url FROM users WHERE github_id = $1
|
SELECT id, username, avatar_url FROM users WHERE github_id = $1
|
||||||
`, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {
|
`, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {
|
||||||
if err = tx.QueryRowContext(ctx, `
|
if err = tx.QueryRowContext(ctx, `
|
||||||
INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3)
|
INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err != nil {
|
`, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err != nil {
|
||||||
respondError(w, fmt.Errorf("could not insert user: %v", err))
|
respondError(w, fmt.Errorf("could not insert user: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.Username = githubUser.Login
|
user.Username = githubUser.Login
|
||||||
user.AvatarURL = githubUser.AvatarURL
|
user.AvatarURL = githubUser.AvatarURL
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not query user by github ID: %v", err))
|
respondError(w, fmt.Errorf("could not query user by github ID: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
if err = tx.Commit(); err != nil {
|
||||||
respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))
|
respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exp := time.Now().Add(jwtLifetime)
|
exp := time.Now().Add(jwtLifetime)
|
||||||
token, err := jwtSigner.Encode(jwt.Claims{
|
token, err := jwtSigner.Encode(jwt.Claims{
|
||||||
Subject: user.ID,
|
Subject: user.ID,
|
||||||
Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),
|
Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not create token: %v", err))
|
respondError(w, fmt.Errorf("could not create token: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt, _ := exp.MarshalText()
|
expiresAt, _ := exp.MarshalText()
|
||||||
|
|
||||||
data := make(url.Values)
|
data := make(url.Values)
|
||||||
data.Set("token", string(token))
|
data.Set("token", string(token))
|
||||||
data.Set("expires_at", string(expiresAt))
|
data.Set("expires_at", string(expiresAt))
|
||||||
|
|
||||||
http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
首先,我们会尝试使用之前保存的状态对 cookie 进行解码。并将其与查询字符串中的状态进行比较。如果它们不匹配,我们会返回一个 `418 I'm teapot`(未知来源)错误。
|
首先,我们会尝试使用之前保存的状态对 cookie 进行解码。并将其与查询字符串中的状态进行比较。如果它们不匹配,我们会返回一个 `418 I'm teapot`(未知来源)错误。
|
||||||
|
|
||||||
接着,我们使用授权码生成一个令牌。这个令牌被用于创建 HTTP 客户端来向 GitHub API 发出请求。所以最终我们会向 `https://api.github.com/user` 发送一个 GET 请求。这个端点将会以 JSON 格式向我们提供当前经过身份验证的用户信息。我们将会解码这些内容,一并获取用户的 ID,登录名(用户名)和头像 URL。
|
接着,我们使用授权码生成一个令牌。这个令牌被用于创建 HTTP 客户端来向 GitHub API 发出请求。所以最终我们会向 `https://api.github.com/user` 发送一个 GET 请求。这个端点将会以 JSON 格式向我们提供当前经过身份验证的用户信息。我们将会解码这些内容,一并获取用户的 ID、登录名(用户名)和头像 URL。
|
||||||
|
|
||||||
然后我们将会尝试在数据库上找到具有该 GitHub ID 的用户。如果没有找到,就使用该数据创建一个新的。
|
然后我们将会尝试在数据库上找到具有该 GitHub ID 的用户。如果没有找到,就使用该数据创建一个新的。
|
||||||
|
|
||||||
之后,对于新创建的用户,我们会发出一个用户 ID 为主题(subject)的 JSON 网络令牌,并使用该令牌重定向到前端,查询字符串中一并包含该令牌的到期日(the expiration date)。
|
之后,对于新创建的用户,我们会发出一个将用户 ID 作为主题(`Subject`)的 JSON 网络令牌,并使用该令牌重定向到前端,查询字符串中一并包含该令牌的到期日(`Expiration`)。
|
||||||
|
|
||||||
这一 Web 应用也会被用在其他帖子,但是重定向的链接会是 `/callback?token=&expires_at=`。在那里,我们将会利用 JavaScript 从 URL 中获取令牌和到期日,并通过 `Authorization` 标头中的令牌以`Bearer token_here` 的形式对 `/ api / auth_user` 进行GET请求,来获取已认证的身份用户并将其保存到 localStorage。
|
这一 Web 应用也会被用在其他帖子,但是重定向的链接会是 `/callback?token=&expires_at=`。在那里,我们将会利用 JavaScript 从 URL 中获取令牌和到期日,并通过 `Authorization` 标头中的令牌以 `Bearer token_here` 的形式对 `/api/auth_user` 进行 GET 请求,来获取已认证的身份用户并将其保存到 localStorage。
|
||||||
|
|
||||||
### Guard 中间件
|
### Guard 中间件
|
||||||
|
|
||||||
@ -355,34 +355,34 @@ func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
```
|
```
|
||||||
type ContextKey struct {
|
type ContextKey struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyAuthUserID = ContextKey{"auth_user_id"}
|
var keyAuthUserID = ContextKey{"auth_user_id"}
|
||||||
|
|
||||||
func guard(handler http.HandlerFunc) http.HandlerFunc {
|
func guard(handler http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var token string
|
var token string
|
||||||
if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {
|
if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {
|
||||||
token = a[7:]
|
token = a[7:]
|
||||||
} else if t := r.URL.Query().Get("token"); t != "" {
|
} else if t := r.URL.Query().Get("token"); t != "" {
|
||||||
token = t
|
token = t
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims jwt.Claims
|
var claims jwt.Claims
|
||||||
if err := jwtSigner.Decode([]byte(token), &claims); err != nil {
|
if err := jwtSigner.Decode([]byte(token), &claims); err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)
|
ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)
|
||||||
|
|
||||||
handler(w, r.WithContext(ctx))
|
handler(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -400,33 +400,31 @@ var guarded = guard(func(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
```
|
```
|
||||||
func getAuthUser(w http.ResponseWriter, r *http.Request) {
|
func getAuthUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
authUserID := ctx.Value(keyAuthUserID).(string)
|
authUserID := ctx.Value(keyAuthUserID).(string)
|
||||||
|
|
||||||
var user User
|
var user User
|
||||||
if err := db.QueryRowContext(ctx, `
|
if err := db.QueryRowContext(ctx, `
|
||||||
SELECT username, avatar_url FROM users WHERE id = $1
|
SELECT username, avatar_url FROM users WHERE id = $1
|
||||||
`, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {
|
`, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {
|
||||||
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
respondError(w, fmt.Errorf("could not query auth user: %v", err))
|
respondError(w, fmt.Errorf("could not query auth user: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.ID = authUserID
|
user.ID = authUserID
|
||||||
|
|
||||||
respond(w, user, http.StatusOK)
|
respond(w, user, http.StatusOK)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们使用 Guard 中间件来获取当前经过身份认证的用户 ID 并查询数据库。
|
我们使用 Guard 中间件来获取当前经过身份认证的用户 ID 并查询数据库。
|
||||||
|
|
||||||
* * *
|
|
||||||
|
|
||||||
这一部分涵盖了后端的 OAuth 流程。在下一篇帖子中,我们将会看到如何开始与其他用户的对话。
|
这一部分涵盖了后端的 OAuth 流程。在下一篇帖子中,我们将会看到如何开始与其他用户的对话。
|
||||||
|
|
||||||
[源代码][3]
|
- [源代码][3]
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
@ -435,7 +433,7 @@ via: https://nicolasparada.netlify.com/posts/go-messenger-oauth/
|
|||||||
作者:[Nicolás Parada][a]
|
作者:[Nicolás Parada][a]
|
||||||
选题:[lujun9972][b]
|
选题:[lujun9972][b]
|
||||||
译者:[PsiACE](https://github.com/PsiACE)
|
译者:[PsiACE](https://github.com/PsiACE)
|
||||||
校对:[校对者ID](https://github.com/校对者ID)
|
校对:[wxy](https://github.com/wxy)
|
||||||
|
|
||||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user