gRPC call authentication


Introduction


gRPC uses a strict authentication mechanism, which can use TLS to realize the encrypted data exchange between the client and the server; it can also verify the identity of the caller and use different calling credential technologies (such as token-based authentication, etc.) to implement access control functions.

gRPC provides the client with the ability to insert credentials (such as username and password) in each call, and the gRPC server can intercept requests from clients and check the credentials of each incoming call.


Basic certification


Introduction

In the Basic authentication mechanism, the request sent by the client has Authorization header information, the value of the header information starts with the word Basic, followed by a space and a base64 encoded string <username>:<password>, if the username and password Both are admin, the header information will be as follows:

Authorization: Basic YWRtaW46YWRtaW4=

gRPC does not advocate the use of usernames and passwords to authenticate services, because there is no time limit for usernames and passwords compared to other tokens such as JSON Web Taken (JWT) and OAuth2 Access Token.


program example

(1) In any directory, create server and client directories to store server and client files, proto directory to write IDL basic.protofiles, and cert directory to store certificate files. The specific directory structure is as follows:

BasicAuth
├── client
│   ├── cert
│   └── proto
│       └── basic.proto
└── server
    ├── cert
    └── proto
        └── basic.proto

basic.protoThe specific content of the file is as follows:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package pb; // 包名

// 定义服务
service Greeter {
    	// SayHello 方法
    	rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    	string name = 1;
}

// 响应消息
message HelloResponse {
    	string reply = 1;
}

(2) According to the generated CA certificate, move the following corresponding certificates to the server and client directories, generate the gRPC source code program, and execute the following commands in the proto directory:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

The directory structure after correct generation is as follows:

BasicAuth
├── client
├── cert
│   ├── ca.crt
│   ├── server.key
│   └── server.pem
│   └── proto
│       ├── basic_grpc.pb.go
│       ├── basic.pb.go
│       └── basic.proto
└── server
    ├── cert
    │   ├── ca.crt
    │   ├── server.key
    │   └── server.pem
    └── proto
        ├── basic_grpc.pb.go
        ├── basic.pb.go
        └── basic.proto

(3) Initialize the project ( ) in the server directory, and write a server-side program to override the defined method. The specific code of the program is as follows:go mod init server

package main

import (
        "context"
        "crypto/tls"
        "encoding/base64"
        pb "server/proto"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/credentials"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc/status"
        "log"
        "net"
        "path/filepath"
        "strings"
)

// server is used to implement ecommerce/product_info.
type server struct {
    
    
        pb.UnimplementedGreeterServer
}

var (
        port = ":50051"
        errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata!")
        errInvalidToken    = status.Errorf(codes.Unauthenticated, "invalid credentials, 调用认证失败!")
)

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
        return &pb.HelloResponse{
    
    Reply: "Hello " + in.Name}, nil
}

func main() {
    
    
        cert, err := tls.LoadX509KeyPair(filepath.Join("cert", "server.pem"),
                filepath.Join("cert", "server.key"))
        if err != nil {
    
    
                log.Fatalf("failed to load key pair: %s", err)
        }
        // 通过 TLS 服务器证书添加新的服务器选项(grpc.ServerOption)
        // grpc.UnaryInterceptor 是一个函数,在其中可以添加拦截器来拦截所以来自客户端的请求,向该函数传递一个引用(ensureValidBasicCredentials),拦截器会将所有的客户端请求传递到该函数
        opts := []grpc.ServerOption{
    
    
                // Enable TLS for all incoming connections.
                grpc.Creds(credentials.NewServerTLSFromCert(&cert)),

                grpc.UnaryInterceptor(ensureValidBasicCredentials),
        }

        s := grpc.NewServer(opts...)
        pb.RegisterGreeterServer(s, &server{
    
    })
        // Register reflection service on gRPC server.
        //reflection.Register(s)

        lis, err := net.Listen("tcp", port)
        if err != nil {
    
    
                log.Fatalf("failed to listen: %v", err)
        }

        if err := s.Serve(lis); err != nil {
    
    
                log.Fatalf("failed to serve: %v", err)
        }
}

