在 gRPC 作为服务间通信的过程中为了数据安全我们一般要进行安全认证,比如 HTTP Restful Api 经常需要做接口之间调用的认证,我们来看下 gRPC 的认证方式。

安全证书认证

gRPC 建立于 HTTP/2 所以对 TLS 支持,我们可以使用服务端客户端双向证书验证通信来提供安全可靠的 gRPC。这里假如我们已经生成好了服务端和客户端的证书和私钥文件,并已经通过根证书做了签名。 我们先来看下服务端大概代码 server.go:

package main

import (
	......
)

......

func main() {
	certificate, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal(err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		log.Fatal(err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatal("failed to append certs")
	}
	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{certificate},
		ClientAuth:   tls.RequireAndVerifyClientCert, // 对客户端验证
		ClientCAs:    certPool, // CA根证书
	})
	grpcServer := grpc.NewServer(grpc.Creds(creds))

	.....

	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}

	grpcServer.Serve(lis)
}

将 TLS 证书配置传入credentials.NewTLS函数来生成证书,然后传入grpc.NewServer服务初始化

相应的客户端同样需要引入 CA 根证书和服务器名字来对服务器验证,客户端大概代码如下 client.go:

package main

import (
	......
)

func main() {
	certificate, err := tls.LoadX509KeyPair("client.crt", "client.key")
	if err != nil {
		log.Fatal(err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		log.Fatal(err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatal("failed to append ca certs")
	}

	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{certificate},
		ServerName:   "server.io", // 服务器名称,证书签名是指定的
		RootCAs:      certPool, // CA 根证书
	})

	conn, err := grpc.Dial("localhost:1234", grpc.WithTransportCredentials(creds))

	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	......
}

Token 认证

gRPC 可以为每个方法的调用进行认证,从而对不同的用户 token 进行不同的访问权限控制,首先需要实现 grpc.PerRPCCredentials 接口:

type PerRPCCredentials interface {

    GetRequestMetadata(ctx context.Context, uri ...string) (
        map[string]string,    error,
    )// 返回认证需要的信息
     RequireTransportSecurity() bool // 是否要求底层使用安全连接
}

我们先来简单实现该接口,用户名和密码的方式来认证 auth.go:

package auth

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
)

type Authentication struct {
	User     string
	Password string
}

func (a *Authentication) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
	return map[string]string{"user": a.User, "password": a.Password}, nil
}

func (a *Authentication) RequireTransportSecurity() bool {
	return false
}

func (a *Authentication) Auth(ctx context.Context) error {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return fmt.Errorf("missing credentials")
	}

	var appid string
	var appkey string

	if val, ok := md["user"]; ok {
		appid = val[0]
	}

	if val, ok := md["password"]; ok {
		appkey = val[0]
	}

	if appid != a.User || appkey != a.Password{
		return grpc.Errorf(codes.Unauthenticated, "invalid token")
	}

	return nil
}

Auth 是我们自定义的方法用于实现验证的逻辑,这里我们只是简单的验证传递过来的 appid 和 appkey 是否和我们初始的用户名和密码匹配。通过 metadata.FromIncomingContext 从 ctx 上下文中获取元信息,然后取出相应的认证信息进行认证。认证失败,则返回一个 codes.Unauthenticated 类型错误。

服务端中被调用的方法Publish添加 Auth 来认证客户端 server.go:

package main

import (
	"context"
	dockerPubsub "github.com/docker/docker/pkg/pubsub"
	"google.golang.org/grpc"
	"grpc/pubsub/proto"
	"grpc/token/auth"
	"log"
	"net"
	"strings"
	"time"
)

type PubsubService struct {
	pub  *dockerPubsub.Publisher
	auth *auth.Authentication
}

func NewPubsubService() *PubsubService {
	return &PubsubService{
		pub:  dockerPubsub.NewPublisher(100*time.Millisecond, 10),
		auth: &auth.Authentication{User: "gopher", Password: "password"},
	}
}

func (p *PubsubService) Publish(ctx context.Context, arg *pubsub.String) (*pubsub.String, error) {
	if err := p.auth.Auth(ctx); err != nil {// 对Publish方法认证
		return nil, err
	}

	p.pub.Publish(arg.GetValue())
	return &pubsub.String{}, nil
}

func (p *PubsubService) Subscribe(arg *pubsub.String, stream pubsub.PubsubService_SubscribeServer) error {
	ch := p.pub.SubscribeTopic(func(v interface{}) bool {
		if key, ok := v.(string); ok {
			if strings.HasPrefix(key, arg.GetValue()) {
				return true
			}
		}
		return false
	})

	for v := range ch {
		if err := stream.Send(&pubsub.String{Value: v.(string)}); err != nil {
			return err
		}
	}

	return nil
}

func main() {
	grpcServer := grpc.NewServer()
	pubsub.RegisterPubsubServiceServer(grpcServer, NewPubsubService())

	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}

	grpcServer.Serve(lis)
}

客户端调用的时候通过 grpc.WithPerRPCCredentials 函数将 Authentication 对象传入 grpc.Dial 参数 client.go:

package main

import (
	"context"
	"google.golang.org/grpc"
	"grpc/pubsub/proto"
	auth2 "grpc/token/auth"
	"log"
)

func main() {
	auth := auth2.Authentication{
		User:     "gopher",
		Password: "password",
	}
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))

	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	client := pubsub.NewPubsubServiceClient(conn)
	_, err = client.Publish(context.Background(), &pubsub.String{Value: "golang: hello Go"})
	if err != nil {
		log.Fatal(err)
	}

转载请注明: 转载自Ryan 是菜鸟 | LNMP 技术栈笔记

如果觉得本篇文章对您十分有益,何不 打赏一下

谢谢打赏

本文链接地址: gRPC 安全认证

知识共享许可协议 本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。