Golang筆記 6.4 JSON Web Tokens (JWT)

前言

我正在學習酷酷的 Golang,可點此查看帖子Golang學習筆記彙總

1 JSON Web Tokens (JWT) 介紹

之前曾在 LoRaServer 筆記 2.4.1 JSON web-tokens 的使用 中學習了 JWT 的原理及其組成:JWT 是一個很長的字符串,xxxxx.yyyyy.zzzzz,中間用點(.)分隔成三個部分,依次爲:Header(頭部)、Payload(負載)、Signature(簽名)。另外還學習使用 jwt.io 網站的調試工具。

go 中使用社區庫 github.com/dgrijalva/jwt-go 來實現。

2 JWT 的代碼實現

2.1 生成 token

官方示例 Simple example of building and signing a token

    // Create a new token object, specifying signing method and the claims
    // you would like it to contain.
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "foo": "bar",
        "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    })

    // Sign and get the complete encoded token as a string using the secret
    tokenString, err := token.SignedString(hmacSampleSecret)

    fmt.Println(tokenString, err)

顯然 JWT 的 Header 由 jwt.SigningMethodHS256 確定,payload 則有 claim 確定,剩下簽名則將密鑰傳入 token.SignedString,生成了最終 token。

2.2 解析 token

官方示例 Simple example of parsing and validating a token

    // sample token string taken from the New example
    tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU"

    // Parse takes the token string and a function for looking up the key. The latter is especially
    // useful if you use multiple keys for your application.  The standard is to use 'kid' in the
    // head of the token to identify which key to use, but the parsed token (head and claims) is provided
    // to the callback, providing flexibility.
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
        return hmacSampleSecret, nil
    })

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        fmt.Println(claims["foo"], claims["nbf"])
    } else {
        fmt.Println(err)
    }

示例中還檢查了 簽名校驗算法,HMAC 對應的是 HS 算法(我們的Header填了 HS256) 。可以在 jwt.io 中看到幾種算法對應的簽名校驗方法。

3 gRPC helloworld demo 增加 JWT 認證

在使用 gRPC 時,token 是放在 metadata 中的相應 key 中。

本例中按照 LoRaServer 對 JWT 的格式要求來進行處理,metadata 中相應的 key 爲 authorization。我們在筆記 6.3.1 gRPC 使用 metadata 自定義認證 的基礎上,調整下 metadata 字段。

3.1 client

生成 token

這邊自己造了一個新的 JWT,簽名密鑰使用 verysecret。

func createToken () (tokenString string) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "iss": "lora-app-server",
        "aud": "lora-app-server",
        "nbf": time.Now().Unix(),
        "exp": time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
        "sub": "user",
        "username": "admin"
    })
    tokenString, err := token.SignedString([]byte("verysecret"))
    return tokenString
}

更新 metadata

// customCredential 自定義認證
type customCredential struct{}

func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
        "authorization": createToken(),
    }, nil
}

func (c customCredential) RequireTransportSecurity() bool {
    return false
}

func main() {
    var opts []grpc.DialOption
	opts = append(opts, grpc.WithInsecure())
	opts = append(opts, grpc.WithBlock())
    // 使用自定義認證
    opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
	// Set up a connection to the server.
	conn, err := grpc.Dial(address, opts...)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	name := defaultName
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

3.2 server

// Claims defines the struct containing the token claims.
type Claims struct {
	jwt.StandardClaims

	// Username defines the identity of the user.
	Username string `json:"username"`
}

// Step1. 從 context 的 metadata 中,取出 token

func getTokenFromContext(ctx context.Context) (string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", ErrNoMetadataInContext
	}
    // md 的類型是 type MD map[string][]string
	token, ok := md["authorization"]
	if !ok || len(token) == 0 {
		return "", ErrNoAuthorizationInMetadata
	}
    // 因此,token 是一個字符串數組,我們只用了 token[0]
	return token[0], nil
}

// Step2. 從 token 解析出 jwt 的 claim

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	tokenStr, err := getTokenFromContext(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "get token from context error")
	}

	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		if token.Header["alg"] != "HS256" {
			return nil, ErrInvalidAlgorithm
		}
		return []byte("verysecret"), nil
	})
	if err != nil {
		return nil, errors.Wrap(err, "jwt parse error")
	}

	if !token.Valid {
		return nil, ErrInvalidToken
    }
    
	log.Printf("Received: %v\ntoken: %v", in.Name, token.Claims)
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

3.3 運行結果

# go run greeter_server/main.go
2019/11/15 14:47:34 Received: world
token: &{{lora-app-server 1577836800  0 lora-app-server 1573800454 user} admin}

可以看到從 token 解析出的 claim 是按照我們定義的結構體來呈現的:

type Claims struct {
	jwt.StandardClaims
	Username string `json:"username"`
}

4 小結

本篇筆記介紹 JWT 庫的 DEMO 應用,還實現了一個比較常用的 gRPC JWT 認證的示例。

具體使用方法可簡單記憶如下:

  • 在 jwt 生成時使用 jwt.NewWithClaims 方法,需傳入 header claim實例 和 密鑰。
  • 在 jwt 解析時使用 jwt.ParseWithClaims 方法,需傳入 claim 結構體 和 密鑰,可返回解析是否正確,及 token 是否有效。

END


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