// valid validates the authorization.
func valid(authorization []string) bool {
    
    
        if len(authorization) < 1 {
    
    
                return false
        }
        token := strings.TrimPrefix(authorization[0], "Basic ")
        return token == base64.StdEncoding.EncodeToString([]byte("admin:cqupthao"))
}

// ensureValidToken ensures a valid token exists within a request's metadata. If
// the token is missing or invalid, the interceptor blocks execution of the
// handler and returns an error. Otherwise, the interceptor invokes the unary
// handler
// 校验调用者身份,context.Context 对象包含所需的元数据,在请求的周期内一直存在
func ensureValidBasicCredentials(ctx context.Context, req interface{
    
    }, info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler) (interface{
    
    }, error) {
    
    
        // 从上下文中抽取元数据,获取 authentication 的值并校验凭证,metadata.MD 中的键会被标准化为小写字母,需检验键的值
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
    
    
                return nil, errMissingMetadata
        }
        // The keys within metadata.MD are normalized to lowercase.
        // See: https://godoc.org/google.golang.org/grpc/metadata#New
        if !valid(md["authorization"]) {
    
    
                return nil, errInvalidToken
        }
        // Continue execution of handler after ensuring a valid token.
        return handler(ctx, req)
}

(4) In the client directory, write a client program to call the service. The specific code of the program is as follows:

package main

import (
        "context"
        "encoding/base64"
        "google.golang.org/grpc/credentials"
        "log"
        "path/filepath"
        "time"
        "flag"
        pb "client/proto"
        "google.golang.org/grpc"
)

const (
        defaultName = "cqupthao!"
        address = "localhost:50051"
)

