JWT與Golang

JWT基礎概念

JWT是 json web token的簡稱

其中的 token 是令牌的意思, 其實這個令牌實質上是服務端生成的一段有規則的字符串

我們看看JWT官方自己對其的定義

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

我們提煉出重點信息:

  1. JWT是一個開放的標準
  2. jwt本身體積比較緊湊,所以傳輸速度比較快
  3. jwt可以放在URL中傳遞,可以放在請求頭中傳遞,可以放在請求體中傳遞
  4. jwt的有效載荷中包含一些自定義的有效信息,在某些場景中可以避免部分數據庫查詢

JWT使用場景

  • 授權 : jwt最常見的使用場景,用戶端首先登陸成功,從服務端獲取jwt(令牌),那麼用戶後面的所有的請求中都應該包含這個令牌,服務端通過這個令牌判斷允許用戶的權限和訪問的資源,服務.

    基於這樣的特點可以做單點登錄(SingleSignOn,SSO)

    基於此也可以做跨域認證

  • 信息交換 : 通訊雙方通過jwt可以傳遞信息, 信息都是簽名之後,可以防止僞造

JWT的結構

如下是一個標準的JWT

在這裏插入圖片描述

JWT 是有三部分組成的,每一部分之間通過 . (點號) 隔開

header 頭部

Payload 載荷

Signature 簽名

那麼JWT的格式如下 :

header . Payload . Signature

頭部

標頭是一個json對象通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法,例如HMAC SHA256或RSA。

{
  "alg": "HS256",
  "typ": "JWT"
}

這一部分的json內容被 Base64Url 編碼之後成爲第一部分

載荷

載荷也是一個json對象,是實際承載傳遞數據的部分,JWT提供了7個預定義好字段可以按需使用

iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受衆
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號

除了這些預定義的字段,我們可以在payload中添加自己的'私貨' ,添加一些自己字段

{
    "name":"admin",
    "pwd":"123456"
}

有了有效載荷之後再對有效載荷進行 Base64Url 編碼 ,編碼之後的這部分就是 jwt 的第二部分

tips : 儘量不要將很重要和私密的信息放在其中,因爲這部分解碼之後是可見的

簽名

簽名就是將前面的編碼之後header 和 編碼之後的 payload 再加上祕鑰secretKey 通過指定的加密算法創建出來的(簽名默認算法是 HMAC SHA256)

創建簽名的公式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secretKey)

通常認爲簽名的作用是防數據被篡改

JWT工作原理

在這裏插入圖片描述

一般而言我們攜帶JWT發送請求可以放在HTTP請求頭中的Authorization 中, 格式如下

Authorization: Bearer <token>

tips: 我們前面說了jwt 這個令牌其實也可以放在 HTTP的url中,也可以在HTTP請求的請求報文中

當 token在HTTP的請求頭中的 Authorization 中發送時可以解決 CORS 的問題

Golang使用JWT

模擬使用場景 :

  1. 新建http服務,提供三個處理接口 /auth ,/home ,/list
  2. /auth 接口處理用戶登陸驗證,並返回 token (jwt)
  3. /home 接口處理登陸成功的用戶都能訪問的 home服務
  4. /list 接口處理登陸成功的用戶並且用戶權限是admin 才能訪問的 list服務

tips : 這裏的http服務和JWT 的 Authorization server(認證服務) 在同一服務器上,在實際開發中 Authorization server(認證服務) 可以單獨部署

在這裏插入圖片描述

代碼實現

Golang中有很多關於jwt的包,我們使用如下包

 # 安裝依賴包
 go get github.com/dgrijalva/jwt-go

JWT服務的關鍵代碼如下

// JWT中的payload中不要放重要數據,因爲這部分數據通過Base64URL算法能反解出來
type Claims struct {
	// 自定義的`私有`數據,在payload中
	UserAccount
	// jwt的標準的claims
	jwt.StandardClaims
}

