Golang 實現 JWT 認證

認證是讓應用知道給應用發送請求的人是他所說的那個人。JSON web token (JWT) 是認證的一種方式,相比於基於 Session 認證,在系統中並不存儲任何關於用戶信息。

本文演示使用 Golang 實現基於 JWT 認證的示例應用。

  1. JWT

1.1. JWT 格式

假設用戶user1嘗試登錄應用,成功後收到 token 信息如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ.2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54

這就是 JWT,有三個部分組成,使用.分隔。

1). 第一部分是頭部 (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9)。頭部信息是生成簽名的算法,這部分非常標準,對於任何使用相同算法的 JWT 都是一樣的。
2). 第二部分是有效載荷 (eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ),它包含特定應用程序信息 (在我們的示例中是用戶名),以及關於 token 有效期和有效性的信息。
3). 第三部分是簽名 (2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54)。它是通過將前兩部分與一個密鑰合併和散列而生成的。

有趣的是頭部和有效載荷是不加密的。僅使用 base64 進行編碼,意味着任何人都能解碼看到內容。使用下面命令 (linux) 可以看到:

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d

返回:

{ "alg""HS256""typ""JWT" }

類似方式可以看到第二部分內容:

{ "username""user1""exp": 1547974082 }

1.2. JWT 簽名

既然 JWT 前兩部分任何人都可以讀寫,JWT 安全由什麼來保證呢?答案是最後部分(簽名)是如何生成的。

假設應用程序希望向成功登錄的用戶 (例如 user1) 發出 JWT。
頭部和有效負載非常簡單: 報頭或多或少是固定的,而有效負載 JSON 對象是通過設置用戶 ID 和終止時間 (以 unix 毫秒爲單位) 形成的。

發出 token 的應用程序也有一個密鑰,只有應用程序本身知道。應用程序將報頭和有效負載的 base64 表示形式與密鑰組合,然後通過一個散列算法 (在本例中是 HS256,報頭中指定的) 進行傳遞。

算法是如何實現的細節超出了本文的範圍, 但要注意的是, 這是一個方式, 這意味着我們不能反向算法和獲得的簽名並使用… 所以我們的密鑰需保護好。

1.3. 驗證 JWT

爲了驗證請求的 JWT, 再次使用請求的報頭和有效載荷以及密鑰生成簽名。如果簽名匹配那麼 JWT 爲有效。

假設一個試圖發出假 token 黑客,他可以輕鬆地生成報頭和有效負載,但是如果不知道密鑰,就無法生成有效的簽名。如果試圖篡改有效 JWT 的現有有效負載,簽名將不再匹配。

通過這種方式 JWT 作爲一種安全方式認證用戶,在應用服務器上無需存儲任何信息 (除了密鑰)。

  1. Golang 實現示例

現在我們已經瞭解 JWT 的認證機制,下面在 GoLang 中實現。

2.1. 創建 HTTP 服務器

首先初始化 HTTP 服務及必要的路由:

package main

import (
 "log"
 "net/http"
)

func main() {
 // "Signin" and "Welcome" are the handlers that we will implement
 http.HandleFunc("/signin", Signin)
 http.HandleFunc("/welcome", Welcome)
 http.HandleFunc("/refresh", Refresh)

 // start the server on port 8000
 log.Fatal(http.ListenAndServe(":8000", nil))
}

下面實現 Signin 和 Welcome 兩個路由。

2.2. 處理用戶登錄

/signin路由使用用戶憑證登錄,爲了簡化我們在內存中存儲用戶信息:

// Create the JWT key used to create the signature
var jwtKey = []byte("my_secret_key")

var users = map[string]string{
 "user1""password1",
 "user2""password2",
}

// Create a struct to read the username and password from the request body
type Credentials struct {
 Password string `json:"password"`
 Username string `json:"username"`
}

// Create a struct that will be encoded to a JWT.
// We add jwt.StandardClaims as an embedded type, to provide fields like expiry time
type Claims struct {
 Username string `json:"username"`
 jwt.StandardClaims
}

// Create the Signin handler
func Signin(w http.ResponseWriter, r *http.Request) {
 var creds Credentials
 // Get the JSON body and decode into credentials
 err := json.NewDecoder(r.Body).Decode(&creds)
 if err != nil {
  // If the structure of the body is wrong, return an HTTP error
  w.WriteHeader(http.StatusBadRequest)
  return
 }

 // Get the expected password from our in memory map
 expectedPassword, ok := users[creds.Username]

 // If a password exists for the given user
 // AND, if it is the same as the password we received, the we can move ahead
 // if NOT, then we return an "Unauthorized" status
 if !ok || expectedPassword != creds.Password {
  w.WriteHeader(http.StatusUnauthorized)
  return
 }

 // Declare the expiration time of the token
 // here, we have kept it as 5 minutes
 expirationTime := time.Now().Add(5 * time.Minute)
 // Create the JWT claims, which includes the username and expiry time
 claims := &Claims{
  Username: creds.Username,
  StandardClaims: jwt.StandardClaims{
   // In JWT, the expiry time is expressed as unix milliseconds
   ExpiresAt: expirationTime.Unix(),
  },
 }

 // Declare the token with the algorithm used for signing, and the claims
 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 // Create the JWT string
 tokenString, err := token.SignedString(jwtKey)
 if err != nil {
  // If there is an error in creating the JWT return an internal server error
  w.WriteHeader(http.StatusInternalServerError)
  return
 }

 // Finally, we set the client cookie for "token" as the JWT we just generated
 // we also set an expiry time which is the same as the token itself
 http.SetCookie(w, &http.Cookie{
  Name:    "token",
  Value:   tokenString,
  Expires: expirationTime,
 })
}

