zoukankan      html  css  js  c++  java
  • 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:Goinclude -I.  --go_out=plugins=grpc:. ./api/api.proto
    protoc -ID:Goinclude -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

    https://razeencheng.com/post/how-to-use-grpc-in-golang-03

  • 相关阅读:
    [Luogu P3626] [APIO2009] 会议中心
    杭电 1869 六度分离 (求每两个节点间的距离)
    杭电 1874 畅通工程续 (求某节点到某节点的最短路径)
    最短路径模板
    杭电 2544 最短路径
    POJ 1287 Networking (最小生成树模板题)
    NYOJ 1875 畅通工程再续 (无节点间距离求最小生成树)
    POJ 2485 Highways (求最小生成树中最大的边)
    杭电 1233 还是畅通工程 (最小生成树)
    杭电 1863 畅通工程 (最小生成树)
  • 原文地址:https://www.cnblogs.com/majiang/p/14235397.html
Copyright © 2011-2022 走看看