【Go Web 開發】認證請求
上一篇文章實現了客戶端通過發送認證信息獲得身份驗證 token,那麼讓我們看看如何使用該 token 來驗證用戶,實現服務端準確地知道請求來自哪個用戶。
本質上,一旦客戶端有了一個認證 token,後續訪問 API 服務時後端服務將從客戶端 Authorization 請求頭中獲取 token,像這樣:
Authorization: Bearer IEYZQUBEMPPAKPOAWTPV6YJ6RM
當我們收到這些帶認證 token 請求時,將使用一個新的 authenticate() 中間件方法來執行以下邏輯:
-
如果認證 token 無效,我們將向客戶端返回 401 Unauthorized 響應以及一個錯誤消息,讓調用者知道他們的 token 是無效的。
-
如果認證 token 有效,查詢用戶詳細信息,然後將用戶詳細信息添加到請求上下文當中。
-
如果沒有提供 Authorization 請求頭,API 服務會添加一個匿名用戶信息到請求上下文中。
創建匿名用戶
我們從上面所述的最後一點開始,先在 internal/data/user.go 中定義一個匿名用戶,如下所示:
File:internal/data/user.go
package main
...
var (
ErrDuplicateEmail = errors.New("duplicate email")
AnonymousUser = &User{} //聲明一個匿名用戶變量
)
type User struct {
ID int64 `json:"id"`
CreateAt time.Time `json:"create_at"`
Name string `json:"name"`
Email string `json:"email"`
Password password `json:"-"`
Activated bool `json:"activated"`
Version int `json:"-"`
}
//檢查用戶是否爲匿名用戶
func (u *User)IsAnonymous() bool {
return u == AnonymousUser
}
...
這裏我們創建一個新的 AnonymousUser 變量,存放指向一個 User 結構體指針表示用戶沒有 ID、name、email 或 password 且未激活。
我們還爲 User 結構體實現了一個 IsAnonymous() 方法,因此只要是 User 實例就可以判斷是否爲 AnonymousUser 實例,例如:
data.AnonymousUser.IsAnonymous() // → 返回 true
otherUser := &data.User{}
otherUser.IsAnonymous() // → 返回 false
讀寫請求上下文
在我們開始創建 authenticate() 中間件之前,另一個設置步驟涉及到在請求上下文中存儲用戶詳細信息。先大概介紹下請求上下文 (request context):
-
應用程序處理的每個 http.Request 都內置了一個 context.Context 實例,我們可以在請求生命週期內存儲任意 key/value 到這個上下文中。在本文例子中我們將存儲包含用戶信息的 User 結構體實例到上下文。
-
任何存儲在請求上下文中的值都是 interface{} 類型。這意味着從上下文中讀取到的值都需要斷言爲值原來的類型,才能使用。
-
爲請求上下文鍵使用自定義類型是一種很好的實踐。這有助於防止您的代碼與使用請求上下文存儲信息的任何第三方包之間的命名衝突。
爲了幫助解決這個問題,我們創建一個新的 cmd/api/context.go 文件,其中包含了一些輔助方法,用於在請求上下文中讀寫 User 結構體。
如果你跟隨本書操作,請創建一個新文件:
touch cmd/api/context.go
然後添加以下代碼:
File:cmd/api/context.go
package main
import (
"context"
"greenlight.alexedwards.net/internal/data"
"net/http"
)
//自定義contextKey類型
type contextKey string
//將字符串"user"轉爲contextKey類型,然後賦值給userContextKey常量。
//我們將使用這個常量來從請求上下文中讀寫用戶信息
const userContextKey = contextKey("user")
//contextSetUser()方法返回一個包含User結構體的請求實例。注意使用userContextKey常量
func (app *application)contextSetUser(r *http.Request, user *data.User) *http.Request {
ctx := context.WithValue(r.Context(), userContextKey, user)
return r.WithContext(ctx)
}
//contextGetUser()方法從請求上下文中讀取User結構體。從http請求中讀取用戶信息的時候
//會用到這個方法,如果用戶不存在將返回"unexpect"錯誤。
func (app *application)contextGetUser(r *http.Request) *data.User {
user, ok := r.Context().Value(userContextKey).(*data.User)
if !ok {
panic("missing user value in request context")
}
return user
}
創建認證中間件
既然已經準備好了這些東西,我們就可以開始處理 authenticate() 中間件了。
打開 cmd/api/middleware.go 文件,添加以下代碼:
File: cmd/api/middle.go
package main
...
func (app *application)authenticate(next http.Handler) http.Handler {
//添加"Vary: Authorization"響應頭。表示緩存的響應根據Authorization請求頭變化
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Vary", "Authorization")
//讀取Authorization請求頭值,如果沒找到會返回""。
authorizationHeader := r.Header.Get("Authorization")
//如果沒有設置Authorization請求頭,使用contextSetUser()幫助函數添加一個匿名用戶AnonymousUser
//到請求上下文中。然後調用next handler並直接返回。
if authorizationHeader == "" {
r = app.contextSetUser(r, data.AnonymousUser)
next.ServeHTTP(w, r)
return
}
//否則,我們希望Authorization請求頭的值以"Bearer <token>"格式。
//我們試着把它分成對應的組成部分,如果請求頭格式不正確,
//使用invalidAuthenticationTokenResponse()幫助函數返回401 Unauthorized
headerParts := strings.Split(authorizationHeader, " ")
if len(headerParts) != 2 || headerParts[0] != "Bearer" {
app.invalidAuthenticationTokenResponse(w, r)
return
}
//提取認證token
token := headerParts[1]
//校驗token的格式
v := validator.New()
//如果格式不正確,使用invalidAuthenticationTokenResponse()幫助函數返回錯誤響應
//而不是使用failedValidationResponse()
if data.ValidateTokenPlaintext(v, token); !v.Valid() {
app.invalidAuthenticationTokenResponse(w, r)
return
}
//根據認證token查詢數據庫中對應用戶,如果未找到用戶信息再次調用invalidAuthenticationTokenResponse()
//注意:使用ScopeAuthentiaction常量查詢
user, err := app.models.Users.GetForToken(data.ScopeAuthentication, token)
if err != nil {
switch {
case errors.Is(err, data.ErrRecordNotFound):
app.invalidAuthenticationTokenResponse(w, r)
default:
app.serverErrorResponse(w, r, err)
}
return
}
//調用contextSetUser()幫助函數,添加用戶信息到請求上下文中
r = app.contextSetUser(r, user)
//調用next handler
next.ServeHTTP(w, r)
})
}
這裏有很多代碼,爲了說明清楚,我們快速重申下中間件中的操作:
-
如果 Authorization 請求頭中提供了有效的認證 token 的話,將 User 結構體包含的用戶信息存儲到請求上下文中。
-
如果沒有提供 Authorization 請求頭的話,將在請求頭中添加匿名用戶 AnonymousUser 到請求上下文。
-
如果提供了 Authorization 請求頭,但格式不正確或包含無效值,將使用 invalidAuthenticationTokenResponse() 幫助函數返回 401 Unauthorized 響應給客戶端。
下面在 cmd/api/errors.go 文件中創建幫助函數:
File: cmd/api/errors.go
package main
...
func (app *application)invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("www-Authenticate", "Bearer")
message := "invalid or missing authentication token"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}
注意: 這裏使用 www - authenticate: bearer 請求頭提醒客戶端,我們希望他們使用一個提供令牌進行身份驗證。
最後,我們需要將 authenticate() 中間件添加到 handler 處理鏈中。我們需要將這個中間件用在所有的請求中,在 panic recovery 和限流中間件後面,在路由之前。
File:cmd/api/routes.go
package main
...
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(app.notFoundResponse)
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
//在所有請求中使用authenticate()中間件
return app.recoverPanic(app.rateLimit(app.authenticate(router)))
}
功能演示
我們先發起一個沒帶 Authorization 的請求測試下。服務端 authenticate() 中間件將添加 AnonymousUser 到請求上下文中,請求將正常處理,如下所示:
$ curl localhost:4000/v1/healthcheck
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
下面使用一個有效的認證 token 來發起相同的請求。這一次,相關的用戶詳細信息應該會添加到請求上下文中,我們可以再次獲得成功的響應。例如:
$ curl -d '{"email": "alice@example.com", "password": "pa55word"}' localhost:4000/v1/tokens/authentication
{
"authentication_token": {
"token": "GVK72GDNDKFDZUVDGLFX4UVB7I",
"expiry": "2022-01-06T20:17:07.444229+08:00"
}
}
$ curl -H "Authorization: Bearer GVK72GDNDKFDZUVDGLFX4UVB7I" localhost:4000/v1/healthcheck
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
提示: 如果在這裏得到錯誤響應,請確保在第二個請求中使用了來自第一個請求的正確身份驗證 token。
相反,如果發送一些包含無效的認證 token 請求,或 Authorization 請求頭格式不正確。這些情況都會得到 401 Unauthorized 響應,如下所示:
$ curl -i -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXX" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Wed, 05 Jan 2022 12:21:20 GMT
Content-Length: 56
{
"error": "invalid or missing authentication token"
}
$ curl -i -H "Authorization: INVALID" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Wed, 05 Jan 2022 12:23:08 GMT
Content-Length: 56
{
"error": "invalid or missing authentication token"
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RRk8ngOMunNMzcUghE60Ow