1. 程式人生 > >JWT Token認證

JWT Token認證

什麼是JWT?

JWT是JSON Web Token的縮寫,定義了一種簡介自暴寒的方法用於通訊雙方之間以Json物件的形式安全的傳遞資訊。因為特定的數字簽名,所以這些通訊的資訊能夠被校驗和信任。 JWT可以使用HMAC演算法或者RSA的公鑰私鑰對進行簽名。

讓我們進一步的解釋下關於JWT的定義:

  • 簡約(Compact): JWT通訊中使用的資料量比較小,JWT可以通過URL、POST引數,或者直接在HTTP header進行傳遞。 並且,因為比較小的資料量,這也意味著傳輸速度會更加快速。

  • 自包含(Self-contained): 負載(payload)中(可以)包含所需的所有使用者部分的資訊,可以避免對服務端資料庫的多次查詢。

JWT組成

JWT由三部分組成,使用.分隔:

  • 頭部(Header)
  • 載荷(Payload)
  • 簽名(Signature)
    所以,JWT最終樣式為:XXXXX.XXXXX.XXXXX

Header

JWT的頭部包含描述該JWT的最基本的資訊:token的型別, 如JWT, 以及token使用的加密演算法, 如 HMAC SHA256或者RSA.。可以被表示為一個JSON物件:

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

Payload

JWT token的第二部分是payload, Payload包含claims. Claims是一些實體(通常指使用者)的狀態資訊和其他元資料。

{
  "iss": "sysu", 
  "iat": 1233458243, 
  "exp": 1448333419, 
  "aud": "sysu-ss", 
  "sub": "sysu-ss", 
}

iss(issuer 簽發者),是否使用是可選的;
iat(issued at簽發時間),這裡是一個Unix時間戳,是否使用是可選的;
exp(expiration time 過期時間) ,這裡是一個Unix時間戳,是否使用是可選的;
aud(audience 接收方 ),是否使用是可選的;
sub(subject 面向的使用者),是否使用是可選的;

關於更多有關Payload欄位的介紹,請檢視

JWT官網

Signature

要建立簽名部分,需要使用經過編碼後的頭部(header)和負載(payload)以及一個金鑰,將header和payload用.連線起來後,使用header中制定的演算法進行簽名。
該簽名是使用者驗證JWT的請求傳送者以及確保資料資訊在傳輸過程中的訊息是未經篡改的。

JWT使用(golang)

我使用的是jwt-gonegroni web中介軟體以及mux搭建服務。關於這三個庫的使用請自行谷歌。

  • 首先定義自己的Token:
// service/jwt.go
var tokens []Token	// 作為全域性變數儲存登入使用者的token,可以儲存到資料庫中
const TokenName = "SW-TOKEN"
const Issuer = "Go-GraphQL-Group"
const SecretKey = "StarWars"

type Token struct {
	SW_TOKEN string `json:"SW-TOKEN"`
}

type jwtCustomClaims struct {
	jwt.StandardClaims

	Admin bool `json:"admin"`
}

func CreateToken(secretKey []byte, issuer string, isAdmin bool) (token Token, err error) {
	claims := &jwtCustomClaims{
		jwt.StandardClaims{
			ExpiresAt: int64(time.Now().Add(time.Hour * 1).Unix()),
			Issuer:    issuer,
		},
		isAdmin,
	}

	tokenStr, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretKey)
	token = Token{
		tokenStr,
	}
	return
}

func ParseToken(tokenStr string, secretKey []byte) (claims jwt.Claims, err error) {
	var token *jwt.Token
	token, err = jwt.Parse(tokenStr, func(*jwt.Token) (interface{}, error) {
		return secretKey, nil
	})
	claims = token.Claims
	return
}
  • 之後生成服務並註冊路由:
// main.go
func NewServer() *negroni.Negroni {
	router := mux.NewRouter()
	initRoutes(router)

	n := negroni.Classic()	// negroni.Classic() 返回帶有預設中介軟體的Negroni例項指標

	n.UseHandler(router)	// 將router中http.Handler加入到negroni的中介軟體棧中
	return n
}

func initRoutes(router *mux.Router) {
	router.HandleFunc("/login", service.LoginHandler).Methods("POST")
	// 使用中介軟體進行token認證
	router.Use(service.TokenMiddleware)
	// query服務
	router.HandleFunc("/query", handler.GraphQL(GraphQL_Service.NewExecutableSchema(GraphQL_Service.Config{Resolvers: &GraphQL_Service.Resolver{}})))
	// 退出路由
	router.HandleFunc("/logout", service.LogoutHandler).Methods("POST", "GET")
}
  • login路由處理函式簽發Token:
func LoginHandler(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	fmt.Println(r.Form.Get("username"))
	if err != nil {
		w.WriteHeader(http.StatusForbidden)
		fmt.Fprint(w, "Error in request")
		return
	}
	// 使用固定的username和password做測試
	if strings.ToLower(r.Form.Get("username")) != "admin" || r.Form.Get("password") != "password" {
		w.WriteHeader(http.StatusForbidden)
		fmt.Println("Error logging in")
		fmt.Fprint(w, "Invalid credentials")
		return
	}

	token, err := CreateToken([]byte(SecretKey), Issuer, false)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, "Error extracting the key")
		log.Fatal(err)
	}

	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")

	tokenBytes, err := json.Marshal(token)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, "Error marshal the token")
		log.Fatal(err)
	}
	tokens = append(tokens, token)
	w.Write(tokenBytes)
}
  • 中介軟體定義如下:
func TokenMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	    // 不是login的請求需要進行token認證
		if r.RequestURI[1:] != "login" {
			/*
				// token位於Authorization中,用此方法
				token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
					return []byte(SecretKey), nil
				})
			*/
			tokenStr := ""
			for k, v := range r.Header {
				if strings.ToUpper(k) == TokenName {
					tokenStr = v[0]
					break
				}
			}
			validToken := false
			for _, token := range tokens {
				if token.SW_TOKEN == tokenStr {
					validToken = true
				}
			}
			if validToken {
				ctx := context.WithValue(r.Context(), TokenName, tokenStr)
				next.ServeHTTP(w, r.WithContext(ctx))
			} else {
				w.WriteHeader(http.StatusUnauthorized)
				w.Write([]byte("Unauthorized access to this resource"))
				//fmt.Fprint(w, "Unauthorized access to this resource")
			}
		} else {
			next.ServeHTTP(w, r)
		}
	})
}
  • logout路由處理函式(刪除token):
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
	tokenStr := ""
	for k, v := range r.Header {
		if strings.ToUpper(k) == TokenName {
			tokenStr = v[0]
			break
		}
	}
	for i, token := range tokens {
		if token.SW_TOKEN == tokenStr {
			tokens = append(tokens[:i], tokens[i+1:]...)
			break
		}
	}
	w.Write([]byte("logout"))
}
  • 啟動服務
// main.go
func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	server := NewServer()
	server.Run(":" + port)

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
}
  • 測試服務
  1. 未登入,query
    在這裡插入圖片描述
  2. 登入login
    在這裡插入圖片描述
  3. query
    在這裡插入圖片描述
  4. 登出logout
    在這裡插入圖片描述
  5. query
    在這裡插入圖片描述

程式碼源地址:Github