// 生成token
func GetToken(name, password, role string) (string, error) {
	c := Claims{
		UserAccount: UserAccount{
			Username: name,
			Password: password,
			Role:     role,
		},
		StandardClaims: jwt.StandardClaims{
			// token過期時間
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
			// 簽發人
			Issuer:  "captain",
            // 主題
			Subject: "jwt test",
		},
	}
	// 生成token,默認採用HMAC SHA256
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	// 加上簽名(需要用到祕鑰),生成完整的token
	return token.SignedString(SecretKey)
}

// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
		return SecretKey, nil
	})
	if err != nil {
		return nil, err
	}
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

此處我們不用框架,直接使用golang一些標準包構建一個http服務,並且集成JWT服務

http服務(包含JWT服務)端完整代碼

jwt.go

package main

import (
	"encoding/json"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"github.com/pkg/errors"
	"log"
	"net/http"
	"strings"
	"time"
)

const (
	// 定義token的有效時間
	TokenExpireDuration = time.Hour * 1
)

var SecretKey = []byte("123456")

// 請求的賬戶信息
type UserAccount struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Role     string `json:"role"`
}

// 響應給客戶端的數據
type ResponseToClient struct {
	Code    string      `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

// JWT中的payload中不要放重要數據,因爲這部分數據通過Base64URL算法能反解出來
type Claims struct {
	// 自定義的`私有`數據,在payload中
	UserAccount
	// jwt的標準的claims
	jwt.StandardClaims
}

// 生成token
func GetToken(name, password, role string) (string, error) {
	c := Claims{
		UserAccount: UserAccount{
			Username: name,
			Password: password,
			Role:     role,
		},
		StandardClaims: jwt.StandardClaims{
			// token過期時間
			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
			// 簽發人
			Issuer:  "captain",
			Subject: "jwt test",
		},
	}
	// 生成token,默認採用HMAC SHA256
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
	// 加上簽名(需要用到祕鑰),生成完整的token
	return token.SignedString(SecretKey)
}

// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
		return SecretKey, nil
	})
	if err != nil {
		return nil, err
	}
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

func WriteToResponse(w http.ResponseWriter, code, message string, data interface{}) {
	var resp ResponseToClient
	resp.Code = code
	resp.Message = message
	resp.Data = data
	respJson, _ := json.Marshal(resp)
	// 設置響應爲json格式
	w.Header().Set("Content-Type", "application/json;charset=utf-8")
	fmt.Fprintf(w, "%v\n", string(respJson))
}

// 默認處理函數
func defaultFunc(w http.ResponseWriter, r *http.Request) {

}

// 模擬用戶登陸接口,響應json數據
// 目的是讓客戶端從服務器端獲取token
// 處理請求邏輯之後,響應給客戶的數據中包含新生成的token值
func AuthFunc(w http.ResponseWriter, r *http.Request) {
	var user UserAccount
	// 讀取客戶端請求的類容
	buf := make([]byte, 2048)
	n, _ := r.Body.Read(buf)
	// debug 調試請求的內容
	log.Println("json :", string(buf[:n]))
	// 將請求json數據解析出來
	err := json.Unmarshal(buf[:n], &user)
	// 如果解析錯誤,給客戶端提示
	if err != nil {
		WriteToResponse(w, "400", err.Error(), "")
		return
	}
	// debug 調試解析之後的內容
	log.Println(user)
	// 模擬驗證賬戶登錄的邏輯(賬戶,密碼都正確)
	if user.Username == "admin" && user.Password == "123456" {
		tokenString, _ := GetToken(user.Username, user.Password, user.Role)
		WriteToResponse(w, "200", "success", map[string]string{"token": tokenString})
		return
	} else {
		WriteToResponse(w, "400", "Account error", "")
		return
	}
	// 通過命令行的 CURL 測試
	// curl -X POST -H -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"admin"}' http://127.0.0.1:8080/auth
}

// 模擬用戶登陸(獲取token)之後再請求某個接口,響應json數據
// 請求時,在請求的數據中包含token
// 包含token的載體可以是請求的url,也可以是請求頭,也可以是請求體
func homeFunc(w http.ResponseWriter, r *http.Request) {
	// 此處我們模擬的token包含在請求頭信息中
	authorH := r.Header.Get("Authorization")
	if authorH == "" {
		WriteToResponse(w, "401", "request header Authorization is null", "")
		return
	}
	// 將獲取的Authorization 內容通過分割出來
	authorArr := strings.SplitN(authorH, " ", 2)
	// debug
	log.Println(authorArr)
	// Authorization的字符串通常是 "Bearer" 開頭(可以理解爲固定格式,標識使用承載模式),然後一個空格 再加上token的內容
	// Tips:  請求頭中Authorization的內容直接是token也是可以的
	if len(authorArr) != 2 || authorArr[0] != "Bearer" {
		WriteToResponse(w, "402", "request header Authorization formal error", "")
		return
	}
	// 解析token這個字符串
	mc, err := ParseToken(authorArr[1])
	if err != nil {
		WriteToResponse(w, "403", err.Error(), "")
		return
	}
	// debug
	log.Println(mc)
	// 請求成功響應給客戶端
	WriteToResponse(w, "200", "welcome to home", "")
}
func listFunc(w http.ResponseWriter, r *http.Request) {
	authorH := r.Header.Get("Authorization")
	if authorH == "" {
		WriteToResponse(w, "401", "request header Authorization is null", "")
		return
	}
	authorArr := strings.SplitN(authorH, " ", 2)
	// debug
	log.Println(authorArr)
	if len(authorArr) != 2 || authorArr[0] != "Bearer" {
		WriteToResponse(w, "402", "request header Authorization formal error", "")
		return
	}
	mc, err := ParseToken(authorArr[1])
	if err != nil {
		WriteToResponse(w, "403", err.Error(), "")
		return
	}
	// 模擬通過jwt確定權限的邏輯
	// 如果 用戶角色是admin,那麼就能訪問該接口,否則就不允許
	if mc.Role == "admin" {
		WriteToResponse(w, "200", "列表數據", "")
		return
	}
	WriteToResponse(w, "404", "無權限訪問", "")
}
func main() {
	http.HandleFunc("/", defaultFunc)
	http.HandleFunc("/auth", AuthFunc)
	http.HandleFunc("/home", homeFunc)
	http.HandleFunc("/list", listFunc)
	fmt.Println("start http server and listen 8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("ListenAndServer err : ", err)
	}
}

# 運行jwt服務
$ go run jwt.go
start http server and listen 8080

服務測試

測試工具是直接使用命令行下的 curl命令工具進行的測試

tips: 當然也可以使用其他的任何能發送http請求的工具進行測試(包括使用代碼編寫http客戶端)

測試登陸驗證服務

# 發送POST並攜帶json數據
curl -X POST -v -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"edit"}' http://127.0.0.1:8080/auth

# 響應數據
> POST /auth HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.69.1
> Accept: */*
> Content-type:application/json
> Content-Length: 54
>
} [54 bytes data]
* upload completely sent off: 54 out of 54 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=utf-8
< Date: Sun, 05 Apr 2020 09:12:56 GMT
< Content-Length: 266
<
// 響應的json數據 包含了token
{"code":"200","message":"success","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4NDUzMCwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.VNIk9nI8SStCMCI_QyJ8gLrUbOLNSQgeoVabjQFzMS0"}}


測試訪問 home 服務

# 無token請求
 curl -X POST -H "Content-type:application/json" http://127.0.0.1:8080/home
# 響應
{"code":"401","message":"request header Authorization is null","data":""}

# 正確的請求
 curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/home

# 響應
{"code":"200","message":"welcome to home","data":""}

測試訪問 list 服務

# 請求
 curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/list
 
# 響應
{"code":"404","message":"無權限訪問","data":""}

參考資料

JSON Web Token 入門教程

jwt官網

sha256

CORS

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章