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

  • 相关阅读:
    JVM执行子系统探究——类文件结构初窥
    解决nexus3报Cannot open local storage 'component' with mode=rw的异常问题
    基础架构之spring cloud基础架构
    基础架构之持续发布
    基础架构之持续集成
    基础架构之Gitlab Runner
    基础架构之GitLab
    基础架构之Docker私有库
    基础架构之Maven私有库
    基础架构之Mongo
  • 原文地址:https://www.cnblogs.com/majiang/p/14235397.html
Copyright © 2011-2022 走看看