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

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

posted on 2021-01-05 13:56  dz45693  阅读(3014)  评论(0编辑  收藏  举报

导航