文章目錄
前言
我正在學習酷酷的 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 是否有效。