之前曾在 LoRaServer 笔记 2.4.1 JSON web-tokens 的使用 中学习了 JWT 的原理及其组成:JWT 是一个很长的字符串,xxxxx.yyyyy.zzzzz,中间用点(.)分隔成三个部分,依次为:Header(头部)、Payload(负载)、Signature(签名)。另外还学习使用 jwt.io 网站的调试工具。
go 中使用社区库 github.com/dgrijalva/jwt-go 来实现。
官方示例 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。
官方示例 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 中看到几种算法对应的签名校验方法。
在使用 gRPC 时,token 是放在 metadata 中的相应 key 中。
本例中按照 LoRaServer 对 JWT 的格式要求来进行处理,metadata 中相应的 key 为 authorization。我们在笔记 6.3.1 gRPC 使用 metadata 自定义认证 的基础上,调整下 metadata 字段。
这边自己造了一个新的 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
}
// 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())
}
// 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
}
# 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"`
}
本篇笔记介绍 JWT 库的 DEMO 应用,还实现了一个比较常用的 gRPC JWT 认证的示例。
具体使用方法可简单记忆如下: