在 http 请求当中我们可以设置 header 用来传递数据,grpc 底层采用 http2 协议也是支持传递数据的,采用的是 metadata。 Metadata 对于 gRPC 本身来说透明, 它使得 client 和 server 能为对方提供本次调用的信息。就像一次 http 请求的 RequestHeader 和 ResponseHeader,http header 的生命周期是一次 http 请求, Metadata 的生命周期则是一次 RPC 调用。在 go 语言中,可以用 grpc.WithPerRPCCredentials 方法来实现。
简单说一下 我的demo吧,一般我们的api有一个login, 会返回token, 然后在请求及其他api的时候 就必须带上这个token。
首先我们创建一个项目文件夹 jwttoken,里面在创建一个api文件夹
1.创建/api/api.proto【为了简单我们没有引入其他的包】
syntax = "proto3"; package api; service Ping { rpc Login (LoginRequest) returns (LoginReply) {} rpc SayHello(PingMessage) returns (PingMessage) {} } message LoginRequest{ string username=1; string password=2; } message LoginReply{ string status=1; string token=2; } message PingMessage { string greeting = 1; }
2.编译该文件, 我一般习惯在根目录下操作:
protoc -I api/ -I${GOPATH}/src --go_out=plugins=grpc:api api/api.proto
3.编写api/handler.go 可以理解是我们日常服务的具体实现:
package api import ( "fmt" "golang.org/x/net/context" ) // Server represents the gRPC server type Server struct { } func (s *Server) Login(ctx context.Context, in *LoginRequest) (*LoginReply, error) { fmt.Println("Loginrequest: ", in.Username) if in.Username == "gavin" && in.Password == "gavin" { tokenString := CreateToken(in.Username) return &LoginReply{Status: "200", Token: tokenString}, nil } else { return &LoginReply{Status: "403", Token: ""}, nil } } // SayHello generates response to a Ping request func (s *Server) SayHello(ctx context.Context, in *PingMessage) (*PingMessage, error) { msg := "bar" userName := CheckAuth(ctx) msg += " " + userName return &PingMessage{Greeting: msg}, nil }
这个方法也很简单, 验证用户名和密码后 调用CreateToken 方法生成token,一般我们验证token 主要是获取 我们登陆的用户名,所以CheckAuth方法返回登录名,
4.具体实现在/api/authtoken.go方法里面:
package api import ( "context" "fmt" "time" "github.com/dgrijalva/jwt-go" "google.golang.org/grpc/metadata" ) func CreateToken(userName string) (tokenString string) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "iss": "lora-app-server", "aud": "lora-app-server", "nbf": time.Now().Unix(), "exp": time.Now().Add(time.Hour).Unix(), "sub": "user", "username": userName, }) tokenString, err := token.SignedString([]byte("verysecret")) if err != nil { panic(err) } return tokenString } // AuthToekn 自定义认证 type AuthToekn struct { Token string } func (c AuthToekn) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "authorization": c.Token, }, nil } func (c AuthToekn) RequireTransportSecurity() bool { return false } // 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 "", fmt.Errorf("ErrNoMetadataInContext") } // md 的类型是 type MD map[string][]string token, ok := md["authorization"] if !ok || len(token) == 0 { return "", fmt.Errorf("ErrNoAuthorizationInMetadata") } // 因此,token 是一个字符串数组,我们只用了 token[0] return token[0], nil } func CheckAuth(ctx context.Context) (username string) { tokenStr, err := getTokenFromContext(ctx) if err != nil { panic("get token from context error") } var clientClaims Claims token, err := jwt.ParseWithClaims(tokenStr, &clientClaims, func(token *jwt.Token) (interface{}, error) { if token.Header["alg"] != "HS256" { panic("ErrInvalidAlgorithm") } return []byte("verysecret"), nil }) if err != nil { panic("jwt parse error") } if !token.Valid { panic("ErrInvalidToken") } return clientClaims.Username }
其中AuthToekn实现了PerRPCCredentials接口【需要实现GetRequestMetadata 和 RequireTransportSecurity方法】
5.编写main.go 【由于是简单的demo,我习惯吧客户端 和服务端放在一起】
package main import ( "context" "fmt" "log" "net" "jwtdemo/api" "google.golang.org/grpc" ) func main() { go GrpcServer() go GrpcClient() var a string fmt.Scan(&a) } // main start a gRPC server and waits for connection func GrpcServer() { // create a listener on TCP port 7777 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 7777)) if err != nil { log.Fatalf("failed to listen: %v", err) } // create a server instance s := api.Server{} // create a gRPC server object grpcServer := grpc.NewServer() // attach the Ping service to the server api.RegisterPingServer(grpcServer, &s) // start the server if err := grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %s", err) } } func GrpcClient() { var conn *grpc.ClientConn //call Login conn, err := grpc.Dial(":7777", grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %s", err) } defer conn.Close() c := api.NewPingClient(conn) loginReply, err := c.Login(context.Background(), &api.LoginRequest{Username: "gavin", Password: "gavin"}) if err != nil { log.Fatalf("Error when calling SayHello: %s", err) } fmt.Println("Login Reply:", loginReply) //Call SayHello requestToken := new(api.AuthToekn) requestToken.Token = loginReply.Token conn, err = grpc.Dial(":7777", grpc.WithInsecure(), grpc.WithPerRPCCredentials(requestToken)) if err != nil { log.Fatalf("did not connect: %s", err) } defer conn.Close() c = api.NewPingClient(conn) helloreply, err := c.SayHello(context.Background(), &api.PingMessage{Greeting: "foo"}) if err != nil { log.Fatalf("Error when calling SayHello: %s", err) } log.Printf("Response from server: %s", helloreply.Greeting) }
注意 这个的客户端代码 c = api.NewPingClient(conn) 而不是我以前 c := &Server{} 【这里要传递conn,不然后端无法获取token】,
运行结果如下 login 正常返回token, 在请求hello的时候 代入token, 后端正常获取到【后面再尝试gateway的集成 以及双向认证】
D:GoProjectsrcjwtdemo>go run main.go Loginrequest: gavin Login Reply: status:"200" token:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJsb3JhLWFwcC1zZXJ2ZXIiLCJleHAiOjE2MDk3NDgxMzgsImlzcyI6ImxvcmEtYXBwLXNlcnZlciIsIm5iZiI6MTYwOTc0NDUzOCwic3ViIjoidXNlciIsInVzZXJuYW1lIjoiZ2F2aW4ifQ.jP-DjlFKNkS1o9KtnQuqdQMDk6ljd_8UK036mGD9m5o" 2021/01/04 15:15:38 Response from server: bar gavin
下载地址 https://github.com/dz45693/gogrpcjwt.git https://download.csdn.net/download/dz45693/14021638
参考:
https://medium.com/pantomath/how-we-use-grpc-to-build-a-client-server-system-in-go-dd20045fa1c2