書接上文 Go Grpc Jwt身份認證 ,本文我們嘗試把gateway也加進來,有關gatewa大家可以參考 go學習筆記 grpc-gateway和swagger。直接開干吧
Grpc Jwt GateWay的集成【包含跨域問題的解決】
1.修改api/api.proto文件
syntax = "proto3"; package api; // 1 導入 gateway 相關的proto 以及 swagger 相關的 proto import "google/api/annotations.proto"; import "protoc-gen-swagger/options/annotations.proto"; // 2 定義 swagger 相關的內容 option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { info: { title: "grpc gateway sample"; version: "1.0"; license: { name: "MIT"; }; }; schemes: HTTP; consumes: "application/json"; produces: "application/json"; }; service Ping { rpc Login (LoginRequest) returns (LoginReply) { option (google.api.http) = { post: "/login" body: "*" }; } rpc SayHello(PingMessage) returns (PingMessage) { option (google.api.http) = { post: "/sayhello" body: "*" }; } } message LoginRequest{ string username=1; string password=2; } message LoginReply{ string status=1; string token=2; } message PingMessage { string greeting = 1; }
2.編譯api/api.proto
protoc -ID:\Go\include -I. --go_out=plugins=grpc:. ./api/api.proto protoc -ID:\Go\include -I. --grpc-gateway_out=logtostderr=true:. ./api/api.proto
3. 這次我們吧server 和client 分開, 分成兩個文件夾,上文中獲取token 用的是metadata.FromIncomingContext(ctx)方法, 這次我們該用metautils.ExtractIncoming(ctx).Get(headerAuthorize)方法比較簡單。修改后的的authtoken.go 如下:
package api import ( "context" "time" "github.com/dgrijalva/jwt-go" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" ) var ( headerAuthorize = "authorization" ) 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{ headerAuthorize: 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 { val := metautils.ExtractIncoming(ctx).Get(headerAuthorize) return val } func CheckAuth(ctx context.Context) (username string) { tokenStr := getTokenFromContext(ctx) if len(tokenStr) == 0 { 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 }
4.server的main.go 我們增加了跨域請求的設置,同時也罷 grpc server 和http 的server整合在一起【原理很簡單 就是整合一個handler 監聽一個端口, 判斷進來的是grpc 還是json,grpc交由grpc 服務處理】,server/main.go代碼如下:
package main import ( "context" "fmt" "log" "net/http" "strings" "jwtdemo/api" "github.com/grpc-ecosystem/grpc-gateway/runtime" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "google.golang.org/grpc" ) const ( port = ":8080" ) func main() { // 創建grpc-gateway服務,轉發到grpc的8080端口 gwmux := runtime.NewServeMux() opt := []grpc.DialOption{grpc.WithInsecure()} err := api.RegisterPingHandlerFromEndpoint(context.Background(), gwmux, "localhost"+port, opt) if err != nil { log.Fatal(err) } // 創建grpc服務 rpcServer := grpc.NewServer() api.RegisterPingServer(rpcServer, new(api.Server)) // 創建http服務,監聽8080端口,並調用上面的兩個服務來處理請求 http.ListenAndServe( port, grpcHandlerFunc(rpcServer, gwmux), ) } // grpcHandlerFunc 根據請求頭判斷是grpc請求還是grpc-gateway請求 func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { grpcServer.ServeHTTP(w, r) } else { allowCORS(otherHandler).ServeHTTP(w, r) } }), &http2.Server{}) } func preflightHandler(w http.ResponseWriter, r *http.Request) { headers := []string{"Content-Type", "Accept", "Authorization"} w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"} w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) fmt.Println("preflight request for:", r.URL.Path) return } // allowCORS allows Cross Origin Resoruce Sharing from any origin. // Don't do this without consideration in production systems. func allowCORS(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if origin := r.Header.Get("Origin"); origin != "" { w.Header().Set("Access-Control-Allow-Origin", origin) if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { preflightHandler(w, r) return } } h.ServeHTTP(w, r) }) }
5客戶端我們增加了 http的調用, client/main.go實現如下:
package main import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "strings" "jwtdemo/api" "google.golang.org/grpc" ) func main() { grpcCall() fmt.Println("http call.....") httpCall() } const ( grpcPort = ":8080" httpPort = ":8080" ) func grpcCall() { var conn *grpc.ClientConn //call Login conn, err := grpc.Dial(grpcPort, 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(grpcPort, 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) } func httpCall() { urlpfx := "http://localhost" + httpPort //call login loginRequest := api.LoginRequest{Username: "gavin", Password: "gavin"} loginrequestByte, _ := json.Marshal(loginRequest) request, _ := http.NewRequest("POST", urlpfx+"/login", strings.NewReader(string(loginrequestByte))) request.Header.Set("Content-Type", "application/json") loginResponse, _ := http.DefaultClient.Do(request) loginReplyBytes, _ := ioutil.ReadAll(loginResponse.Body) defer loginResponse.Body.Close() var loginReply api.LoginReply json.Unmarshal(loginReplyBytes, &loginReply) //fmt.Println("token:" + loginReply.Token) ///call say hello sayhelloRequest := api.PingMessage{Greeting: "gavin say "} sayhelloRequestByte, _ := json.Marshal(sayhelloRequest) request, _ = http.NewRequest("POST", urlpfx+"/sayhello", strings.NewReader(string(sayhelloRequestByte))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", loginReply.Token) sayhelloResponse, err := http.DefaultClient.Do(request) if err != nil { fmt.Println(err) } sayhelloReplyBytes, err := ioutil.ReadAll(sayhelloResponse.Body) if err != nil { fmt.Println(err) } log.Printf(string(sayhelloReplyBytes)) }
6.為了驗證跨域問題, 我們增加了一個html/hello.html頁面 內容如下:
<html> <head> <title>grpc gate way test</title> </head> <body> <div id="divtoke"></div> <input type="button" value="token" id="btnToken"><br> <div id="divhelllo"></div><input type="button" value="Sayhello" id="btnHello"><br> <script type="text/javascript" src="./jquery-2.2.3.min.js"></script> <script> var prfx="http://localhost:8080/"; $("#btnToken").click(function(){ var obj={ username:"gavin",password:"gavin"}; var objstr= JSON.stringify(obj); $.ajax({ "type": "POST", "contentType": "application/json", "url": prfx + "login", "dataType": "json", "data": objstr , "success": function(data, status, xhr) { $("#divtoke").html(data.token) } }); }); $("#btnHello").click(function(){ var obj={greeting:"world"}; var objstr= JSON.stringify(obj); var userToken=$("#divtoke").html(); $.ajax({ "headers": {"Authorization":userToken}, "type": "POST", "contentType": "application/json", "url": prfx + "sayhello", "dataType": "json", "data": objstr, "success": function(data, status, xhr) { $("#divhelllo").html(data.greeting) } }); }); </script> </body> </html>
7。 為了便於之間看文章的朋友我吧 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 }
8.運行結果如下:
------------------------------------------------------------------------------------------------------------------------------------------------------------
Https雙向認證的集成
到目前為止我們 還沒有使用證書,為了方便先前的code 跑起來, 我新建servertls 和clienttls文件夾,關於證書的生成利用MySSL測試證書生成工具我們可以很簡單的生成兩張證書,要是用https首先需要修改api/api.proto文件的schemes 為https 然后重新編譯, 為了讓AuthToekn兼容http和https 我們修改為如下:
// AuthToekn 自定義認證 type AuthToekn struct { Token string Tsl bool } func (c AuthToekn) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ headerAuthorize: c.Token, }, nil } func (c AuthToekn) RequireTransportSecurity() bool { return c.Tsl //return false }
最后我們來看看 servertls/main.go如何實現:
package main import ( "context" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" api "jwtdemo/api" "log" "net/http" "strings" "github.com/grpc-ecosystem/grpc-gateway/runtime" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) const ( port = ":8283" serverPem = "../certs/server.pem" serverkey = "../certs/server.key" rootPem = "../certs/ca.pem" ) func main() { cert, _ := tls.LoadX509KeyPair(serverPem, serverkey) certPool := x509.NewCertPool() ca, _ := ioutil.ReadFile(rootPem) certPool.AppendCertsFromPEM(ca) creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: certPool, }) // 創建grpc-gateway服務,轉發到grpc的8080端口 gwmux := runtime.NewServeMux() creds = credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: certPool, }) opt := []grpc.DialOption{grpc.WithTransportCredentials(creds)} err := api.RegisterPingHandlerFromEndpoint(context.Background(), gwmux, "localhost"+port, opt) if err != nil { log.Fatal(err) } // 創建grpc服務 rpcServer := grpc.NewServer() api.RegisterPingServer(rpcServer, new(api.Server)) // 創建http服務,監聽8080端口,並調用上面的兩個服務來處理請求 http.ListenAndServeTLS(port, serverPem, serverkey, grpcHandlerFunc(rpcServer, gwmux)) } // grpcHandlerFunc 根據請求頭判斷是grpc請求還是grpc-gateway請求 func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { grpcServer.ServeHTTP(w, r) } else { allowCORS(otherHandler).ServeHTTP(w, r) } }), &http2.Server{}) } func preflightHandler(w http.ResponseWriter, r *http.Request) { headers := []string{"Content-Type", "Accept", "Authorization"} w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"} w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) fmt.Println("preflight request for:", r.URL.Path) return } // allowCORS allows Cross Origin Resoruce Sharing from any origin. // Don't do this without consideration in production systems. func allowCORS(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if origin := r.Header.Get("Origin"); origin != "" { w.Header().Set("Access-Control-Allow-Origin", origin) if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { preflightHandler(w, r) return } } h.ServeHTTP(w, r) }) } func getTLSConfig(host, caCertFile string, certOpt tls.ClientAuthType) *tls.Config { var caCert []byte var err error var caCertPool *x509.CertPool if certOpt > tls.RequestClientCert { caCert, err = ioutil.ReadFile(caCertFile) if err != nil { fmt.Printf("Error opening cert file %s error: %v", caCertFile, err) } caCertPool = x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) } return &tls.Config{ ServerName: host, ClientAuth: certOpt, ClientCAs: caCertPool, MinVersion: tls.VersionTLS12, // TLS versions below 1.2 are considered insecure - see https://www.rfc-editor.org/rfc/rfc7525.txt for details } }
最后clienttls/main.go修改后如下:
package main import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "strings" "jwtdemo/api" "golang.org/x/net/http2" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) func main() { grpcCall() fmt.Println("http call.....") httpCall() } const ( port = ":8283" clientPem = "../certs/server.pem" clientkey = "../certs/server.key" rootPem = "../certs/ca.pem" ) func grpcCall() { var conn *grpc.ClientConn cert, _ := tls.LoadX509KeyPair(clientPem, clientkey) certPool := x509.NewCertPool() ca, _ := ioutil.ReadFile(rootPem) certPool.AppendCertsFromPEM(ca) creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: "localhost", RootCAs: certPool, }) //call Login conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalf("did not connect: %s", err) } defer conn.Close() //c := api.NewPingClient(conn) c := api.NewPingClient(conn) loginReply, err := c.Login(context.Background(), &api.LoginRequest{Username: "gavin", Password: "gavin"}) if err != nil { log.Fatalf("Error when calling Login: %s", err) } //fmt.Println("Login Reply:", loginReply) //Call SayHello requestToken := new(api.AuthToekn) requestToken.Token = loginReply.Token requestToken.Tsl = true conn, err = grpc.Dial(port, grpc.WithTransportCredentials(creds), 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) } func httpCall() { urlpfx := "https://localhost" + port cert, _ := tls.LoadX509KeyPair(clientPem, clientkey) certPool := x509.NewCertPool() ca, _ := ioutil.ReadFile(rootPem) certPool.AppendCertsFromPEM(ca) t := &http2.Transport{ TLSClientConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: certPool, }, } httpClient := http.Client{Transport: t} //call login loginRequest := api.LoginRequest{Username: "gavin", Password: "gavin"} loginrequestByte, _ := json.Marshal(loginRequest) request, _ := http.NewRequest("POST", urlpfx+"/login", strings.NewReader(string(loginrequestByte))) request.Header.Set("Content-Type", "application/json") loginResponse, _ := httpClient.Do(request) loginReplyBytes, _ := ioutil.ReadAll(loginResponse.Body) defer loginResponse.Body.Close() var loginReply api.LoginReply json.Unmarshal(loginReplyBytes, &loginReply) //fmt.Println("token:" + loginReply.Token) ///call say hello sayhelloRequest := api.PingMessage{Greeting: "gavin say "} sayhelloRequestByte, _ := json.Marshal(sayhelloRequest) request, _ = http.NewRequest("POST", urlpfx+"/sayhello", strings.NewReader(string(sayhelloRequestByte))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", loginReply.Token) sayhelloResponse, err := httpClient.Do(request) if err != nil { fmt.Println(err) } sayhelloReplyBytes, err := ioutil.ReadAll(sayhelloResponse.Body) if err != nil { fmt.Println(err) } log.Printf(string(sayhelloReplyBytes)) }
最后運行成功!!!!!!
備注 在win7 如果提示證書握手失敗, 請安裝ca.crt證書 到受信任中心 【openssl x509 -outform der -in ca.pem -out ca.crt】
下載地址 https://github.com/dz45693/gogrpcjwt.git
參考:
https://www.mdeditor.tw/pl/p1Vq/zh-hk
https://github.com/Bingjian-Zhu/go-grpc-example