var (
        name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    
    

        creds, err := credentials.NewClientTLSFromFile(filepath.Join("cert", "server.pem"),"*.mszlu.com")

        if err != nil {
    
    
                log.Fatalf("failed to load credentials: %v", err)
        }
        // 使用有效的用户凭证(用户名和密码)初始化 auth 变量
        auth := basicAuth{
    
    
                username: "admin",
                password: "cqupthao",
        }
        opts := []grpc.DialOption{
    
    
                grpc.WithPerRPCCredentials(auth),
                // transport credentials,传递 auth 变量给 grpc.WithPerRPCCredentials 函数
                grpc.WithTransportCredentials(creds),
        }

        // Set up a connection to the server.
        conn, err := grpc.Dial(address, opts...)
        if err != nil {
    
    
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        // Contact the server and print out its response.
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()

        r, err := c.SayHello(ctx, &pb.HelloRequest{
    
    Name: *name})
        if err != nil {
    
    
                log.Fatalf("调用服务失败: %v", err)
        }
        log.Printf("调用服务成功: %s", r.GetReply())
}

// 定义结构体存放要注入 RPC 的字段集合(用户的凭证)
type basicAuth struct {
    
    
        username string
        password string
}

// 实现 GetRequestMetadata() 方法并将用户凭证转换为请求元数据
func (b basicAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
    
    
        auth := b.username + ":" + b.password
        enc := base64.StdEncoding.EncodeToString([]byte(auth))
        // 键是 authorization,值是由 Basic 和加上 < 用户名 >:< 密码 > 的 base64 算法计算结果所组成
        return map[string]string{
    
    
                "authorization": "Basic " + enc,
        }, nil
}

func (b basicAuth) RequireTransportSecurity() bool {
    
    
        return true // 传递凭证是是否启用通道安全
}

Execute the programs on the Server side and the Client side, and output the following results:

2023/02/25 13:56:58 调用服务成功: Hello cqupthao!

If the wrong username and password are passed, the following results will be output:

2023/02/25 13:57:31 调用服务失败: rpc error: code = Unauthenticated desc = invalid credentials, 调用认证失败!
exit status 1

Metadata + interceptor authentication


Introduction

gRPC applications typically share information via RPC between gRPC services and consumers, and in most scenarios, information directly related to the service business logic and consumers is included as part of the remote method call parameters.


program example

(1) In any directory, create server and client directories to store server and client files, and proto directory is used to write IDL metadata.protofiles. The specific directory structure is as follows:

MetadataInterceptorAuth
├── client
│   └── proto
│       └── metadata.proto
└── server
    └── proto
        └── metadata.proto

metadata.protoThe specific content of the file is as follows:

syntax = "proto3";
option go_package="../proto";

package metadata;
// The greeting service definition.
service Greeter {
  		//  Sends a greeting
 	 	rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  	string name = 1;
}

// The response message containing the greetings
message HelloReply {
  	string message = 1;
}

(2) To generate the gRPC source code program, execute the following command in the proto directory:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

The directory structure after correct generation is as follows:

MetadataInterceptorAuth
├── client
│   └── proto
│       ├── metadata_grpc.pb.go
│       ├── metadata.pb.go
│       └── metadata.proto
└── server
    └── proto
        ├── metadata_grpc.pb.go
        ├── metadata.pb.go
        └── metadata.proto

(3) Initialize the project ( ) in the server directory, and write a server-side program to override the defined method. The specific code of the program is as follows:go mod init server

package main

import (
        "context"
        "fmt"
        "log"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "net"
        pb "server/proto"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc/status"
)

type Server struct {
    
    
        pb.UnimplementedGreeterServer
}

func (s *Server) SayHello(ctx context.Context,request *pb.HelloRequest) (*pb.HelloReply,error){
    
    
        return &pb.HelloReply{
    
    Message: "Hello "+ request.Name},nil
}

func main() {
    
    
        interceptor := func(ctx context.Context, req interface{
    
    }, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{
    
    }, err error) {
    
    
                md ,ok := metadata.FromIncomingContext(ctx)
                if !ok {
    
    
                        return nil,status.Errorf(codes.Unauthenticated,"无 token 信息!")
                }

                var (
                        user string
                        password string
                )
                if id,ok :=  md["user"];ok {
    
    
                        user = id[0]
                }
                if key ,ok := md["password"]; ok {
    
    
                        password = key[0]
                }

                if user != "admin" || password != "cqupthao" {
    
    
                        return nil,status.Errorf(codes.Unauthenticated,"验证失败!")
                }

                fmt.Println("前置处理逻辑执行!")
                resp ,err = handler(ctx,req)
                fmt.Println("后置处理逻辑执行!")
                return resp,err
        }
        opts := []grpc.ServerOption{
    
    }
        opts = append(opts,grpc.UnaryInterceptor(interceptor))

        lis,err := net.Listen("tcp","0.0.0.0:1234")
        if err != nil {
    
    
                log.Println("failed to listen: %v ", err)
        }

        grpcs := grpc.NewServer(opts ... )

        pb.RegisterGreeterServer(grpcs,new(Server))

        err = grpcs.Serve(lis)
        if err != nil{
    
    
                log.Println("failed to server: %v ", err)
        }

}

(4) In the client directory, write a client program to call the service. The specific code of the program is as follows:

package main

import (
        "context"
        "fmt"
        "google.golang.org/grpc"
        pb "client/proto"
        "google.golang.org/grpc/metadata"
)

func main() {
    
    
        conn,err := grpc.Dial("127.0.0.1:1234",grpc.WithInsecure())
        if err != nil {
    
    
                panic(err)
        }
        defer  conn.Close()

        m := map[string]string{
    
    
                "user" : "admin",
                "password" : "cqupthao",
        }
        md := metadata.New(m)

        client := pb.NewGreeterClient(conn)

        ctx := metadata.NewOutgoingContext(context.Background(),md)
        replay ,err := client.SayHello(ctx,&pb.HelloRequest{
    
    Name: "cqupthao"})
        if err != nil {
    
    
                panic(err)
        }
        fmt.Println(replay)
}

Execute the programs on the Server side and the Client side, and output the following results:

message:"Hello cqupthao"

If the wrong username and password are passed, the following results will be output:

2023/02/25 21:49:12 failed to call: %v  rpc error: code = Unauthenticated desc = 验证失败!
<nil>

Oauth 2.0 Authentication


Introduction

OAuth 2.0 is a framework for access delegation, which allows users to grant limited access to services on their own behalf, instead of giving full service permissions to services like username and password.

In the OAuth 2.0 process, there are four main roles (client, authorization server, resource server and resource owner), and the client needs to obtain a token from the authorization server (arbitrary string ), the token must have a certain length and should be unpredictable; after the client receives the token, it can use it to send a request to the resource server, and the resource server will communicate with the corresponding authorization server and verify the token If the owner of the resource verifies it, the client can serve the resource.


program example

(1) In any directory, create server and client directories to store server and client files, proto directory to write IDL basic.protofiles, and cert directory to store certificate files. The specific directory structure is as follows:

TokenBasedAuth
├── client
│   ├── cert
│   └── proto
│       └── basic.proto
└── server
    ├── cert
    └── proto
        └── basic.proto

basic.protoThe specific content of the file is as follows:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package pb; // 包名

// 定义服务
service Greeter {
    	// SayHello 方法
    	rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    	string name = 1;
}

// 响应消息
message HelloResponse {
    	string reply = 1;
}

(2) According to the generated CA certificate, move the following corresponding certificates to the server and client directories, enter the proto directory to generate the gRPC source code program, and execute the following commands in the proto directory:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

The directory structure after correct generation is as follows:

TokenBasedAuth
├── client
├── cert
│   ├── ca.crt
│   ├── server.key
│   └── server.pem
│   └── proto
│       ├── basic_grpc.pb.go
│       ├── basic.pb.go
│       └── basic.proto
└── server
    ├── cert
    │   ├── ca.crt
    │   ├── server.key
    │   └── server.pem
    └── proto
        ├── basic_grpc.pb.go
        ├── basic.pb.go
        └── basic.proto

(3) Initialize the project ( ) in the server directory, and write a server-side program to override the defined method. The specific code of the program is as follows:go mod init server

package main

import (
        "context"
        "crypto/tls"
        "log"
        "net"
        "path/filepath"
        "strings"
        pb "server/proto"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/credentials"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc/status"
)

// server is used to implement ecommerce/product_info.
type server struct {
    
    
        pb.UnimplementedGreeterServer
}

var (
        port               = ":50051"
        crtFile            = filepath.Join("cert", "server.pem")
        keyFile            = filepath.Join("cert", "server.key")
        errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata!")
        errInvalidToken    = status.Errorf(codes.Unauthenticated, "invalid token, 调用认证失败!")
)

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
        return &pb.HelloResponse{
    
    Reply: "Hello " + in.Name}, nil
}

