Go Grpc Jwt身份认证和Gateway集成以及HTTPS双向认证
书接上文 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2014-01-05 sharepoint 2010 记录管理 对象模型