如果用戶登錄憑證正確,該處理器在客戶通過 cookie 設置 JWT。客戶端有了 cookie,後續每個請求將攜帶 cookie。下面我們實現 Welcome 處理器。

2.3. 處理後續認證

登錄成功用戶在客戶端存儲了會話信息,可以使用會話信息進行:

Welcome 處理器實現:

func Welcome(w http.ResponseWriter, r *http.Request) {
 // We can obtain the session token from the requests cookies, which come with every request
 c, err := r.Cookie("token")
 if err != nil {
  if err == http.ErrNoCookie {
   // If the cookie is not set, return an unauthorized status
   w.WriteHeader(http.StatusUnauthorized)
   return
  }
  // For any other type of error, return a bad request status
  w.WriteHeader(http.StatusBadRequest)
  return
 }

 // Get the JWT string from the cookie
 tknStr := c.Value

 // Initialize a new instance of `Claims`
 claims := &Claims{}

 // Parse the JWT string and store the result in `claims`.
 // Note that we are passing the key in this method as well. This method will return an error
 // if the token is invalid (if it has expired according to the expiry time we set on sign in),
 // or if the signature does not match
 tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
  return jwtKey, nil
 })
 if err != nil {
  if err == jwt.ErrSignatureInvalid {
   w.WriteHeader(http.StatusUnauthorized)
   return
  }
  w.WriteHeader(http.StatusBadRequest)
  return
 }
 if !tkn.Valid {
  w.WriteHeader(http.StatusUnauthorized)
  return
 }

 // Finally, return the welcome message to the user, along with their
 // username given in the token
 w.Write([]byte(fmt.Sprintf("Welcome %s!", claims.Username)))
}

2.4. 更新 token

該示例中,有效時間爲 5 分鐘。用戶當然不希望每 5 分鐘登錄一次。因此我們實現另一個路由/refresh,使用之前的 token(仍然有效) 獲取帶有更新時間的 token。

爲了儘量少使用 JWT,過期時間通常保持在幾分鐘左右。通常,客戶機應用程序將在後臺刷新令牌。

func Refresh(w http.ResponseWriter, r *http.Request) {
 // (BEGIN) The code uptil this point is the same as the first part of the `Welcome` route
 c, err := r.Cookie("token")
 if err != nil {
  if err == http.ErrNoCookie {
   w.WriteHeader(http.StatusUnauthorized)
   return
  }
  w.WriteHeader(http.StatusBadRequest)
  return
 }
 tknStr := c.Value
 claims := &Claims{}
 tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
  return jwtKey, nil
 })
 if err != nil {
  if err == jwt.ErrSignatureInvalid {
   w.WriteHeader(http.StatusUnauthorized)
   return
  }
  w.WriteHeader(http.StatusBadRequest)
  return
 }
 if !tkn.Valid {
  w.WriteHeader(http.StatusUnauthorized)
  return
 }
 // (END) The code up-till this point is the same as the first part of the `Welcome` route

 // We ensure that a new token is not issued until enough time has elapsed
 // In this case, a new token will only be issued if the old token is within
 // 30 seconds of expiry. Otherwise, return a bad request status
 if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second {
  w.WriteHeader(http.StatusBadRequest)
  return
 }

 // Now, create a new token for the current use, with a renewed expiration time
 expirationTime := time.Now().Add(5 * time.Minute)
 claims.ExpiresAt = expirationTime.Unix()
 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 tokenString, err := token.SignedString(jwtKey)
 if err != nil {
  w.WriteHeader(http.StatusInternalServerError)
  return
 }

 // Set the new token as the users `token` cookie
 http.SetCookie(w, &http.Cookie{
  Name:    "token",
  Value:   tokenString,
  Expires: expirationTime,
 })
}
  1. 運行應用

編譯並運行程序:

go build
./jwtDemo

現在使用支持 cookie 的 HTTP 客戶端工具進行測試,如 POSTMAN, 使用憑證進行登錄:

POST http://localhost:8000/signin

{"username":"user1","password":"password1"}

下面可以使用同樣的工具發送 welcome 請求獲取返回信息:

GET http://localhost:8000/welcome

發送更新 token 請求,然後檢查客戶端 cookie 及 token 新的值:

POST http://localhost:8000/refresh
  1. 總結

本文介紹了 JWT 認證機制,通過示例使用 Golang 進行演示。

轉自:

blog.csdn.net/neweastsun/article/details/105919915

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gyP4y0MKR_3fSECyOBcmMA