func main() {
    
    
        cert, err := tls.LoadX509KeyPair(crtFile, keyFile)
        if err != nil {
    
    
                log.Fatalf("failed to load key pair: %s", err)
        }
        opts := []grpc.ServerOption{
    
    
                // Enable TLS for all incoming connections. 
                grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
				// 添加新的服务选项(grpc.ServerOption)以及 TLS 服务证书,借助 grpc.UnaryInterceptor 函数添加拦截器以拦截所有来自客户端请求
                grpc.UnaryInterceptor(ensureValidToken),
        }

        s := grpc.NewServer(opts...)
        pb.RegisterGreeterServer(s, &server{
    
    })
        // Register reflection service on gRPC server.
        //reflection.Register(s)

        lis, err := net.Listen("tcp", port)
        if err != nil {
    
    
                log.Fatalf("failed to listen: %v", err)
        }

        if err := s.Serve(lis); err != nil {
    
    
                log.Fatalf("failed to serve: %v", err)
        }
}

// valid validates the authorization.
func valid(authorization []string) bool {
    
    
        if len(authorization) < 1 {
    
    
                return false
        }
        token := strings.TrimPrefix(authorization[0], "Bearer ")
        // Perform the token validation here. For the sake of this example, the code
        // here forgoes any of the usual OAuth2 token validation and instead checks
        // for a token matching an arbitrary string.
        return token == "some-secret-token"
}

