Go Grpc Jwt身份认证

在 http 请求当中我们可以设置 header 用来传递数据,grpc 底层采用 http2 协议也是支持传递数据的,采用的是 metadata。 Metadata 对于 gRPC 本身来说透明, 它使得 client 和 server 能为对方提供本次调用的信息。就像一次 http 请求的 RequestHeader 和 ResponseHeader,http header 的生命周期是一次 http 请求, Metadata 的生命周期则是一次 RPC 调用。在 go 语言中,可以用 grpc.WithPerRPCCredentials 方法来实现。

简单说一下 我的demo吧,一般我们的api有一个login, 会返回token, 然后在请求及其他api的时候 就必须带上这个token。

首先我们创建一个项目文件夹 jwttoken,里面在创建一个api文件夹

1.创建/api/api.proto【为了简单我们没有引入其他的包】

syntax = "proto3";
package api;
 
 
service Ping {
  rpc Login (LoginRequest) returns (LoginReply) {}
  rpc SayHello(PingMessage) returns (PingMessage) {}
}
 
message LoginRequest{
  string username=1;
  string password=2;
}
message LoginReply{
  string status=1;
  string token=2;
}
message PingMessage {
  string greeting = 1;
}

2.编译该文件, 我一般习惯在根目录下操作:

protoc -I api/ -I${GOPATH}/src  --go_out=plugins=grpc:api api/api.proto

3.编写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
}

这个方法也很简单, 验证用户名和密码后 调用CreateToken 方法生成token,一般我们验证token 主要是获取 我们登陆的用户名,所以CheckAuth方法返回登录名,

4.具体实现在/api/authtoken.go方法里面:

package api
 
import (
    "context"
    "fmt"
    "time"
 
    "github.com/dgrijalva/jwt-go"
    "google.golang.org/grpc/metadata"
)
 
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{
        "authorization": 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, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return "", fmt.Errorf("ErrNoMetadataInContext")
    }
    // md 的类型是 type MD map[string][]string
    token, ok := md["authorization"]
    if !ok || len(token) == 0 {
        return "", fmt.Errorf("ErrNoAuthorizationInMetadata")
    }
    // 因此,token 是一个字符串数组,我们只用了 token[0]
    return token[0], nil
}
 
func CheckAuth(ctx context.Context) (username string) {
    tokenStr, err := getTokenFromContext(ctx)
    if err != nil {
        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
}

其中AuthToekn实现了PerRPCCredentials接口【需要实现GetRequestMetadata 和 RequireTransportSecurity方法】

5.编写main.go 【由于是简单的demo,我习惯吧客户端 和服务端放在一起】

package main
 
import (
    "context"
    "fmt"
    "log"
    "net"
 
    "jwtdemo/api"
 
    "google.golang.org/grpc"
)
 
func main() {
    go GrpcServer()
    go GrpcClient()
    var a string
    fmt.Scan(&a)
}
 
// main start a gRPC server and waits for connection
func GrpcServer() {
    // create a listener on TCP port 7777
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 7777))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    // create a server instance
    s := api.Server{}
    // create a gRPC server object
    grpcServer := grpc.NewServer()
    // attach the Ping service to the server
    api.RegisterPingServer(grpcServer, &s)
    // start the server
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %s", err)
    }
}
func GrpcClient() {
    var conn *grpc.ClientConn
    //call Login
    conn, err := grpc.Dial(":7777", 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(":7777", 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)
}

注意 这个的客户端代码 c = api.NewPingClient(conn) 而不是我以前 c := &Server{} 【这里要传递conn,不然后端无法获取token】,

运行结果如下 login 正常返回token, 在请求hello的时候 代入token, 后端正常获取到【后面再尝试gateway的集成 以及双向认证】

D:\GoProject\src\jwtdemo>go run main.go
Loginrequest:  gavin
Login Reply: status:"200" token:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJsb3JhLWFwcC1zZXJ2ZXIiLCJleHAiOjE2MDk3NDgxMzgsImlzcyI6ImxvcmEtYXBwLXNlcnZlciIsIm5iZiI6MTYwOTc0NDUzOCwic3ViIjoidXNlciIsInVzZXJuYW1lIjoiZ2F2aW4ifQ.jP-DjlFKNkS1o9KtnQuqdQMDk6ljd_8UK036mGD9m5o"
2021/01/04 15:15:38 Response from server: bar gavin

下载地址 https://github.com/dz45693/gogrpcjwt.git https://download.csdn.net/download/dz45693/14021638

参考:

https://medium.com/pantomath/how-we-use-grpc-to-build-a-client-server-system-in-go-dd20045fa1c2

https://blog.csdn.net/iotisan/article/details/103056622

https://blog.csdn.net/iotisan/article/details/103085973