gRPC 的调用认证

文章目录

  • 简介
  • Basic 认证
    • 简介
    • 程序示例
  • Metadata + 拦截器认证
    • 简介
    • 程序示例
  • Oauth 2.0 认证
    • 简介
    • 程序示例


简介


gRPC 使用严格的认证机制,可以使用 TLS 实现客户端和服务端的加密数据交换;还可以验证调用者的身份并用不同的调用凭证技术(如基于令牌的认证等)实现访问的控制功能。

gRPC 为客户端提供了在每次调用中插入凭证(如用户名和密码)的功能,gRPC 服务端能够拦截来自客户端的请求并检查每一个传入调用的凭证。


Basic 认证


简介

在 Basic 认证机制中,客户端发送的请求带有 Authorization 头信息,该头信息的值以 Basic 单词 开头,随后是一个空格和 base64 编码的字符串 < 用户名 >:< 密码 > ,如果用户名和密码均为 admin ,则头信息将如下所示:

Authorization: Basic YWRtaW46YWRtaW4=

gRPC 不提倡使用用户名和密码来对服务进行认证,因为相对于 JSON Web Taken(JWT)和 OAuth2 Access Token 等其它令牌,用户名和密码没有时间方面的限制。


程序示例

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 basic.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

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

basic.proto 文件的具体内容如下所示:

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)根据生成的 CA 证书,移动以下相应的证书到服务端和客户端目录下,生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

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

正确生成后的目录结构如下所示:

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)在 server 目录下初始化项目( go mod init server ),编写 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)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

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 // 传递凭证是是否启用通道安全
}

执行 Server 端和 Client 端的程序,输出如下的结果:

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

若传递错误的用户名和密码,则输出如下的结果:

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

Metadata + 拦截器认证


简介

gRPC 应用程序通常会通过 gRPC 服务和消费者之间的 RPC 来共享信息,在大多数场景中,与服务业务逻辑和消费者直接相关的信息会作为远程方法调用参数的一部分。


程序示例

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 metadata.proto 文件,具体的目录结构如下所示:

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

metadata.proto 文件的具体内容如下所示:

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)生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

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

正确生成后的目录结构如下所示:

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

(3)在 server 目录下初始化项目( go mod init server ),编写 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)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

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)
}

执行 Server 端和 Client 端的程序,输出如下的结果:

message:"Hello cqupthao"

若传递错误的用户名和密码,则输出如下的结果:

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


Oauth 2.0 认证


简介

OAuth 2.0 是一个用于访问委托的框架,它允许用户以自己的名义授予服务有限的访问权限,而不会像用户名和密码方式那样给予服务全部服务权限。

在 OAuth 2.0 的流程中,有四个主要的角色(客户端,授权服务器,资源服务器和资源所有者),客户端访问资源服务器上的资源需要获取一个来自授权服务器的令牌(任意的字符串),该令牌必须具备一定的长度并且应该是不可预知的;客户端收到该令牌后,就可以使用它向资源服务器发送请求,资源服务器会与对应的授权服务器通信并校验该令牌,若该资源的所有者校验了它,则客户端就可以服务该资源了。


程序示例

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 basic.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

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

basic.proto 文件的具体内容如下所示:

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)根据生成的 CA 证书,移动以下相应的证书到服务端和客户端目录下,进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

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

正确生成后的目录结构如下所示:

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)在 server 目录下初始化项目( go mod init server ),编写 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)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

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",
        }
}

执行 Server 端和 Client 端的程序,输出如下的结果:

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

若传递错误的 Token ,则输出如下的结果:

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

  • 参考链接: gRPC 官网教程

  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)

你可能感兴趣的:(微服务系列,golang,rpc,后端,服务器)