// ensureValidToken ensures a valid token exists within a request's metadata. If
// the token is missing or invalid, the interceptor blocks execution of the
// handler and returns an error. Otherwise, the interceptor invokes the unary

// 定义名为 ensureVaildToken 的函数来校验令牌,若令牌丢失或不合法,则拦截器会阻止执行并同时错误;否则,拦截器调用传递上下文和接口的下一个 handler
func ensureValidToken(ctx context.Context, req interface{
    
    }, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{
    
    }, error) {
    
    
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
    
    
                return nil, errMissingMetadata
        }
        // The keys within metadata.MD are normalized to lowercase.
        // See: https://godoc.org/google.golang.org/grpc/metadata#New
        if !valid(md["authorization"]) {
    
    
                return nil, errInvalidToken
        }
        // Continue execution of handler after ensuring a valid token.
        return handler(ctx, req)
}

(4) In the client directory, write a client program to call the service. The specific code of the program is as follows:

package main

import (
        "context"
        "log"
        "path/filepath"
        "time"
        "flag"
        "google.golang.org/grpc/credentials"
        "google.golang.org/grpc/credentials/oauth"
        pb "client/proto"
        "golang.org/x/oauth2"
        "google.golang.org/grpc"
)

const (
        address  = "localhost:50051"
        hostname = "*.mszlu.com"
        defaultName = "cqupthao!"
)

var (
        name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    
    
        // Set up the credentials for the connection.
        //设置连接的凭证,需要提供 OAuth 令牌值来创建凭证(这里使用一个硬编码的字符串值作为令牌的值)
        perRPC := oauth.NewOauthAccess(fetchToken())

        crtFile := filepath.Join("cert", "server.pem")
        creds, err := credentials.NewClientTLSFromFile(crtFile, hostname)
        if err != nil {
    
    
                log.Fatalf("failed to load credentials: %v", err)
        }
        opts := []grpc.DialOption{
    
    
        		// 配置 gRPC DialOption 为同一个连接的所有 RPC 使用同一个令牌
                grpc.WithPerRPCCredentials(perRPC),
                // transport credentials.
                grpc.WithTransportCredentials(creds),
        }

        // Set up a connection to the server.
        conn, err := grpc.Dial(address, opts...)
        if err != nil {
    
    
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        // Contact the server and print out its response.
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{
    
    Name: *name})
        if err != nil {
    
    
                log.Fatalf("调用服务失败: %v", err)
        }
        log.Printf("调用服务成功: %s", r.GetReply())
}

func fetchToken() *oauth2.Token {
    
    
        return &oauth2.Token{
    
    
                AccessToken: "some-secret-token",
        }
}

Execute the programs on the Server side and the Client side, and output the following results:

2023/02/25 15:45:35 调用服务成功: Hello cqupthao!

If the wrong one is passed Token, the following result will be output:

2023/02/25 15:46:24 调用服务失败: rpc error: code = Unauthenticated desc = invalid token, 调用认证失败!
exit status 1

  • Reference link: gRPC official website tutorial

  • Reference book: "gRPC and Cloud Native Application Development: Taking Go and Java as Examples" ([Sri Lanka] Kashan Indrasili Danish Kurup)

Guess you like

Origin blog.csdn.net/qq_46457076/article/details/129214872